diff --git a/.gitignore b/.gitignore index df702bf32..66c08a1f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ .DS_Store - *.log - +*.sublime-project +*.sublime-workspace +.npmrc +.prettierignore +.pi/sandbox.json +.pi/allowed-paths.json node_modules +CLAUDE.md \ No newline at end of file diff --git a/README.md b/README.md index 6629f6a98..14403f081 100644 --- a/README.md +++ b/README.md @@ -32,26 +32,23 @@ Help the project become sustainable by sponsoring us on - + - - + - + - - + - + - - + @@ -65,46 +62,45 @@ Help the project become sustainable by sponsoring us on - + - - + + - + - - + - + - + - + - + diff --git a/assets/sponsors/ice-open-network-logomark-dark.png b/assets/sponsors/ice-open-network-logomark-dark.png deleted file mode 100644 index 14df0b272..000000000 Binary files a/assets/sponsors/ice-open-network-logomark-dark.png and /dev/null differ diff --git a/assets/sponsors/ice-open-network-logomark.png b/assets/sponsors/ice-open-network-logomark.png deleted file mode 100644 index 3f8deeb01..000000000 Binary files a/assets/sponsors/ice-open-network-logomark.png and /dev/null differ diff --git a/assets/sponsors/inspatial-logomark-dark.png b/assets/sponsors/inspatial-logomark-dark.png deleted file mode 100644 index f1e32556d..000000000 Binary files a/assets/sponsors/inspatial-logomark-dark.png and /dev/null differ diff --git a/assets/sponsors/inspatial-logomark.png b/assets/sponsors/inspatial-logomark.png deleted file mode 100644 index b792c6aac..000000000 Binary files a/assets/sponsors/inspatial-logomark.png and /dev/null differ diff --git a/assets/sponsors/juspay-logomark-dark.png b/assets/sponsors/juspay-logomark-dark.png deleted file mode 100644 index 8b12b058a..000000000 Binary files a/assets/sponsors/juspay-logomark-dark.png and /dev/null differ diff --git a/assets/sponsors/juspay-logomark.png b/assets/sponsors/juspay-logomark.png deleted file mode 100644 index 59b5f5e37..000000000 Binary files a/assets/sponsors/juspay-logomark.png and /dev/null differ diff --git a/assets/sponsors/lambdatest-logomark-dark.png b/assets/sponsors/lambdatest-logomark-dark.png deleted file mode 100644 index 2cd6faab0..000000000 Binary files a/assets/sponsors/lambdatest-logomark-dark.png and /dev/null differ diff --git a/assets/sponsors/lambdatest-logomark.png b/assets/sponsors/lambdatest-logomark.png deleted file mode 100644 index 1cb04cd85..000000000 Binary files a/assets/sponsors/lambdatest-logomark.png and /dev/null differ diff --git a/assets/sponsors/placeholder-large.png b/assets/sponsors/placeholder-large.png new file mode 100644 index 000000000..5c713a852 Binary files /dev/null and b/assets/sponsors/placeholder-large.png differ diff --git a/assets/sponsors/testmu-ai-logomark-dark.png b/assets/sponsors/testmu-ai-logomark-dark.png new file mode 100644 index 000000000..f2307085a Binary files /dev/null and b/assets/sponsors/testmu-ai-logomark-dark.png differ diff --git a/assets/sponsors/testmu-ai-logomark.png b/assets/sponsors/testmu-ai-logomark.png new file mode 100644 index 000000000..19902d764 Binary files /dev/null and b/assets/sponsors/testmu-ai-logomark.png differ diff --git a/assets/sponsors/warp-logomark-dark.png b/assets/sponsors/warp-logomark-dark.png deleted file mode 100644 index c10fbdf0c..000000000 Binary files a/assets/sponsors/warp-logomark-dark.png and /dev/null differ diff --git a/assets/sponsors/warp-logomark.png b/assets/sponsors/warp-logomark.png deleted file mode 100644 index 42dc42ae8..000000000 Binary files a/assets/sponsors/warp-logomark.png and /dev/null differ diff --git a/dist/bundles/anime.esm.js b/dist/bundles/anime.esm.js index bcb2acd46..65e03b2c6 100644 --- a/dist/bundles/anime.esm.js +++ b/dist/bundles/anime.esm.js @@ -1,8 +1,8 @@ /** * Anime.js - ESM bundle - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ // Global types @@ -22,7 +22,7 @@ * @property {Number|FunctionValue} [duration] * @property {Number|FunctionValue} [delay] * @property {Number} [loopDelay] - * @property {EasingParam} [ease] + * @property {EasingParam|FunctionValue} [ease] * @property {'none'|'replace'|'blend'|compositionTypes} [composition] * @property {(v: any) => any} [modifier] * @property {Callback} [onBegin] @@ -37,7 +37,7 @@ /** @typedef {JSAnimation|Timeline} Renderable */ /** @typedef {Timer|Renderable} Tickable */ /** @typedef {Timer&JSAnimation&Timeline} CallbackArgument */ -/** @typedef {Animatable|Tickable|WAAPIAnimation|Draggable|ScrollObserver|TextSplitter|Scope} Revertible */ +/** @typedef {Animatable|Tickable|WAAPIAnimation|Draggable|ScrollObserver|TextSplitter|Scope|AutoLayout} Revertible */ // Stagger types @@ -46,7 +46,8 @@ * @callback StaggerFunction * @param {Target} [target] * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [targets] + * @param {Tween|null} [prevTween] * @param {Timeline} [tl] * @return {T} */ @@ -54,9 +55,9 @@ /** * @typedef {Object} StaggerParams * @property {Number|String} [start] - * @property {Number|'first'|'center'|'last'|'random'} [from] + * @property {Number|'first'|'center'|'last'|'random'|Array.} [from] * @property {Boolean} [reversed] - * @property {Array.} [grid] + * @property {Array.|Boolean} [grid] * @property {('x'|'y')} [axis] * @property {String|((target: Target, i: Number, length: Number) => Number)} [use] * @property {Number} [total] @@ -117,8 +118,8 @@ // A hack to get both ease names suggestions AND allow any strings // https://github.com/microsoft/TypeScript/issues/29729#issuecomment-460346421 -/** @typedef {(String & {})|EaseStringParamNames|EasingFunction|Spring} EasingParam */ -/** @typedef {(String & {})|EaseStringParamNames|WAAPIEaseStringParamNames|EasingFunction|Spring} WAAPIEasingParam */ +/** @typedef {(String & {})|EaseStringParamNames|EasingFunction|Spring|TweakRegister} EasingParam */ +/** @typedef {(String & {})|EaseStringParamNames|WAAPIEaseStringParamNames|EasingFunction|Spring|TweakRegister} WAAPIEasingParam */ // Spring types @@ -174,6 +175,7 @@ * @property {Boolean|ScrollObserver} [autoplay] * @property {Number} [frameRate] * @property {Number} [playbackRate] + * @property {Number} [priority] */ /** @@ -184,10 +186,11 @@ /** * @callback FunctionValue - * @param {Target} target - The animated target - * @param {Number} index - The target index - * @param {Number} length - The total number of animated targets - * @return {Number|String|TweenObjectValue|Array.} + * @param {Target} [target] - The animated target + * @param {Number} [index] - The target index + * @param {TargetsArray} [targets] - The array of all animated targets + * @param {Tween|null} [prevTween] - The previous sibling tween for the same target and property + * @return {Number|String|TweenObjectValue|EasingParam|Array.} */ /** @@ -205,7 +208,8 @@ * @property {String} property * @property {Target} target * @property {String|Number} _value - * @property {Function|null} _func + * @property {Function|null} _toFunc + * @property {Function|null} _fromFunc * @property {EasingFunction} _ease * @property {Array.} _fromNumbers * @property {Array.} _toNumbers @@ -255,7 +259,7 @@ // JSAnimation types /** - * @typedef {Number|String|FunctionValue} TweenParamValue + * @typedef {Number|String|FunctionValue|EasingParam} TweenParamValue */ /** @@ -270,7 +274,7 @@ * @typedef {Object} TweenParamsOptions * @property {TweenParamValue} [duration] * @property {TweenParamValue} [delay] - * @property {EasingParam} [ease] + * @property {EasingParam|FunctionValue} [ease] * @property {TweenModifier} [modifier] * @property {TweenComposition} [composition] */ @@ -354,13 +358,14 @@ * - `'label'` - Label: Position animation at a named label position (e.g., `'My Label'`)
* - `stagger(String|Nummber)` - Stagger multi-elements animation positions (e.g., 10, 20, 30...) * - * @typedef {TimelinePosition | StaggerFunction} TimelineAnimationPosition + * @typedef {TimelinePosition | StaggerFunction | TweakRegister} TimelineAnimationPosition */ /** * @typedef {Object} TimelineOptions * @property {DefaultsParams} [defaults] * @property {EasingParam} [playbackEase] + * @property {Boolean} [composition] */ /** @@ -377,8 +382,8 @@ * @callback WAAPIFunctionValue * @param {DOMTarget} target - The animated target * @param {Number} index - The target index - * @param {Number} length - The total number of animated targets - * @return {WAAPITweenValue} + * @param {DOMTargetsArray} targets - The array of all animated targets + * @return {WAAPITweenValue|WAAPIEasingParam} */ /** @@ -404,7 +409,7 @@ * @property {Number} [playbackRate] * @property {Number|WAAPIFunctionValue} [duration] * @property {Number|WAAPIFunctionValue} [delay] - * @property {WAAPIEasingParam} [ease] + * @property {WAAPIEasingParam|WAAPIFunctionValue} [ease] * @property {CompositeOperation} [composition] * @property {Boolean} [persist] * @property {Callback} [onComplete] @@ -535,6 +540,7 @@ * @property {Callback} [onEnterBackward] * @property {Callback} [onLeaveBackward] * @property {Callback} [onUpdate] + * @property {Callback} [onResize] * @property {Callback} [onSyncComplete] */ @@ -623,6 +629,26 @@ * @property {Boolean} [debug] */ +/** + * @typedef {Object} ScrambleTextParams + * @property {String|function(Target, Number, TargetsArray): String} [text] - the text to transition to, otherwise uses the original text + * @property {String|function(Target, Number, TargetsArray): String} [chars] - the characters used for scramble; named sets: 'lowercase', 'uppercase', 'numbers', 'symbols', 'braille', 'blocks', 'shades'; range syntax: 'A-Z', 'a-z0-9'; defaults to 'a-zA-Z0-9!%#_' + * @property {EasingParam} [ease] - the easing applied to the scramble animation + * @property {Number|'left'|'center'|'right'|'random'|'auto'} [from] - where the reveal wave starts from, 'auto' (default) uses 'left' when text grows and 'right' when it shrinks + * @property {Boolean} [reversed] - reverses the reveal order, so 'center' reveals from edges inward instead of center outward + * @property {Boolean|Number|String} [cursor] - characters displayed at the leading edge of the reveal wave; true uses '_', a number is a char code, a string is used directly + * @property {Number} [perturbation] - adds random timing offsets to each character's start and end, creating a more organic reveal + * @property {Number} [seed] - a seed for the random number generator to produce reproducible scramble sequences + * @property {Boolean|String} [override] - controls the starting appearance: false shows original text, true scrambles it (default), '' starts from blank, ' ' replaces characters with spaces, a custom string (supports range syntax like 'A-Z') uses its characters as scramble set + * @property {Number} [revealRate] - characters per second entering the active zone; higher values make the reveal wave move faster (default: 60) + * @property {Number} [settleDuration] - time in ms each character spends scrambling before settling into its final glyph (default: 300) + * @property {Number} [settleRate] - how many times per second scramble characters cycle in the active zone (default: 30) + * @property {Number|function(Target, Number, TargetsArray): Number} [duration] - if set to a value greater than 0, overrides the computed duration from interval and settle; if unset or 0, duration is calculated automatically from text length and timing parameters + * @property {Number|function(Target, Number, TargetsArray): Number} [revealDelay] - delay in ms before the reveal wave starts within the scramble animation + * @property {Number|function(Target, Number, TargetsArray): Number} [delay] - delay in ms before the entire scramble animation starts + * @property {function(String, Number): void} [onChange] - callback fired each time a character changes during scramble; receives the current scrambled text and the eased progress (0-1) + */ + // SVG types /** @@ -638,8 +664,10 @@ // TODO: Do we need to check if we're running inside a worker ? const isBrowser = typeof window !== 'undefined'; -/** @type {Window & {AnimeJS: Array}|null} */ -const win = isBrowser ? /** @type {Window & {AnimeJS: Array}} */(/** @type {unknown} */(window)) : null; +/** @typedef {Window & {AnimeJS: Array}|null} AnimeJSWindow + +/** @type {AnimeJSWindow} */ +const win = isBrowser ? /** @type {AnimeJSWindow} */(/** @type {unknown} */(window)) : null; /** @type {Document|null} */ const doc = isBrowser ? document : null; @@ -683,7 +711,6 @@ const isRegisteredTargetSymbol = Symbol(); const isDomSymbol = Symbol(); const isSvgSymbol = Symbol(); const transformsSymbol = Symbol(); -const morphPointsSymbol = Symbol(); const proxyTargetSymbol = Symbol(); // Numbers @@ -691,7 +718,7 @@ const proxyTargetSymbol = Symbol(); const minValue = 1e-11; const maxValue = 1e12; const K = 1e3; -const maxFps = 120; +const maxFps = 240; // Strings @@ -707,6 +734,7 @@ const shortTransforms = /*#__PURE__*/ (() => { })(); const validTransforms = [ + 'perspective', 'translateX', 'translateY', 'translateZ', @@ -721,9 +749,6 @@ const validTransforms = [ 'skew', 'skewX', 'skewY', - 'matrix', - 'matrix3d', - 'perspective', ]; const transformsFragmentStrings = /*#__PURE__*/ validTransforms.reduce((a, v) => ({...a, [v]: v + '('}), {}); @@ -735,6 +760,7 @@ const noop = () => {}; // Regex +const validRgbHslRgx = /\)\s*[-.\d]/; const hexTestRgx = /(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i; const rgbExecRgx = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i; const rgbaExecRgx = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i; @@ -745,12 +771,23 @@ const digitWithExponentRgx = /[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi; // export const unitsExecRgx = /^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)+([a-z]+|%)$/i; const unitsExecRgx = /^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i; const lowerCaseRgx = /([a-z])([A-Z])/g; -const transformsExecRgx = /(\w+)(\([^)]+\)+)/g; // Match inline transforms with cacl() values, returns the value wrapped in () const relativeValuesExecRgx = /(\*=|\+=|-=)/; const cssVariableMatchRgx = /var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/; +/** + * @typedef {Object} EditorGlobals + * @property {boolean} showPanel + * @property {boolean} synced + * @property {Function} addAnimation + * @property {Function} addTimeline + * @property {Function} addTimelineChild + * @property {Function} resolveStagger + * @property {Object|null} _head + * @property {Object|null} _tail + */ + /** @type {DefaultsParams} */ const defaults = { id: null, @@ -794,9 +831,11 @@ const globals = { timeScale: 1, /** @type {Number} */ tickThreshold: 200, + /** @type {EditorGlobals|null} */ + editor: null, }; -const globalVersions = { version: '4.2.2', engine: null }; +const globalVersions = { version: '4.4.1', engine: null }; if (isBrowser) { if (!win.AnimeJS) win.AnimeJS = []; @@ -847,8 +886,8 @@ const isHex = a => hexTestRgx.test(a); const isRgb = a => stringStartsWith(a, 'rgb'); /**@param {any} a @return {Boolean} */ const isHsl = a => stringStartsWith(a, 'hsl'); -/**@param {any} a @return {Boolean} */ -const isCol = a => isHex(a) || isRgb(a) || isHsl(a); +/**@param {any} a @return {Boolean} */ // Make sure boxShadow syntax like 'rgb(255, 0, 0) 0px 0px 6px 0px' is not a valid color type +const isCol = a => isHex(a) || ((isRgb(a) || isHsl(a)) && (a[a.length - 1] === ')' || !validRgbHslRgx.test(a))); /**@param {any} a @return {Boolean} */ const isKey = a => !globals.defaults.hasOwnProperty(a); @@ -912,8 +951,6 @@ const _round = Math.round; */ const clamp$1 = (v, min, max) => v < min ? min : v > max ? max : v; -const powCache = {}; - /** * Rounds a number to specified decimal places * @@ -921,13 +958,12 @@ const powCache = {}; * @param {Number} decimalLength - Number of decimal places * @return {Number} */ -const round$1 = (v, decimalLength) => { - if (decimalLength < 0) return v; - if (!decimalLength) return _round(v); - let p = powCache[decimalLength]; - if (!p) p = powCache[decimalLength] = 10 ** decimalLength; - return _round(v * p) / p; -}; + const round$1 = (v, decimalLength) => { + if (decimalLength < 0) return v; + if (!decimalLength) return _round(v); + const p = 10 ** decimalLength; + return _round(v * p) / p; + }; /** * Snaps a value to nearest increment or array value @@ -1058,28 +1094,143 @@ const addChild = (parent, child, sortMethod, prevProp = '_prev', nextProp = '_ne */ const parseInlineTransforms = (target, propName, animationInlineStyles) => { const inlineTransforms = target.style.transform; - let inlinedStylesPropertyValue; if (inlineTransforms) { const cachedTransforms = target[transformsSymbol]; - let t; while (t = transformsExecRgx.exec(inlineTransforms)) { - const inlinePropertyName = t[1]; - // const inlinePropertyValue = t[2]; - const inlinePropertyValue = t[2].slice(1, -1); - cachedTransforms[inlinePropertyName] = inlinePropertyValue; - if (inlinePropertyName === propName) { - inlinedStylesPropertyValue = inlinePropertyValue; - // Store the new parsed inline styles if animationInlineStyles is provided - if (animationInlineStyles) { - animationInlineStyles[propName] = inlinePropertyValue; + let pos = 0; + const len = inlineTransforms.length; + let fullTranslateValue; + while (pos < len) { + // Skip whitespace + while (pos < len && inlineTransforms.charCodeAt(pos) === 32) pos++; + if (pos >= len) break; + // Read function name + const nameStart = pos; + while (pos < len && inlineTransforms.charCodeAt(pos) !== 40) pos++; + if (pos >= len) break; + const name = inlineTransforms.substring(nameStart, pos); + // Scan to closing paren, recording top-level comma positions + let depth = 1; + const valueStart = pos + 1; + let c1 = -1, c2 = -1; + pos++; + while (pos < len && depth > 0) { + const c = inlineTransforms.charCodeAt(pos); + if (c === 40) depth++; + else if (c === 41) depth--; + else if (c === 44 && depth === 1) { + if (c1 === -1) c1 = pos; + else if (c2 === -1) c2 = pos; + } + pos++; + } + const valueEnd = pos - 1; + // Decompose multi-arg functions into individual axis properties + if (name === 'translate' || name === 'translate3d') { + if (c1 === -1) { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.translateZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } + } + fullTranslateValue = inlineTransforms.substring(valueStart, valueEnd); + } else if (name === 'scale' || name === 'scale3d') { + if (c1 === -1) { + cachedTransforms.scale = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.scaleX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.scaleZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } } + } else { + cachedTransforms[name] = inlineTransforms.substring(valueStart, valueEnd); } } + // Resolve the requested property from the cache + if (propName === 'translate3d' && fullTranslateValue) { + if (animationInlineStyles) animationInlineStyles[propName] = fullTranslateValue; + return fullTranslateValue; + } + const cached = cachedTransforms[propName]; + if (!isUnd(cached)) { + if (animationInlineStyles) animationInlineStyles[propName] = cached; + return cached; + } } - return inlineTransforms && !isUnd(inlinedStylesPropertyValue) ? inlinedStylesPropertyValue : + return propName === 'translate3d' ? '0px, 0px, 0px' : + propName === 'rotate3d' ? '0, 0, 0, 0deg' : stringStartsWith(propName, 'scale') ? '1' : stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew') ? '0deg' : '0px'; }; +/** + * Builds a CSS transform string from the target's cached transform properties. + * Iterates validTransforms in order (perspective > translate > rotate > scale > skew > matrix). + * When adjacent axis properties are all present, emits a shorter shorthand (translateX + translateY -> translate(x, y)) + * The index is advanced past consumed properties so they are not emitted twice. + * Properties without a grouping partner (e.g. translateY alone, scaleZ alone) emit individually. + * + * @param {Record} props + * @return {String} + */ +const buildTransformString = (props) => { + let str = emptyString; + for (let i = 0, l = validTransforms.length; i < l; i++) { + const key = validTransforms[i]; + const val = props[key]; + if (val !== undefined) { + // Group translateX with adjacent translateY / translateZ + if (key === 'translateX') { + const next = props.translateY; + if (next !== undefined) { + const next2 = props.translateZ; + if (next2 !== undefined) { + str += `translate3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `translate(${val},${next}) `; + i += 1; + } + continue; + } + } + // Group scaleX with adjacent scaleY / scaleZ (only when standalone scale is absent) + if (key === 'scaleX' && props.scale === undefined) { + const next = props.scaleY; + if (next !== undefined) { + const next2 = props.scaleZ; + if (next2 !== undefined) { + str += `scale3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `scale(${val},${next}) `; + i += 1; + } + continue; + } + } + // All other properties: emit individually using pre-built fragment string + str += `${transformsFragmentStrings[key]}${val}) `; + } + // Preserve non-animatable rotate3d in correct position (after rotateZ, before scale) + if (key === 'rotateZ') { + if (props.rotate3d !== undefined) str += `rotate3d(${props.rotate3d}) `; + } + } + // Preserve non-animatable matrix/matrix3d from inline styles + if (props.matrix !== undefined) str += `matrix(${props.matrix}) `; + if (props.matrix3d !== undefined) str += `matrix3d(${props.matrix3d}) `; + return str; +}; + /** @@ -1181,15 +1332,16 @@ const setValue = (targetValue, defaultValue) => { * @param {TweenPropValue} value * @param {Target} target * @param {Number} index - * @param {Number} total - * @param {Object} [store] + * @param {TargetsArray} targets + * @param {Object|null} store + * @param {Tween|null} prevTween * @return {any} */ -const getFunctionValue = (value, target, index, total, store) => { +const getFunctionValue = (value, target, index, targets, store, prevTween) => { let func; if (isFnc(value)) { func = () => { - const computed = /** @type {Function} */(value)(target, index, total); + const computed = /** @type {Function} */(value)(target, index, targets, prevTween); // Fallback to 0 if the function returns undefined / NaN / null / false / 0 return !isNaN(+computed) ? +computed : computed || 0; }; @@ -1256,9 +1408,17 @@ const getCSSValue = (target, propName, animationInlineStyles) => { */ const getOriginalAnimatableValue = (target, propName, tweenType, animationInlineStyles) => { const type = !isUnd(tweenType) ? tweenType : getTweenType(target, propName); - return type === tweenTypes.OBJECT ? target[propName] || 0 : - type === tweenTypes.ATTRIBUTE ? /** @type {DOMTarget} */(target).getAttribute(propName) : - type === tweenTypes.TRANSFORM ? parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : + if (type === tweenTypes.OBJECT) { + const value = target[propName]; + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value || 0; + } + if (type === tweenTypes.ATTRIBUTE) { + const value = /** @type {DOMTarget} */(target).getAttribute(propName); + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value; + } + return type === tweenTypes.TRANSFORM ? parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : type === tweenTypes.CSS_VAR ? getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles).trimStart() : getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles); }; @@ -1360,6 +1520,54 @@ const decomposeTweenValue = (tween, targetObject) => { const decomposedOriginalValue = createDecomposedValueTargetObject(); +/** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ +const composeColorValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const r = round$1(clamp$1(/** @type {Number} */(mod(lerp$1(fn[0], tn[0], progress))), 0, 255), 0); + const g = round$1(clamp$1(/** @type {Number} */(mod(lerp$1(fn[1], tn[1], progress))), 0, 255), 0); + const b = round$1(clamp$1(/** @type {Number} */(mod(lerp$1(fn[2], tn[2], progress))), 0, 255), 0); + const a = clamp$1(/** @type {Number} */(mod(round$1(lerp$1(fn[3], tn[3], progress), precision))), 0, 1); + if (tween._composition !== compositionTypes.none) { + const ns = tween._numbers; + ns[0] = r; + ns[1] = g; + ns[2] = b; + ns[3] = a; + } + return `rgba(${r},${g},${b},${a})`; +}; + +/** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ +const composeComplexValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const ts = tween._strings; + const hasComposition = tween._composition !== compositionTypes.none; + let v = ts[0]; + for (let j = 0, l = tn.length; j < l; j++) { + const n = /** @type {Number} */(mod(round$1(lerp$1(fn[j], tn[j], progress), precision))); + const s = ts[j + 1]; + v += `${s ? n + s : n}`; + if (hasComposition) { + tween._numbers[j] = n; + } + } + return v; +}; + @@ -1388,7 +1596,6 @@ const render = (tickable, time, muteCallbacks, internalRender, tickMode) => { const _hasChildren = tickable._hasChildren; const tickableDelay = tickable._delay; const tickablePrevAbsoluteTime = tickable._currentTime; // TODO: rename ._currentTime to ._absoluteCurrentTime - const tickableEndTime = tickableDelay + iterationDuration; const tickableAbsoluteTime = time - tickableDelay; const tickablePrevTime = clamp$1(tickablePrevAbsoluteTime, -tickableDelay, duration); @@ -1520,30 +1727,9 @@ const render = (tickable, time, muteCallbacks, internalRender, tickMode) => { number = /** @type {Number} */(tweenModifier(round$1(lerp$1(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision))); value = `${number}${tween._unit}`; } else if (tweenValueType === valueTypes.COLOR) { - const fn = tween._fromNumbers; - const tn = tween._toNumbers; - const r = round$1(clamp$1(/** @type {Number} */(tweenModifier(lerp$1(fn[0], tn[0], tweenProgress))), 0, 255), 0); - const g = round$1(clamp$1(/** @type {Number} */(tweenModifier(lerp$1(fn[1], tn[1], tweenProgress))), 0, 255), 0); - const b = round$1(clamp$1(/** @type {Number} */(tweenModifier(lerp$1(fn[2], tn[2], tweenProgress))), 0, 255), 0); - const a = clamp$1(/** @type {Number} */(tweenModifier(round$1(lerp$1(fn[3], tn[3], tweenProgress), tweenPrecision))), 0, 1); - value = `rgba(${r},${g},${b},${a})`; - if (tweenHasComposition) { - const ns = tween._numbers; - ns[0] = r; - ns[1] = g; - ns[2] = b; - ns[3] = a; - } + value = composeColorValue(tween, tweenProgress, tweenPrecision); } else if (tweenValueType === valueTypes.COMPLEX) { - value = tween._strings[0]; - for (let j = 0, l = tween._toNumbers.length; j < l; j++) { - const n = /** @type {Number} */(tweenModifier(round$1(lerp$1(tween._fromNumbers[j], tween._toNumbers[j], tweenProgress), tweenPrecision))); - const s = tween._strings[j + 1]; - value += `${s ? n + s : n}`; - if (tweenHasComposition) { - tween._numbers[j] = n; - } - } + value = composeComplexValue(tween, tweenProgress, tweenPrecision); } // For additive tweens and Animatables @@ -1586,14 +1772,8 @@ const render = (tickable, time, muteCallbacks, internalRender, tickMode) => { } - // NOTE: Possible improvement: Use translate(x,y) / translate3d(x,y,z) syntax - // to reduce memory usage on string composition if (tweenTransformsNeedUpdate && tween._renderTransforms) { - let str = emptyString; - for (let key in tweenTargetTransformsProperties) { - str += `${transformsFragmentStrings[key]}${tweenTargetTransformsProperties[key]}) `; - } - tweenStyle.transform = str; + tweenStyle.transform = buildTransformString(tweenTargetTransformsProperties); tweenTransformsNeedUpdate = 0; } @@ -1704,7 +1884,6 @@ const tick = (tickable, time, muteCallbacks, internalRender, tickMode) => { // Renders on timeline are triggered by its children so it needs to be set after rendering the children if (!muteCallbacks && tlChildrenHasRendered) tl.onRender(/** @type {CallbackArgument} */(tl)); - // Triggers the timeline onComplete() once all chindren all completed and the current time has reached the end if ((tlChildrenHaveCompleted || tlIsRunningBackwards) && tl._currentTime >= tl.duration) { // Make sure the paused flag is false in case it has been skipped in the render function @@ -1758,59 +1937,78 @@ const sanitizePropertyName = (propertyName, target, tweenType) => { /** * @template {Renderable} T * @param {T} renderable + * @param {Boolean} [inlineStylesOnly] * @return {T} */ -const cleanInlineStyles = renderable => { - // Allow cleanInlineStyles() to be called on timelines +const revertValues = (renderable, inlineStylesOnly = false) => { + // Allow revertValues() to be called on timelines if (renderable._hasChildren) { - forEachChildren(renderable, cleanInlineStyles, true); + forEachChildren(renderable, (/** @type {Renderable} */child) => revertValues(child, inlineStylesOnly), true); } else { const animation = /** @type {JSAnimation} */(renderable); animation.pause(); forEachChildren(animation, (/** @type {Tween} */tween) => { const tweenProperty = tween.property; const tweenTarget = tween.target; - if (tweenTarget[isDomSymbol]) { - const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; - const originalInlinedValue = tween._inlineValue; - const tweenHadNoInlineValue = isNil(originalInlinedValue) || originalInlinedValue === emptyString; - if (tween._tweenType === tweenTypes.TRANSFORM) { - const cachedTransforms = tweenTarget[transformsSymbol]; - if (tweenHadNoInlineValue) { - delete cachedTransforms[tweenProperty]; - } else { - cachedTransforms[tweenProperty] = originalInlinedValue; - } - if (tween._renderTransforms) { - if (!Object.keys(cachedTransforms).length) { - targetStyle.removeProperty('transform'); + const tweenType = tween._tweenType; + const originalInlinedValue = tween._inlineValue; + const tweenHadNoInlineValue = isNil(originalInlinedValue) || originalInlinedValue === emptyString; + if (tweenType === tweenTypes.OBJECT) { + if (!inlineStylesOnly && !tweenHadNoInlineValue) { + tweenTarget[tweenProperty] = originalInlinedValue; + } + } else if (tweenTarget[isDomSymbol]) { + if (tweenType === tweenTypes.ATTRIBUTE) { + if (!inlineStylesOnly) { + if (tweenHadNoInlineValue) { + /** @type {DOMTarget} */(tweenTarget).removeAttribute(tweenProperty); } else { - let str = emptyString; - for (let key in cachedTransforms) { - str += transformsFragmentStrings[key] + cachedTransforms[key] + ') '; - } - targetStyle.transform = str; + /** @type {DOMTarget} */(tweenTarget).setAttribute(tweenProperty, /** @type {String} */(originalInlinedValue)); } } } else { - if (tweenHadNoInlineValue) { - targetStyle.removeProperty(toLowerCase(tweenProperty)); + const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; + if (tweenType === tweenTypes.TRANSFORM) { + const cachedTransforms = tweenTarget[transformsSymbol]; + if (tweenHadNoInlineValue) { + delete cachedTransforms[tweenProperty]; + } else { + cachedTransforms[tweenProperty] = originalInlinedValue; + } + if (tween._renderTransforms) { + if (!Object.keys(cachedTransforms).length) { + targetStyle.removeProperty('transform'); + } else { + targetStyle.transform = buildTransformString(cachedTransforms); + } + } } else { - targetStyle[tweenProperty] = originalInlinedValue; + if (tweenHadNoInlineValue) { + targetStyle.removeProperty(toLowerCase(tweenProperty)); + } else { + targetStyle[tweenProperty] = originalInlinedValue; + } } } - if (animation._tail === tween) { - animation.targets.forEach(t => { - if (t.getAttribute && t.getAttribute('style') === emptyString) { - t.removeAttribute('style'); - } }); - } + } + if (tweenTarget[isDomSymbol] && animation._tail === tween) { + animation.targets.forEach(t => { + if (t.getAttribute && t.getAttribute('style') === emptyString) { + t.removeAttribute('style'); + } }); } }); } return renderable; }; +/** + * @template {Renderable} T + * @param {T} renderable + * @return {T} + */ +const cleanInlineStyles = renderable => revertValues(renderable, true); + /* @@ -1826,7 +2024,7 @@ class Clock { /** @type {Number} */ this._currentTime = initTime; /** @type {Number} */ - this._elapsedTime = initTime; + this._lastTickTime = initTime; /** @type {Number} */ this._startTime = initTime; /** @type {Number} */ @@ -1834,7 +2032,7 @@ class Clock { /** @type {Number} */ this._scheduledTime = 0; /** @type {Number} */ - this._frameDuration = round$1(K / maxFps, 0); + this._frameDuration = K / maxFps; /** @type {Number} */ this._fps = maxFps; /** @type {Number} */ @@ -1855,7 +2053,8 @@ class Clock { const previousFrameDuration = this._frameDuration; const fr = +frameRate; const fps = fr < minValue ? minValue : fr; - const frameDuration = round$1(K / fps, 0); + const frameDuration = K / fps; + if (fps > defaults.frameRate) defaults.frameRate = fps; this._fps = fps; this._frameDuration = frameDuration; this._scheduledTime += frameDuration - previousFrameDuration; @@ -1876,14 +2075,13 @@ class Clock { */ requestTick(time) { const scheduledTime = this._scheduledTime; - const elapsedTime = this._elapsedTime; - this._elapsedTime += (time - elapsedTime); - // If the elapsed time is lower than the scheduled time + this._lastTickTime = time; + // If the current time is lower than the scheduled time // this means not enough time has passed to hit one frameDuration // so skip that frame - if (elapsedTime < scheduledTime) return tickModes.NONE; + if (time < scheduledTime) return tickModes.NONE; const frameDuration = this._frameDuration; - const frameDelta = elapsedTime - scheduledTime; + const frameDelta = time - scheduledTime; // Ensures that _scheduledTime progresses in steps of at least 1 frameDuration. // Skips ahead if the actual elapsed time is higher. this._scheduledTime += frameDelta < frameDuration ? frameDuration : frameDelta; @@ -2020,7 +2218,7 @@ class Engine extends Clock { wake() { if (this.useDefaultMainLoop && !this.reqId) { - // Imediatly request a tick to update engine._elapsedTime and get accurate offsetPosition calculation in timer.js + // Imediatly request a tick to update engine._lastTickTime and get accurate offsetPosition calculation in timer.js this.requestTick(now()); this.reqId = engineTickMethod(tickEngine); } @@ -2495,6 +2693,9 @@ const reviveTimer = timer => { let timerId = 0; +/** @param {Timer} prev @param {Timer} child */ +const sortByPriority = (prev, child) => prev._priority > child._priority; + /** * Base class used to create Timers, Animations and Timelines */ @@ -2508,6 +2709,8 @@ class Timer extends Clock { super(0); + ++timerId; + const { id, delay, @@ -2519,6 +2722,7 @@ class Timer extends Clock { autoplay, frameRate, playbackRate, + priority, onComplete, onLoop, onPause, @@ -2529,31 +2733,32 @@ class Timer extends Clock { if (scope.current) scope.current.register(this); - const timerInitTime = parent ? 0 : engine._elapsedTime; + const timerInitTime = parent ? 0 : engine._lastTickTime; const timerDefaults = parent ? parent.defaults : globals.defaults; const timerDelay = /** @type {Number} */(isFnc(delay) || isUnd(delay) ? timerDefaults.delay : +delay); const timerDuration = isFnc(duration) || isUnd(duration) ? Infinity : +duration; const timerLoop = setValue(loop, timerDefaults.loop); const timerLoopDelay = setValue(loopDelay, timerDefaults.loopDelay); - const timerIterationCount = timerLoop === true || - timerLoop === Infinity || - /** @type {Number} */(timerLoop) < 0 ? Infinity : - /** @type {Number} */(timerLoop) + 1; + let timerIterationCount = timerLoop === true || + timerLoop === Infinity || + /** @type {Number} */(timerLoop) < 0 ? Infinity : + /** @type {Number} */(timerLoop) + 1; let offsetPosition = 0; if (parent) { offsetPosition = parentPosition; } else { - // Make sure to tick the engine once if not currently running to get up to date engine._elapsedTime + // Make sure to tick the engine once if not currently running to get up to date engine._lastTickTime // to avoid big gaps with the following offsetPosition calculation if (!engine.reqId) engine.requestTick(now()); // Make sure to scale the offset position with globals.timeScale to properly handle seconds unit - offsetPosition = (engine._elapsedTime - engine._startTime) * globals.timeScale; + offsetPosition = (engine._lastTickTime - engine._startTime) * globals.timeScale; } // Timer's parameters - this.id = !isUnd(id) ? id : ++timerId; + /** @type {String|Number} */ + this.id = !isUnd(id) ? id : timerId; /** @type {Timeline} */ this.parent = parent; // Total duration of the timer @@ -2613,7 +2818,7 @@ class Timer extends Clock { // Clock's parameters /** @type {Number} */ - this._elapsedTime = timerInitTime; + this._lastTickTime = timerInitTime; /** @type {Number} */ this._startTime = timerInitTime; /** @type {Number} */ @@ -2622,6 +2827,8 @@ class Timer extends Clock { this._fps = setValue(frameRate, timerDefaults.frameRate); /** @type {Number} */ this._speed = setValue(playbackRate, timerDefaults.playbackRate); + /** @type {Number} */ + this._priority = +setValue(priority, 1); } get cancelled() { @@ -2644,7 +2851,7 @@ class Timer extends Clock { } get iterationCurrentTime() { - return round$1(this._iterationTime, globals.precision); + return clamp$1(round$1(this._iterationTime, globals.precision), 0, this.iterationDuration); } set iterationCurrentTime(time) { @@ -2742,9 +2949,9 @@ class Timer extends Clock { /** @return {this} */ resetTime() { const timeScale = 1 / (this._speed * engine._speed); - // TODO: See if we can safely use engine._elapsedTime here + // TODO: See if we can safely use engine._lastTickTime here // if (!engine.reqId) engine.requestTick(now()) - // this._startTime = engine._elapsedTime - (this._currentTime + this._delay) * timeScale; + // this._startTime = engine._lastTickTime - (this._currentTime + this._delay) * timeScale; this._startTime = now() - (this._currentTime + this._delay) * timeScale; return this; } @@ -2766,7 +2973,7 @@ class Timer extends Clock { tick(this, minValue, 0, 0, tickModes.FORCE); } else { if (!this._running) { - addChild(engine, this); + addChild(engine, this, sortByPriority); engine._hasChildren = true; this._running = true; } @@ -2876,10 +3083,11 @@ class Timer extends Clock { /** * Imediatly completes the timer, cancels it and triggers the onComplete callback + * @param {Boolean|Number} [muteCallbacks] * @return {this} */ - complete() { - return this.seek(this.duration).cancel(); + complete(muteCallbacks = 0) { + return this.seek(this.duration, muteCallbacks).cancel(); } /** @@ -3252,12 +3460,14 @@ const fromTargetObject = createDecomposedValueTargetObject(); const toTargetObject = createDecomposedValueTargetObject(); const inlineStylesStore = {}; const toFunctionStore = { func: null }; +const fromFunctionStore = { func: null }; const keyframesTargetArray = [null]; const fastSetValuesArray = [null, null]; /** @type {TweenKeyValue} */ const keyObjectTarget = { to: null }; let tweenId = 0; +let JSAnimationId = 0; let keyframes; /** @type {TweenParamsOptions & TweenValues} */ let key; @@ -3358,7 +3568,7 @@ class JSAnimation extends Timer { * @param {Number} [parentPosition] * @param {Boolean} [fastSet=false] * @param {Number} [index=0] - * @param {Number} [length=0] + * @param {TargetsArray} [allTargets] */ constructor( targets, @@ -3367,11 +3577,13 @@ class JSAnimation extends Timer { parentPosition, fastSet = false, index = 0, - length = 0 + allTargets ) { super(/** @type {TimerParams & AnimationParams} */(parameters), parent, parentPosition); + ++JSAnimationId; + const parsedTargets = registerTargets(targets); const targetsLength = parsedTargets.length; @@ -3381,6 +3593,7 @@ class JSAnimation extends Timer { const params = /** @type {AnimationParams} */(kfParams ? mergeObjects(generateKeyframes(/** @type {DurationKeyframes} */(kfParams), parameters), parameters) : parameters); const { + id, delay, duration, ease, @@ -3391,11 +3604,12 @@ class JSAnimation extends Timer { } = params; const animDefaults = parent ? parent.defaults : globals.defaults; - const animaPlaybackEase = setValue(playbackEase, animDefaults.playbackEase); - const animEase = animaPlaybackEase ? parseEase(animaPlaybackEase) : null; - const hasSpring = !isUnd(ease) && !isUnd(/** @type {Spring} */(ease).ease); - const tEasing = hasSpring ? /** @type {Spring} */(ease).ease : setValue(ease, animEase ? 'linear' : animDefaults.ease); - const tDuration = hasSpring ? /** @type {Spring} */(ease).settlingDuration : setValue(duration, animDefaults.duration); + const animEase = setValue(ease, animDefaults.ease); + const animPlaybackEase = setValue(playbackEase, animDefaults.playbackEase); + const parsedAnimPlaybackEase = animPlaybackEase ? parseEase(animPlaybackEase) : null; + const hasSpring = !isUnd(/** @type {Spring} */(animEase).ease); + const tEasing = hasSpring ? /** @type {Spring} */(animEase).ease : setValue(ease, parsedAnimPlaybackEase ? 'linear' : animDefaults.ease); + const tDuration = hasSpring ? /** @type {Spring} */(animEase).settlingDuration : setValue(duration, animDefaults.duration); const tDelay = setValue(delay, animDefaults.delay); const tModifier = modifier || animDefaults.modifier; // If no composition is defined and the targets length is high (>= 1000) set the composition to 'none' (0) for faster tween creation @@ -3403,7 +3617,7 @@ class JSAnimation extends Timer { // const absoluteOffsetTime = this._offset; const absoluteOffsetTime = this._offset + (parent ? parent._offset : 0); // This allows targeting the current animation in the spring onComplete callback - if (hasSpring) /** @type {Spring} */(ease).parent = this; + if (hasSpring) /** @type {Spring} */(animEase).parent = this; let iterationDuration = NaN; let iterationDelay = NaN; @@ -3414,7 +3628,7 @@ class JSAnimation extends Timer { const target = parsedTargets[targetIndex]; const ti = index || targetIndex; - const tl = length || targetsLength; + const tl = allTargets || parsedTargets; let lastTransformGroupIndex = NaN; let lastTransformGroupLength = NaN; @@ -3488,8 +3702,16 @@ class JSAnimation extends Timer { } toFunctionStore.func = null; + fromFunctionStore.func = null; - const computedToValue = getFunctionValue(key.to, target, ti, tl, toFunctionStore); + const computedComposition = getFunctionValue(setValue(key.composition, tComposition), target, ti, tl, null, null); + const tweenComposition = isNum(computedComposition) ? computedComposition : compositionTypes[computedComposition]; + if (!siblings && tweenComposition !== compositionTypes.none) siblings = getTweenSiblings(target, propName); + // Timelines pass the last sibling tween if it belongs to the same timeline + // Standalone animations only pass prevTween when the property has multiple keyframes + const tailTween = siblings ? siblings._tail : null; + const prevSiblingTween = parent && tailTween && tailTween.parent.parent === parent ? tailTween : prevTween; + const computedToValue = getFunctionValue(key.to, target, ti, tl, toFunctionStore, prevSiblingTween); let tweenToValue; // Allows function based values to return an object syntax value ({to: v}) @@ -3499,17 +3721,18 @@ class JSAnimation extends Timer { } else { tweenToValue = computedToValue; } - const tweenFromValue = getFunctionValue(key.from, target, ti, tl); - const keyEasing = key.ease; + const tweenFromValue = getFunctionValue(key.from, target, ti, tl, null, prevSiblingTween); + const easeToParse = key.ease || tEasing; + + const easeFunctionResult = getFunctionValue(easeToParse, target, ti, tl, null, prevSiblingTween); + const keyEasing = isFnc(easeFunctionResult) || isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; + const hasSpring = !isUnd(keyEasing) && !isUnd(/** @type {Spring} */(keyEasing).ease); - // Easing are treated differently and don't accept function based value to prevent having to pass a function wrapper that returns an other function all the time - const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing || tEasing; + const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; // Calculate default individual keyframe duration by dividing the tl of keyframes - const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(setValue(key.duration, (l > 1 ? getFunctionValue(tDuration, target, ti, tl) / l : tDuration)), target, ti, tl); + const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(setValue(key.duration, (l > 1 ? getFunctionValue(tDuration, target, ti, tl, null, prevSiblingTween) / l : tDuration)), target, ti, tl, null, prevSiblingTween); // Default delay value should only be applied to the first tween - const tweenDelay = getFunctionValue(setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl); - const computedComposition = getFunctionValue(setValue(key.composition, tComposition), target, ti, tl); - const tweenComposition = isNum(computedComposition) ? computedComposition : compositionTypes[computedComposition]; + const tweenDelay = getFunctionValue(setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl, null, prevSiblingTween); // Modifiers are treated differently and don't accept function based value to prevent having to pass a function wrapper const tweenModifier = key.modifier || tModifier; const hasFromvalue = !isUnd(tweenFromValue); @@ -3526,7 +3749,6 @@ class JSAnimation extends Timer { let prevSibling = prevTween; if (tweenComposition !== compositionTypes.none) { - if (!siblings) siblings = getTweenSiblings(target, propName); let nextSibling = siblings._head; // Iterate trough all the next siblings until we find a sibling with an equal or inferior start time while (nextSibling && !nextSibling._isOverridden && nextSibling._absoluteStartTime <= absoluteStartTime) { @@ -3545,8 +3767,10 @@ class JSAnimation extends Timer { // Decompose values if (isFromToValue) { - decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[0], target, ti, tl) : tweenFromValue, fromTargetObject); - decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore) : tweenToValue, toTargetObject); + decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[0], target, ti, tl, fromFunctionStore, prevSiblingTween) : tweenFromValue, fromTargetObject); + decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore, prevSiblingTween) : tweenToValue, toTargetObject); + // Needed to force an inline style registration + const originalValue = getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore); if (fromTargetObject.t === valueTypes.NUMBER) { if (prevSibling) { if (prevSibling._valueType === valueTypes.UNIT) { @@ -3555,7 +3779,7 @@ class JSAnimation extends Timer { } } else { decomposeRawValue( - getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore), + originalValue, decomposedOriginalValue ); if (decomposedOriginalValue.t === valueTypes.UNIT) { @@ -3660,7 +3884,8 @@ class JSAnimation extends Timer { property: propName, target: target, _value: null, - _func: toFunctionStore.func, + _toFunc: toFunctionStore.func, + _fromFunc: fromFunctionStore.func, _ease: parseEase(tweenEasing), _fromNumbers: cloneArray(fromTargetObject.d), _toNumbers: cloneArray(toTargetObject.d), @@ -3697,6 +3922,18 @@ class JSAnimation extends Timer { composeTween(tween, siblings); } + // Pre-compute the tween end value for function-based value chaining (ie morphTo / scrambleText in keyframe arrays and timelines) + const vt = tween._valueType; + if (vt === valueTypes.COMPLEX) { + tween._value = composeComplexValue(tween, 1, -1); + } else if (vt === valueTypes.COLOR) { + tween._value = composeColorValue(tween, 1, -1); + } else if (vt === valueTypes.UNIT) { + tween._value = `${tweenModifier(tween._toNumber)}${tween._unit}`; + } else { + tween._value = tweenModifier(tween._toNumber); + } + if (isNaN(firstTweenChangeStartTime)) { firstTweenChangeStartTime = tween._startTime; } @@ -3774,12 +4011,14 @@ class JSAnimation extends Timer { } /** @type {TargetsArray} */ this.targets = parsedTargets; + /** @type {String|Number} */ + this.id = !isUnd(id) ? id : JSAnimationId; /** @type {Number} */ this.duration = iterationDuration === minValue ? minValue : clampInfinity(((iterationDuration + this._loopDelay) * this.iterationCount) - this._loopDelay) || minValue; /** @type {Callback} */ this.onRender = onRender || animDefaults.onRender; /** @type {EasingFunction} */ - this._ease = animEase; + this._ease = parsedAnimPlaybackEase; /** @type {Number} */ this._delay = iterationDelay; // NOTE: I'm keeping delay values separated from offsets in timelines because delays can override previous tweens and it could be confusing to debug a timeline with overridden tweens and no associated visible delays. @@ -3816,18 +4055,29 @@ class JSAnimation extends Timer { */ refresh() { forEachChildren(this, (/** @type {Tween} */tween) => { - const tweenFunc = tween._func; - if (tweenFunc) { - const ogValue = getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType); - decomposeRawValue(ogValue, decomposedOriginalValue); - // TODO: Check for from / to Array based values here, - decomposeRawValue(tweenFunc(), toTargetObject); - tween._fromNumbers = cloneArray(decomposedOriginalValue.d); - tween._fromNumber = decomposedOriginalValue.n; - tween._toNumbers = cloneArray(toTargetObject.d); - tween._strings = cloneArray(toTargetObject.s); - // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 - tween._toNumber = toTargetObject.o ? getRelativeValue(decomposedOriginalValue.n, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + const toFunc = tween._toFunc; + const fromFunc = tween._fromFunc; + if (toFunc || fromFunc) { + if (fromFunc) { + decomposeRawValue(fromFunc(), fromTargetObject); + if (fromTargetObject.u !== tween._unit && tween.target[isDomSymbol]) { + convertValueUnit(/** @type {DOMTarget} */(tween.target), fromTargetObject, tween._unit, true); + } + tween._fromNumbers = cloneArray(fromTargetObject.d); + tween._fromNumber = fromTargetObject.n; + } else if (toFunc) { + // When only toFunc exists, get from value from target + decomposeRawValue(getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType), decomposedOriginalValue); + tween._fromNumbers = cloneArray(decomposedOriginalValue.d); + tween._fromNumber = decomposedOriginalValue.n; + } + if (toFunc) { + decomposeRawValue(toFunc(), toTargetObject); + tween._toNumbers = cloneArray(toTargetObject.d); + tween._strings = cloneArray(toTargetObject.s); + // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 + tween._toNumber = toTargetObject.o ? getRelativeValue(tween._fromNumber, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + } } }); // This forces setter animations to render once @@ -3841,7 +4091,7 @@ class JSAnimation extends Timer { */ revert() { super.revert(); - return cleanInlineStyles(this); + return revertValues(this); } /** @@ -3863,231 +4113,75 @@ class JSAnimation extends Timer { * @param {AnimationParams} parameters * @return {JSAnimation} */ -const animate = (targets, parameters) => new JSAnimation(targets, parameters, null, 0, false).init(); - +const animate = (targets, parameters) => { + if (globals.editor) { + return globals.editor.addAnimation(targets, parameters); + } else { + return new JSAnimation(targets, parameters, null, 0, false).init(); + } +}; -const WAAPIAnimationsLookups = { - _head: null, - _tail: null, -}; /** - * @param {DOMTarget} $el - * @param {String} [property] - * @param {WAAPIAnimation} [parent] - * @return {globalThis.Animation} + * Timeline's children offsets positions parser + * @param {Timeline} timeline + * @param {String} timePosition + * @return {Number} */ -const removeWAAPIAnimation = ($el, property, parent) => { - let nextLookup = WAAPIAnimationsLookups._head; - let anim; - while (nextLookup) { - const next = nextLookup._next; - const matchTarget = nextLookup.$el === $el; - const matchProperty = !property || nextLookup.property === property; - const matchParent = !parent || nextLookup.parent === parent; - if (matchTarget && matchProperty && matchParent) { - anim = nextLookup.animation; - try { anim.commitStyles(); } catch {} anim.cancel(); - removeChild(WAAPIAnimationsLookups, nextLookup); - const lookupParent = nextLookup.parent; - if (lookupParent) { - lookupParent._completed++; - if (lookupParent.animations.length === lookupParent._completed) { - lookupParent.completed = true; - lookupParent.paused = true; - if (!lookupParent.muteCallbacks) { - lookupParent.onComplete(lookupParent); - lookupParent._resolve(lookupParent); - } - } - } - } - nextLookup = next; +const getPrevChildOffset = (timeline, timePosition) => { + if (stringStartsWith(timePosition, '<')) { + const goToPrevAnimationOffset = timePosition[1] === '<'; + const prevAnimation = /** @type {Tickable} */(timeline._tail); + const prevOffset = prevAnimation ? prevAnimation._offset + prevAnimation._delay : 0; + return goToPrevAnimationOffset ? prevOffset : prevOffset + prevAnimation.duration; } - return anim; }; /** - * @param {WAAPIAnimation} parent - * @param {DOMTarget} $el - * @param {String} property - * @param {PropertyIndexedKeyframes} keyframes - * @param {KeyframeAnimationOptions} params - * @retun {globalThis.Animation} + * @param {Timeline} timeline + * @param {TimelinePosition} [timePosition] + * @return {Number} */ -const addWAAPIAnimation = (parent, $el, property, keyframes, params) => { - const animation = $el.animate(keyframes, params); - const animTotalDuration = params.delay + (+params.duration * params.iterations); - animation.playbackRate = parent._speed; - if (parent.paused) animation.pause(); - if (parent.duration < animTotalDuration) { - parent.duration = animTotalDuration; - parent.controlAnimation = animation; - } - parent.animations.push(animation); - removeWAAPIAnimation($el, property); - addChild(WAAPIAnimationsLookups, { parent, animation, $el, property, _next: null, _prev: null }); - const handleRemove = () => { removeWAAPIAnimation($el, property, parent); }; - animation.oncancel = handleRemove; - animation.onremove = handleRemove; - if (!parent.persist) { - animation.onfinish = handleRemove; +const parseTimelinePosition = (timeline, timePosition) => { + let tlDuration = timeline.iterationDuration; + if (tlDuration === minValue) tlDuration = 0; + if (isUnd(timePosition)) return tlDuration; + if (isNum(+timePosition)) return +timePosition; + const timePosStr = /** @type {String} */(timePosition); + const tlLabels = timeline ? timeline.labels : null; + const hasLabels = !isNil(tlLabels); + const prevOffset = getPrevChildOffset(timeline, timePosStr); + const hasSibling = !isUnd(prevOffset); + const matchedRelativeOperator = relativeValuesExecRgx.exec(timePosStr); + if (matchedRelativeOperator) { + const fullOperator = matchedRelativeOperator[0]; + const split = timePosStr.split(fullOperator); + const labelOffset = hasLabels && split[0] ? tlLabels[split[0]] : tlDuration; + const parsedOffset = hasSibling ? prevOffset : hasLabels ? labelOffset : tlDuration; + const parsedNumericalOffset = +split[1]; + return getRelativeValue(parsedOffset, parsedNumericalOffset, fullOperator[0]); + } else { + return hasSibling ? prevOffset : + hasLabels ? !isUnd(tlLabels[timePosStr]) ? tlLabels[timePosStr] : + tlDuration : tlDuration; } - return animation; }; + + /** - * @overload - * @param {DOMTargetSelector} targetSelector - * @param {String} propName - * @return {String} - * - * @overload - * @param {JSTargetsParam} targetSelector - * @param {String} propName - * @return {Number|String} - * - * @overload - * @param {DOMTargetsParam} targetSelector - * @param {String} propName - * @param {String} unit - * @return {String} - * - * @overload - * @param {TargetsParam} targetSelector - * @param {String} propName - * @param {Boolean} unit + * @param {Timeline} tl * @return {Number} - * - * @param {TargetsParam} targetSelector - * @param {String} propName - * @param {String|Boolean} [unit] */ -function get(targetSelector, propName, unit) { - const targets = registerTargets(targetSelector); - if (!targets.length) return; - const [ target ] = targets; - const tweenType = getTweenType(target, propName); - const normalizePropName = sanitizePropertyName(propName, target, tweenType); - let originalValue = getOriginalAnimatableValue(target, normalizePropName); - if (isUnd(unit)) { - return originalValue; - } else { - decomposeRawValue(originalValue, decomposedOriginalValue); - if (decomposedOriginalValue.t === valueTypes.NUMBER || decomposedOriginalValue.t === valueTypes.UNIT) { - if (unit === false) { - return decomposedOriginalValue.n; - } else { - const convertedValue = convertValueUnit(/** @type {DOMTarget} */(target), decomposedOriginalValue, /** @type {String} */(unit), false); - return `${round$1(convertedValue.n, globals.precision)}${convertedValue.u}`; - } - } - } -} - -/** - * @param {TargetsParam} targets - * @param {AnimationParams} parameters - * @return {JSAnimation} - */ -const set = (targets, parameters) => { - if (isUnd(parameters)) return; - parameters.duration = minValue; - // Do not overrides currently active tweens by default - parameters.composition = setValue(parameters.composition, compositionTypes.none); - // Skip init() and force rendering by playing the animation - return new JSAnimation(targets, parameters, null, 0, true).resume(); -}; - -/** - * @param {TargetsParam} targets - * @param {Renderable|WAAPIAnimation} [renderable] - * @param {String} [propertyName] - * @return {TargetsArray} - */ -const remove = (targets, renderable, propertyName) => { - const targetsArray = parseTargets(targets); - for (let i = 0, l = targetsArray.length; i < l; i++) { - removeWAAPIAnimation( - /** @type {DOMTarget} */(targetsArray[i]), - propertyName, - renderable && /** @type {WAAPIAnimation} */(renderable).controlAnimation && /** @type {WAAPIAnimation} */(renderable), - ); - } - removeTargetsFromRenderable( - targetsArray, - /** @type {Renderable} */(renderable), - propertyName - ); - return targetsArray; -}; - - - - - -/** - * Timeline's children offsets positions parser - * @param {Timeline} timeline - * @param {String} timePosition - * @return {Number} - */ -const getPrevChildOffset = (timeline, timePosition) => { - if (stringStartsWith(timePosition, '<')) { - const goToPrevAnimationOffset = timePosition[1] === '<'; - const prevAnimation = /** @type {Tickable} */(timeline._tail); - const prevOffset = prevAnimation ? prevAnimation._offset + prevAnimation._delay : 0; - return goToPrevAnimationOffset ? prevOffset : prevOffset + prevAnimation.duration; - } -}; - -/** - * @param {Timeline} timeline - * @param {TimelinePosition} [timePosition] - * @return {Number} - */ -const parseTimelinePosition = (timeline, timePosition) => { - let tlDuration = timeline.iterationDuration; - if (tlDuration === minValue) tlDuration = 0; - if (isUnd(timePosition)) return tlDuration; - if (isNum(+timePosition)) return +timePosition; - const timePosStr = /** @type {String} */(timePosition); - const tlLabels = timeline ? timeline.labels : null; - const hasLabels = !isNil(tlLabels); - const prevOffset = getPrevChildOffset(timeline, timePosStr); - const hasSibling = !isUnd(prevOffset); - const matchedRelativeOperator = relativeValuesExecRgx.exec(timePosStr); - if (matchedRelativeOperator) { - const fullOperator = matchedRelativeOperator[0]; - const split = timePosStr.split(fullOperator); - const labelOffset = hasLabels && split[0] ? tlLabels[split[0]] : tlDuration; - const parsedOffset = hasSibling ? prevOffset : hasLabels ? labelOffset : tlDuration; - const parsedNumericalOffset = +split[1]; - return getRelativeValue(parsedOffset, parsedNumericalOffset, fullOperator[0]); - } else { - return hasSibling ? prevOffset : - hasLabels ? !isUnd(tlLabels[timePosStr]) ? tlLabels[timePosStr] : - tlDuration : tlDuration; - } -}; - - - - - -/** - * @param {Timeline} tl - * @return {Number} - */ -function getTimelineTotalDuration(tl) { - return clampInfinity(((tl.iterationDuration + tl._loopDelay) * tl.iterationCount) - tl._loopDelay) || minValue; +function getTimelineTotalDuration(tl) { + return clampInfinity(((tl.iterationDuration + tl._loopDelay) * tl.iterationCount) - tl._loopDelay) || minValue; } /** @@ -4103,7 +4197,7 @@ function getTimelineTotalDuration(tl) { * @param {Number} timePosition * @param {TargetsParam} targets * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] * @return {Timeline} * * @param {TimerParams|AnimationParams} childParams @@ -4111,17 +4205,17 @@ function getTimelineTotalDuration(tl) { * @param {Number} timePosition * @param {TargetsParam} [targets] * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] */ -function addTlChild(childParams, tl, timePosition, targets, index, length) { +function addTlChild(childParams, tl, timePosition, targets, index, allTargets) { const isSetter = isNum(childParams.duration) && /** @type {Number} */(childParams.duration) <= minValue; // Offset the tl position with -minValue for 0 duration animations or .set() calls in order to align their end value with the defined position const adjustedPosition = isSetter ? timePosition - minValue : timePosition; - tick(tl, adjustedPosition, 1, 1, tickModes.AUTO); + if (tl.composition) tick(tl, adjustedPosition, 1, 1, tickModes.AUTO); const tlChild = targets ? - new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, length) : + new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, allTargets) : new Timer(/** @type {TimerParams} */(childParams), tl, adjustedPosition); - tlChild.init(true); + if (tl.composition) tlChild.init(true); // TODO: Might be better to insert at a position relative to startTime? addChild(tl, tlChild); forEachChildren(tl, (/** @type {Renderable} */child) => { @@ -4133,6 +4227,8 @@ function addTlChild(childParams, tl, timePosition, targets, index, length) { return tl; } +let TLId = 0; + class Timeline extends Timer { /** @@ -4140,6 +4236,9 @@ class Timeline extends Timer { */ constructor(parameters = {}) { super(/** @type {TimerParams&TimelineParams} */(parameters), null, 0); + ++TLId; + /** @type {String|Number} */ + this.id = !isUnd(parameters.id) ? parameters.id : TLId; /** @type {Number} */ this.duration = 0; // TL duration starts at 0 and grows when adding children /** @type {Record} */ @@ -4148,6 +4247,8 @@ class Timeline extends Timer { const globalDefaults = globals.defaults; /** @type {DefaultsParams} */ this.defaults = defaultsParams ? mergeObjects(defaultsParams, globalDefaults) : globalDefaults; + /** @type {Boolean} */ + this.composition = setValue(parameters.composition, true); /** @type {Callback} */ this.onRender = parameters.onRender || globalDefaults.onRender; const tlPlaybackEase = setValue(parameters.playbackEase, globalDefaults.playbackEase); @@ -4160,7 +4261,7 @@ class Timeline extends Timer { * @overload * @param {TargetsParam} a1 * @param {AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] * @return {this} * * @overload @@ -4170,7 +4271,7 @@ class Timeline extends Timer { * * @param {TargetsParam|TimerParams} a1 * @param {TimelinePosition|AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] */ add(a1, a2, a3) { const isAnim = isObj(a2); @@ -4179,9 +4280,11 @@ class Timeline extends Timer { this._hasChildren = true; if (isAnim) { const childParams = /** @type {AnimationParams} */(a2); - // Check for function for children stagger positions - if (isFnc(a3)) { - const staggeredPosition = a3; + const editorHook = globals.editor && globals.editor.addTimelineChild; + const isStaggerType = a3 && /** @type {TweakRegister} */(a3).type === 'Stagger' && globals.editor; + // Check for function or Stagger type children positions + const staggeredPosition = isFnc(a3) ? a3 : null; + if (staggeredPosition || isStaggerType) { const parsedTargetsArray = parseTargets(/** @type {TargetsParam} */(a1)); // Store initial duration before adding new children that will change the duration const tlDuration = this.duration; @@ -4192,28 +4295,36 @@ class Timeline extends Timer { let i = 0; /** @type {Number} */ const parsedLength = (parsedTargetsArray.length); + // Call editor hook once for the entire stagger group instead of per target + const resolvedParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3, parsedLength) : null; + // Resolve stagger AFTER editor hook so tweaked position value (a3.defaultValue) is used + const staggerFn = staggeredPosition || globals.editor.resolveStagger(/** @type {TweakRegister} */(a3).defaultValue); parsedTargetsArray.forEach((/** @type {Target} */target) => { // Create a new parameter object for each staggered children - const staggeredChildParams = { ...childParams }; + const staggeredChildParams = { ...(resolvedParams || childParams) }; // Reset the duration of the timeline iteration before each stagger to prevent wrong start value calculation this.duration = tlDuration; this.iterationDuration = tlIterationDuration; if (!isUnd(id)) staggeredChildParams.id = id + '-' + i; + const staggeredTimePosition = parseTimelinePosition(this, staggerFn(target, i, parsedTargetsArray, null, this)); addTlChild( staggeredChildParams, this, - parseTimelinePosition(this, staggeredPosition(target, i, parsedLength, this)), + staggeredTimePosition, target, i, - parsedLength + parsedTargetsArray, ); i++; }); } else { + // Call editor hook before resolving position so tweaked values are applied + const resolvedChildParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3) : childParams; + const resolvedPosition = a3 && /** @type {*} */(a3).type ? /** @type {*} */(a3).defaultValue : a3; addTlChild( - childParams, + resolvedChildParams, this, - parseTimelinePosition(this, a3), + parseTimelinePosition(this, resolvedPosition), /** @type {TargetsParam} */(a1), ); } @@ -4225,7 +4336,8 @@ class Timeline extends Timer { parseTimelinePosition(this,a2), ); } - return this.init(true); + if (this.composition) this.init(true); + return this; } } @@ -4252,7 +4364,11 @@ class Timeline extends Timer { if (isUnd(synced) || synced && isUnd(synced.pause)) return this; synced.pause(); const duration = +(/** @type {globalThis.Animation} */(synced).effect ? /** @type {globalThis.Animation} */(synced).effect.getTiming().duration : /** @type {Tickable} */(synced).duration); - return this.add(synced, { currentTime: [0, duration], duration, ease: 'linear' }, position); + // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. + if (!isUnd(synced) && !isUnd(/** @type {WAAPIAnimation} */(synced).persist)) { + /** @type {WAAPIAnimation} */(synced).persist = true; + } + return this.add(synced, { currentTime: [0, duration], duration, delay: 0, ease: 'linear', playbackEase: 'linear' }, position); } /** @@ -4275,7 +4391,7 @@ class Timeline extends Timer { */ call(callback, position) { if (isUnd(callback) || callback && !isFnc(callback)) return this; - return this.add({ duration: 0, onComplete: () => callback(this) }, position); + return this.add({ duration: 0, delay: 0, onComplete: () => callback(this) }, position); } /** @@ -4318,8 +4434,8 @@ class Timeline extends Timer { * @return {this} */ refresh() { - forEachChildren(this, (/** @type {JSAnimation} */child) => { - if (child.refresh) child.refresh(); + forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => { + if (/** @type {JSAnimation} */(child).refresh) /** @type {JSAnimation} */(child).refresh(); }); return this; } @@ -4329,8 +4445,8 @@ class Timeline extends Timer { */ revert() { super.revert(); - forEachChildren(this, (/** @type {JSAnimation} */child) => child.revert, true); - return cleanInlineStyles(this); + forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => child.revert, true); + return revertValues(this); } /** @@ -4350,7 +4466,12 @@ class Timeline extends Timer { * @param {TimelineParams} [parameters] * @return {Timeline} */ -const createTimeline = parameters => new Timeline(parameters).init(); +const createTimeline = parameters => { + if (globals.editor) { + return /** @type {Timeline} */(/** @type {unknown} */(globals.editor.addTimeline(parameters))); + } + return new Timeline(parameters).init(); +}; @@ -4797,66 +4918,230 @@ const createSpring = (parameters) => { +const WAAPIAnimationsLookups = { + _head: null, + _tail: null, +}; + /** - * @param {Event} e + * @param {DOMTarget} $el + * @param {String} [property] + * @param {WAAPIAnimation} [parent] + * @return {globalThis.Animation} */ -const preventDefault = e => { - if (e.cancelable) e.preventDefault(); +const removeWAAPIAnimation = ($el, property, parent) => { + let nextLookup = WAAPIAnimationsLookups._head; + let anim; + while (nextLookup) { + const next = nextLookup._next; + const matchTarget = nextLookup.$el === $el; + const matchProperty = !property || nextLookup.property === property; + const matchParent = !parent || nextLookup.parent === parent; + if (matchTarget && matchProperty && matchParent) { + anim = nextLookup.animation; + try { anim.commitStyles(); } catch {} anim.cancel(); + removeChild(WAAPIAnimationsLookups, nextLookup); + const lookupParent = nextLookup.parent; + if (lookupParent) { + lookupParent._completed++; + if (lookupParent.animations.length === lookupParent._completed) { + lookupParent.completed = true; + lookupParent.paused = true; + if (!lookupParent.muteCallbacks) { + lookupParent.onComplete(lookupParent); + lookupParent._resolve(lookupParent); + } + } + } + } + nextLookup = next; + } + return anim; }; -class DOMProxy { - /** @param {Object} el */ - constructor(el) { - this.el = el; - this.zIndex = 0; - this.parentElement = null; - this.classList = { - add: noop, - remove: noop, - }; +/** + * @param {WAAPIAnimation} parent + * @param {DOMTarget} $el + * @param {String} property + * @param {PropertyIndexedKeyframes} keyframes + * @param {KeyframeAnimationOptions} params + * @retun {globalThis.Animation} + */ +const addWAAPIAnimation = (parent, $el, property, keyframes, params) => { + const animation = $el.animate(keyframes, params); + const animTotalDuration = params.delay + (+params.duration * params.iterations); + animation.playbackRate = parent._speed; + if (parent.paused) animation.pause(); + if (parent.duration < animTotalDuration) { + parent.duration = animTotalDuration; + parent.controlAnimation = animation; + } + parent.animations.push(animation); + removeWAAPIAnimation($el, property); + addChild(WAAPIAnimationsLookups, { parent, animation, $el, property, _next: null, _prev: null }); + const handleRemove = () => removeWAAPIAnimation($el, property, parent); + animation.oncancel = handleRemove; + animation.onremove = handleRemove; + if (!parent.persist) { + animation.onfinish = handleRemove; } + return animation; +}; - get x() { return this.el.x || 0 }; - set x(v) { this.el.x = v; }; - get y() { return this.el.y || 0 }; - set y(v) { this.el.y = v; }; - get width() { return this.el.width || 0 }; - set width(v) { this.el.width = v; }; - get height() { return this.el.height || 0 }; - set height(v) { this.el.height = v; }; - getBoundingClientRect() { - return { - top: this.y, - right: this.x, - bottom: this.y + this.height, - left: this.x + this.width, +/** + * @overload + * @param {DOMTargetSelector} targetSelector + * @param {String} propName + * @return {String} + * + * @overload + * @param {JSTargetsParam} targetSelector + * @param {String} propName + * @return {Number|String} + * + * @overload + * @param {DOMTargetsParam} targetSelector + * @param {String} propName + * @param {String} unit + * @return {String} + * + * @overload + * @param {TargetsParam} targetSelector + * @param {String} propName + * @param {Boolean} unit + * @return {Number} + * + * @param {TargetsParam} targetSelector + * @param {String} propName + * @param {String|Boolean} [unit] + */ +function get(targetSelector, propName, unit) { + const targets = registerTargets(targetSelector); + if (!targets.length) return; + const [ target ] = targets; + const tweenType = getTweenType(target, propName); + const normalizePropName = sanitizePropertyName(propName, target, tweenType); + let originalValue = getOriginalAnimatableValue(target, normalizePropName); + if (isUnd(unit)) { + return originalValue; + } else { + decomposeRawValue(originalValue, decomposedOriginalValue); + if (decomposedOriginalValue.t === valueTypes.NUMBER || decomposedOriginalValue.t === valueTypes.UNIT) { + if (unit === false) { + return decomposedOriginalValue.n; + } else { + const convertedValue = convertValueUnit(/** @type {DOMTarget} */(target), decomposedOriginalValue, /** @type {String} */(unit), false); + return `${round$1(convertedValue.n, globals.precision)}${convertedValue.u}`; + } } } } -class Transforms { - /** - * @param {DOMTarget|DOMProxy} $el - */ - constructor($el) { - this.$el = $el; - this.inlineTransforms = []; - this.point = new DOMPoint(); - this.inversedMatrix = this.getMatrix().inverse(); - } - - /** - * @param {Number} x - * @param {Number} y - * @return {DOMPoint} - */ - normalizePoint(x, y) { - this.point.x = x; - this.point.y = y; +/** + * @param {TargetsParam} targets + * @param {AnimationParams} parameters + * @return {JSAnimation} + */ +const set = (targets, parameters) => { + if (isUnd(parameters)) return; + parameters.duration = minValue; + // Do not overrides currently active tweens by default + parameters.composition = setValue(parameters.composition, compositionTypes.none); + // Skip init() and force rendering by playing the animation + return new JSAnimation(targets, parameters, null, 0, true).resume(); +}; + +/** + * @param {TargetsParam} targets + * @param {Renderable|WAAPIAnimation} [renderable] + * @param {String} [propertyName] + * @return {TargetsArray} + */ +const remove = (targets, renderable, propertyName) => { + const targetsArray = parseTargets(targets); + for (let i = 0, l = targetsArray.length; i < l; i++) { + removeWAAPIAnimation( + /** @type {DOMTarget} */(targetsArray[i]), + propertyName, + renderable && /** @type {WAAPIAnimation} */(renderable).controlAnimation && /** @type {WAAPIAnimation} */(renderable), + ); + } + removeTargetsFromRenderable( + targetsArray, + /** @type {Renderable} */(renderable), + propertyName + ); + return targetsArray; +}; + + + + + +/** + * @param {Event} e + */ +const preventDefault = e => { + if (e.cancelable) e.preventDefault(); +}; + +class DOMProxy { + /** @param {Object} el */ + constructor(el) { + this.el = el; + this.zIndex = 0; + this.parentElement = null; + this.classList = { + add: noop, + remove: noop, + }; + } + + get x() { return this.el.x || 0 }; + set x(v) { this.el.x = v; }; + + get y() { return this.el.y || 0 }; + set y(v) { this.el.y = v; }; + + get width() { return this.el.width || 0 }; + set width(v) { this.el.width = v; }; + + get height() { return this.el.height || 0 }; + set height(v) { this.el.height = v; }; + + getBoundingClientRect() { + return { + top: this.y, + right: this.x, + bottom: this.y + this.height, + left: this.x + this.width, + } + } +} + +class Transforms { + /** + * @param {DOMTarget|DOMProxy} $el + */ + constructor($el) { + this.$el = $el; + this.inlineTransforms = []; + this.point = new DOMPoint(); + this.inversedMatrix = this.getMatrix().inverse(); + } + + /** + * @param {Number} x + * @param {Number} y + * @return {DOMPoint} + */ + normalizePoint(x, y) { + this.point.x = x; + this.point.y = y; return this.point.matrixTransform(this.inversedMatrix); } @@ -5999,19 +6284,20 @@ const sync = (callback = noop) => { }; /** - * @param {(...args: any[]) => Tickable | ((...args: any[]) => void)} constructor + * @param {(...args: any[]) => Tickable | ((...args: any[]) => void) | void} constructor * @return {(...args: any[]) => Tickable | ((...args: any[]) => void)} */ const keepTime = constructor => { /** @type {Tickable} */ let tracked; return (...args) => { - let currentIteration, currentIterationProgress, reversed, alternate; + let currentIteration, currentIterationProgress, reversed, alternate, startTime; if (tracked) { currentIteration = tracked.currentIteration; currentIterationProgress = tracked.iterationProgress; reversed = tracked.reversed; alternate = tracked._alternate; + startTime = tracked._startTime; tracked.revert(); } const cleanup = constructor(...args); @@ -6019,6 +6305,7 @@ const keepTime = constructor => { if (!isUnd(currentIterationProgress)) { /** @type {Tickable} */(tracked).currentIteration = currentIteration; /** @type {Tickable} */(tracked).iterationProgress = (alternate ? !(currentIteration % 2) ? reversed : !reversed : reversed) ? 1 - currentIterationProgress : currentIterationProgress; + /** @type {Tickable} */(tracked)._startTime = startTime; } return cleanup || noop; } @@ -6426,6 +6713,7 @@ class ScrollContainer { this.updateBounds(); forEachChildren(this, (/** @type {ScrollObserver} */child) => { child.refresh(); + child.onResize(child); if (child._debug) { child.debug(); } @@ -6637,6 +6925,8 @@ class ScrollObserver { /** @type {Callback} */ this.onUpdate = parameters.onUpdate || noop; /** @type {Callback} */ + this.onResize = parameters.onResize || noop; + /** @type {Callback} */ this.onSyncComplete = parameters.onSyncComplete || noop; /** @type {Boolean} */ this.reverted = false; @@ -6700,7 +6990,9 @@ class ScrollObserver { linked.pause(); this.linked = linked; // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. - if (!isUnd(/** @type {WAAPIAnimation} */(linked))) /** @type {WAAPIAnimation} */(linked).persist = true; + if (!isUnd(linked) && !isUnd(/** @type {WAAPIAnimation} */(linked).persist)) { + /** @type {WAAPIAnimation} */(linked).persist = true; + } // Try to use a target of the linked object if no target parameters specified if (!this._params.target) { /** @type {HTMLElement} */ @@ -6902,12 +7194,11 @@ class ScrollObserver { // let offsetX = 0; // let offsetY = 0; // let $offsetParent = $el; - /** @type {Element} */ if (linked) { linkedTime = linked.currentTime; linked.seek(0, true); } - /* Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 + // Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 // const isContainerStatic = get(container.element, 'position') === 'static' ? set(container.element, { position: 'relative '}) : false; // while ($el && $el !== container.element && $el !== doc.body) { // const isSticky = get($el, 'position') === 'sticky' ? @@ -7281,1517 +7572,3441 @@ var index$3 = /*#__PURE__*/Object.freeze({ steps: steps }); -// Chain-able utilities -const numberUtils = numberImports; // Needed to keep the import when bundling -const chainables = {}; -/** - * @callback UtilityFunction - * @param {...*} args - * @return {Number|String} - * - * @param {UtilityFunction} fn - * @param {Number} [last=0] - * @return {function(...(Number|String)): function(Number|String): (Number|String)} - */ -const curry = (fn, last = 0) => (...args) => last ? v => fn(...args, v) : v => fn(v, ...args); + + /** - * @param {Function} fn - * @return {function(...(Number|String))} + * Converts an easing function into a valid CSS linear() timing function string + * @param {EasingFunction} fn + * @param {number} [samples=100] + * @returns {string} CSS linear() timing function */ -const chain = fn => { - return (...args) => { - const result = fn(...args); - return new Proxy(noop, { - apply: (_, __, [v]) => result(v), - get: (_, prop) => chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { - const nextResult = chainables[prop](...nextArgs); - return (/**@type {Number|String} */v) => nextResult(result(v)); - }) - }); - } +const easingToLinear = (fn, samples = 100) => { + const points = []; + for (let i = 0; i <= samples; i++) points.push(round$1(fn(i / samples), 4)); + return `linear(${points.join(', ')})`; }; +const WAAPIEasesLookups = {}; + /** - * @param {UtilityFunction} fn - * @param {String} name - * @param {Number} [right] - * @return {function(...(Number|String)): UtilityFunction} + * @param {EasingParam} ease + * @return {String} */ -const makeChainable = (name, fn, right = 0) => { - const chained = (...args) => (args.length < fn.length ? chain(curry(fn, right)) : fn)(...args); - if (!chainables[name]) chainables[name] = chained; - return chained; +const parseWAAPIEasing = (ease) => { + let parsedEase = WAAPIEasesLookups[ease]; + if (parsedEase) return parsedEase; + parsedEase = 'linear'; + if (isStr(ease)) { + if ( + stringStartsWith(ease, 'linear') || + stringStartsWith(ease, 'cubic-') || + stringStartsWith(ease, 'steps') || + stringStartsWith(ease, 'ease') + ) { + parsedEase = ease; + } else if (stringStartsWith(ease, 'cubicB')) { + parsedEase = toLowerCase(ease); + } else { + const parsed = parseEaseString(ease); + if (isFnc(parsed)) parsedEase = parsed === none ? 'linear' : easingToLinear(parsed); + } + // Only cache string based easing name, otherwise function arguments get lost + WAAPIEasesLookups[ease] = parsedEase; + } else if (isFnc(ease)) { + const easing = easingToLinear(ease); + if (easing) parsedEase = easing; + } else if (/** @type {Spring} */(ease).ease) { + parsedEase = easingToLinear(/** @type {Spring} */(ease).ease); + } + return parsedEase; }; -/** - * @typedef {Object} ChainablesMap - * @property {ChainedClamp} clamp - * @property {ChainedRound} round - * @property {ChainedSnap} snap - * @property {ChainedWrap} wrap - * @property {ChainedLerp} lerp - * @property {ChainedDamp} damp - * @property {ChainedMapRange} mapRange - * @property {ChainedRoundPad} roundPad - * @property {ChainedPadStart} padStart - * @property {ChainedPadEnd} padEnd - * @property {ChainedDegToRad} degToRad - * @property {ChainedRadToDeg} radToDeg - */ +const transformsShorthands = ['x', 'y', 'z']; +const commonDefaultPXProperties = [ + 'perspective', + 'width', + 'height', + 'margin', + 'padding', + 'top', + 'right', + 'bottom', + 'left', + 'borderWidth', + 'fontSize', + 'borderRadius', + ...transformsShorthands +]; -/** - * @callback ChainedUtilsResult - * @param {Number} value - The value to process through the chained operations - * @return {Number} The processed result - */ +const validIndividualTransforms = /*#__PURE__*/ (() => [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))])(); + +let transformsPropertiesRegistered = null; /** - * @typedef {ChainablesMap & ChainedUtilsResult} ChainableUtil + * @param {String} propName + * @param {WAAPIKeyframeValue} value + * @param {DOMTarget} $el + * @param {Number} i + * @param {DOMTargetsArray} parsedTargets + * @return {String} */ - -// Chainable +const normalizeTweenValue = (propName, value, $el, i, parsedTargets) => { + // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables + let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, parsedTargets, null, null); + if (!isNum(v)) return v; + if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`; + if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`; + return `${v}`; +}; /** - * @callback ChainedRoundPad - * @param {Number} decimalLength - Number of decimal places - * @return {ChainableUtil} + * @param {DOMTarget} $el + * @param {String} propName + * @param {WAAPIKeyframeValue} from + * @param {WAAPIKeyframeValue} to + * @param {Number} i + * @param {DOMTargetsArray} parsedTargets + * @return {WAAPITweenValue} */ -const roundPad = /** @type {typeof numberUtils.roundPad & ChainedRoundPad} */(makeChainable('roundPad', numberUtils.roundPad)); +const parseIndividualTweenValue = ($el, propName, from, to, i, parsedTargets) => { + /** @type {WAAPITweenValue} */ + let tweenValue = '0'; + const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, parsedTargets) : getComputedStyle($el)[propName]; + if (!isUnd(from)) { + const computedFrom = normalizeTweenValue(propName, from, $el, i, parsedTargets); + tweenValue = [computedFrom, computedTo]; + } else { + tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, parsedTargets)) : computedTo; + } + return tweenValue; +}; +class WAAPIAnimation { /** - * @callback ChainedPadStart - * @param {Number} totalLength - Target length - * @param {String} padString - String to pad with - * @return {ChainableUtil} + * @param {DOMTargetsParam} targets + * @param {WAAPIAnimationParams} params */ -const padStart = /** @type {typeof numberUtils.padStart & ChainedPadStart} */(makeChainable('padStart', numberUtils.padStart)); + constructor(targets, params) { -/** - * @callback ChainedPadEnd - * @param {Number} totalLength - Target length - * @param {String} padString - String to pad with - * @return {ChainableUtil} - */ -const padEnd = /** @type {typeof numberUtils.padEnd & ChainedPadEnd} */(makeChainable('padEnd', numberUtils.padEnd)); + if (scope.current) scope.current.register(this); -/** - * @callback ChainedWrap - * @param {Number} min - Minimum boundary - * @param {Number} max - Maximum boundary - * @return {ChainableUtil} - */ -const wrap = /** @type {typeof numberUtils.wrap & ChainedWrap} */(makeChainable('wrap', numberUtils.wrap)); + // Skip the registration and fallback to no animation in case CSS.registerProperty is not supported + if (isNil(transformsPropertiesRegistered)) { + if (isBrowser && (isUnd(CSS) || !Object.hasOwnProperty.call(CSS, 'registerProperty'))) { + transformsPropertiesRegistered = false; + } else { + validTransforms.forEach(t => { + const isSkew = stringStartsWith(t, 'skew'); + const isScale = stringStartsWith(t, 'scale'); + const isRotate = stringStartsWith(t, 'rotate'); + const isTranslate = stringStartsWith(t, 'translate'); + const isAngle = isRotate || isSkew; + const syntax = isAngle ? '' : isScale ? "" : isTranslate ? "" : "*"; + try { + CSS.registerProperty({ + name: '--' + t, + syntax, + inherits: false, + initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0', + }); + } catch {} }); + transformsPropertiesRegistered = true; + } + } -/** - * @callback ChainedMapRange - * @param {Number} inLow - Input range minimum - * @param {Number} inHigh - Input range maximum - * @param {Number} outLow - Output range minimum - * @param {Number} outHigh - Output range maximum - * @return {ChainableUtil} - */ -const mapRange = /** @type {typeof numberUtils.mapRange & ChainedMapRange} */(makeChainable('mapRange', numberUtils.mapRange)); + const parsedTargets = registerTargets(targets); -/** - * @callback ChainedDegToRad - * @return {ChainableUtil} - */ -const degToRad = /** @type {typeof numberUtils.degToRad & ChainedDegToRad} */(makeChainable('degToRad', numberUtils.degToRad)); + if (!parsedTargets.length) { + console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`); + } -/** - * @callback ChainedRadToDeg - * @return {ChainableUtil} - */ -const radToDeg = /** @type {typeof numberUtils.radToDeg & ChainedRadToDeg} */(makeChainable('radToDeg', numberUtils.radToDeg)); + const autoplay = setValue(params.autoplay, globals.defaults.autoplay); + const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false; + const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true; + const reversed = params.reversed && /** @type {Boolean} */(params.reversed) === true; + const loop = setValue(params.loop, globals.defaults.loop); + const iterations = /** @type {Number} */((loop === true || loop === Infinity) ? Infinity : isNum(loop) ? loop + 1 : 1); + /** @type {PlaybackDirection} */ + const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal'; + /** @type {FillMode} */ + const fill = 'both'; // We use 'both' here because the animation can be reversed during playback + const timeScale = (globals.timeScale === 1 ? 1 : K); -/** - * @callback ChainedSnap - * @param {Number|Array} increment - Step size or array of snap points - * @return {ChainableUtil} - */ -const snap = /** @type {typeof numberUtils.snap & ChainedSnap} */(makeChainable('snap', numberUtils.snap)); + /** @type {DOMTargetsArray}] */ + this.targets = parsedTargets; + /** @type {Array}] */ + this.animations = []; + /** @type {globalThis.Animation}] */ + this.controlAnimation = null; + /** @type {Callback} */ + this.onComplete = params.onComplete || /** @type {Callback} */(/** @type {unknown} */(globals.defaults.onComplete)); + /** @type {Number} */ + this.duration = 0; + /** @type {Boolean} */ + this.muteCallbacks = false; + /** @type {Boolean} */ + this.completed = false; + /** @type {Boolean} */ + this.paused = !autoplay || scroll !== false; + /** @type {Boolean} */ + this.reversed = reversed; + /** @type {Boolean} */ + this.persist = setValue(params.persist, globals.defaults.persist); + /** @type {Boolean|ScrollObserver} */ + this.autoplay = autoplay; + /** @type {Number} */ + this._speed = setValue(params.playbackRate, globals.defaults.playbackRate); + /** @type {Function} */ + this._resolve = noop; // Used by .then() + /** @type {Number} */ + this._completed = 0; + /** @type {Array.} */ + this._inlineStyles = []; -/** - * @callback ChainedClamp - * @param {Number} min - Minimum boundary - * @param {Number} max - Maximum boundary - * @return {ChainableUtil} - */ -const clamp = /** @type {typeof numberUtils.clamp & ChainedClamp} */(makeChainable('clamp', numberUtils.clamp)); + parsedTargets.forEach(($el, i) => { -/** - * @callback ChainedRound - * @param {Number} decimalLength - Number of decimal places - * @return {ChainableUtil} - */ -const round = /** @type {typeof numberUtils.round & ChainedRound} */(makeChainable('round', numberUtils.round)); + const cachedTransforms = $el[transformsSymbol]; + const hasIndividualTransforms = validIndividualTransforms.some(t => params.hasOwnProperty(t)); + const elStyle = $el.style; + const inlineStyles = this._inlineStyles[i] = {}; -/** - * @callback ChainedLerp - * @param {Number} start - Starting value - * @param {Number} end - Ending value - * @return {ChainableUtil} - */ -const lerp = /** @type {typeof numberUtils.lerp & ChainedLerp} */(makeChainable('lerp', numberUtils.lerp, 1)); + const easeToParse = setValue(params.ease, globals.defaults.ease); -/** - * @callback ChainedDamp - * @param {Number} start - Starting value - * @param {Number} end - Target value - * @param {Number} deltaTime - Delta time in ms - * @return {ChainableUtil} - */ -const damp = /** @type {typeof numberUtils.damp & ChainedDamp} */(makeChainable('damp', numberUtils.damp, 1)); + const easeFunctionResult = getFunctionValue(easeToParse, $el, i, parsedTargets, null, null); + const keyEasing = isFnc(easeFunctionResult) || isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; -/** - * Generate a random number between optional min and max (inclusive) and decimal precision - * - * @callback RandomNumberGenerator - * @param {Number} [min=0] - The minimum value (inclusive) - * @param {Number} [max=1] - The maximum value (inclusive) - * @param {Number} [decimalLength=0] - Number of decimal places to round to - * @return {Number} A random number between min and max - */ + const spring = /** @type {Spring} */(easeToParse).ease && easeToParse; + /** @type {String} */ + const easing = parseWAAPIEasing(keyEasing); -/** - * Generates a random number between min and max (inclusive) with optional decimal precision - * - * @type {RandomNumberGenerator} - */ -const random = (min = 0, max = 1, decimalLength = 0) => { - const m = 10 ** decimalLength; - return Math.floor((Math.random() * (max - min + (1 / m)) + min) * m) / m; -}; + /** @type {Number} */ + const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, parsedTargets, null, null)) * timeScale; + /** @type {Number} */ + const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, parsedTargets, null, null) * timeScale; + /** @type {CompositeOperation} */ + const composite = /** @type {CompositeOperation} */(setValue(params.composition, 'replace')); -let _seed = 0; + for (let name in params) { + if (!isKey(name)) continue; + /** @type {PropertyIndexedKeyframes} */ + const keyframes = {}; + /** @type {KeyframeAnimationOptions} */ + const tweenParams = { iterations, direction, fill, easing, duration, delay, composite }; + const propertyValue = params[name]; + const individualTransformProperty = hasIndividualTransforms ? validTransforms.includes(name) ? name : shortTransforms.get(name) : false; -/** - * Creates a seeded pseudorandom number generator function - * - * @param {Number} [seed] - The seed value for the random number generator - * @param {Number} [seededMin=0] - The minimum default value (inclusive) of the returned function - * @param {Number} [seededMax=1] - The maximum default value (inclusive) of the returned function - * @param {Number} [seededDecimalLength=0] - Default number of decimal places to round to of the returned function - * @return {RandomNumberGenerator} A function to generate a random number between optional min and max (inclusive) and decimal precision - */ -const createSeededRandom = (seed, seededMin = 0, seededMax = 1, seededDecimalLength = 0) => { - let t = seed === undefined ? _seed++ : seed; - return (min = seededMin, max = seededMax, decimalLength = seededDecimalLength) => { - t += 0x6D2B79F5; - t = Math.imul(t ^ t >>> 15, t | 1); - t ^= t + Math.imul(t ^ t >>> 7, t | 61); - const m = 10 ** decimalLength; - return Math.floor(((((t ^ t >>> 14) >>> 0) / 4294967296) * (max - min + (1 / m)) + min) * m) / m; + const styleName = individualTransformProperty ? 'transform' : name; + if (!inlineStyles[styleName]) { + inlineStyles[styleName] = elStyle[styleName]; + } + + let parsedPropertyValue; + if (isObj(propertyValue)) { + const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue); + const tweenOptionsEase = setValue(tweenOptions.ease, easing); + const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase; + const to = /** @type {WAAPITweenOptions} */(tweenOptions).to; + const from = /** @type {WAAPITweenOptions} */(tweenOptions).from; + /** @type {Number} */ + tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, parsedTargets, null, null)) * timeScale; + /** @type {Number} */ + tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, parsedTargets, null, null) * timeScale; + /** @type {CompositeOperation} */ + tweenParams.composite = /** @type {CompositeOperation} */(setValue(tweenOptions.composition, composite)); + /** @type {String} */ + tweenParams.easing = parseWAAPIEasing(tweenOptionsEase); + parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); + if (individualTransformProperty) { + keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; + cachedTransforms[individualTransformProperty] = parsedPropertyValue; + } else { + keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); + } + addWAAPIAnimation(this, $el, name, keyframes, tweenParams); + if (!isUnd(from)) { + if (!individualTransformProperty) { + elStyle[name] = keyframes[name][0]; + } else { + const key = `--${individualTransformProperty}`; + elStyle.setProperty(key, keyframes[key][0]); + } + } + } else { + parsedPropertyValue = isArr(propertyValue) ? + propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, parsedTargets)) : + normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, parsedTargets); + if (individualTransformProperty) { + keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; + cachedTransforms[individualTransformProperty] = parsedPropertyValue; + } else { + keyframes[name] = parsedPropertyValue; + } + addWAAPIAnimation(this, $el, name, keyframes, tweenParams); + } + } + if (hasIndividualTransforms) { + let transforms = emptyString; + for (let t in cachedTransforms) { + transforms += `${transformsFragmentStrings[t]}var(--${t})) `; + } + elStyle.transform = transforms; + } + }); + + if (scroll) { + /** @type {ScrollObserver} */(this.autoplay).link(this); + } } -}; - -/** - * Picks a random element from an array or a string - * - * @template T - * @param {String|Array} items - The array or string to pick from - * @return {String|T} A random element from the array or character from the string - */ -const randomPick = items => items[random(0, items.length - 1)]; - -/** - * Shuffles an array in-place using the Fisher-Yates algorithm - * Adapted from https://bost.ocks.org/mike/shuffle/ - * - * @param {Array} items - The array to shuffle (will be modified in-place) - * @return {Array} The same array reference, now shuffled - */ -const shuffle = items => { - let m = items.length, t, i; - while (m) { i = random(0, --m); t = items[m]; items[m] = items[i]; items[i] = t; } - return items; -}; + /** + * @callback forEachCallback + * @param {globalThis.Animation} animation + */ + /** + * @param {forEachCallback|String} callback + * @return {this} + */ + forEach(callback) { + try { + const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; + this.animations.forEach(cb); + } catch {} return this; + } + get speed() { + return this._speed; + } + set speed(speed) { + this._speed = +speed; + this.forEach(anim => anim.playbackRate = speed); + } -/** - * @overload - * @param {Number} val - * @param {StaggerParams} [params] - * @return {StaggerFunction} - */ -/** - * @overload - * @param {String} val - * @param {StaggerParams} [params] - * @return {StaggerFunction} - */ -/** - * @overload - * @param {[Number, Number]} val - * @param {StaggerParams} [params] - * @return {StaggerFunction} - */ -/** - * @overload - * @param {[String, String]} val - * @param {StaggerParams} [params] - * @return {StaggerFunction} - */ -/** - * @param {Number|String|[Number, Number]|[String, String]} val The staggered value or range - * @param {StaggerParams} [params] The stagger parameters - * @return {StaggerFunction} - */ -const stagger = (val, params = {}) => { - let values = []; - let maxValue = 0; - const from = params.from; - const reversed = params.reversed; - const ease = params.ease; - const hasEasing = !isUnd(ease); - const hasSpring = hasEasing && !isUnd(/** @type {Spring} */(ease).ease); - const staggerEase = hasSpring ? /** @type {Spring} */(ease).ease : hasEasing ? parseEase(ease) : null; - const grid = params.grid; - const axis = params.axis; - const customTotal = params.total; - const fromFirst = isUnd(from) || from === 0 || from === 'first'; - const fromCenter = from === 'center'; - const fromLast = from === 'last'; - const fromRandom = from === 'random'; - const isRange = isArr(val); - const useProp = params.use; - const val1 = isRange ? parseNumber(val[0]) : parseNumber(val); - const val2 = isRange ? parseNumber(val[1]) : 0; - const unitMatch = unitsExecRgx.exec((isRange ? val[1] : val) + emptyString); - const start = params.start || 0 + (isRange ? val1 : 0); - let fromIndex = fromFirst ? 0 : isNum(from) ? from : 0; - return (target, i, t, tl) => { - const [ registeredTarget ] = registerTargets(target); - const total = isUnd(customTotal) ? t : customTotal; - const customIndex = !isUnd(useProp) ? isFnc(useProp) ? useProp(registeredTarget, i, total) : getOriginalAnimatableValue(registeredTarget, useProp) : false; - const staggerIndex = isNum(customIndex) || isStr(customIndex) && isNum(+customIndex) ? +customIndex : i; - if (fromCenter) fromIndex = (total - 1) / 2; - if (fromLast) fromIndex = total - 1; - if (!values.length) { - for (let index = 0; index < total; index++) { - if (!grid) { - values.push(abs(fromIndex - index)); - } else { - const fromX = !fromCenter ? fromIndex % grid[0] : (grid[0] - 1) / 2; - const fromY = !fromCenter ? floor(fromIndex / grid[0]) : (grid[1] - 1) / 2; - const toX = index % grid[0]; - const toY = floor(index / grid[0]); - const distanceX = fromX - toX; - const distanceY = fromY - toY; - let value = sqrt(distanceX * distanceX + distanceY * distanceY); - if (axis === 'x') value = -distanceX; - if (axis === 'y') value = -distanceY; - values.push(value); - } - maxValue = max(...values); - } - if (staggerEase) values = values.map(val => staggerEase(val / maxValue) * maxValue); - if (reversed) values = values.map(val => axis ? (val < 0) ? val * -1 : -val : abs(maxValue - val)); - if (fromRandom) values = shuffle(values); - } - const spacing = isRange ? (val2 - val1) / maxValue : val1; - const offset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); - /** @type {String|Number} */ - let output = offset + ((spacing * round$1(values[staggerIndex], 2)) || 0); - if (params.modifier) output = params.modifier(output); - if (unitMatch) output = `${output}${unitMatch[2]}`; - return output; + get currentTime() { + const controlAnimation = this.controlAnimation; + const timeScale = globals.timeScale; + return this.completed ? this.duration : controlAnimation ? +controlAnimation.currentTime * (timeScale === 1 ? 1 : timeScale) : 0; } -}; -var index$2 = /*#__PURE__*/Object.freeze({ - __proto__: null, - $: registerTargets, - clamp: clamp, - cleanInlineStyles: cleanInlineStyles, - createSeededRandom: createSeededRandom, - damp: damp, - degToRad: degToRad, - get: get, - keepTime: keepTime, - lerp: lerp, - mapRange: mapRange, - padEnd: padEnd, - padStart: padStart, - radToDeg: radToDeg, - random: random, - randomPick: randomPick, - remove: remove, - round: round, - roundPad: roundPad, - set: set, - shuffle: shuffle, - snap: snap, - stagger: stagger, - sync: sync, - wrap: wrap -}); + set currentTime(time) { + const t = time * (globals.timeScale === 1 ? 1 : K); + this.forEach(anim => { + // Make sure the animation playState is not 'paused' in order to properly trigger an onfinish callback. + // The "paused" play state supersedes the "finished" play state; if the animation is both paused and finished, the "paused" state is the one that will be reported. + // https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish_event + // This is not needed for persisting animations since they never finish. + if (!this.persist && t >= this.duration) anim.play(); + anim.currentTime = t; + }); + } + get progress() { + return this.currentTime / this.duration; + } + set progress(progress) { + this.forEach(anim => anim.currentTime = progress * this.duration || 0); + } -/** - * @param {TargetsParam} path - * @return {SVGGeometryElement|void} - */ -const getPath = path => { - const parsedTargets = parseTargets(path); - const $parsedSvg = /** @type {SVGGeometryElement} */(parsedTargets[0]); - if (!$parsedSvg || !isSvg($parsedSvg)) return console.warn(`${path} is not a valid SVGGeometryElement`); - return $parsedSvg; -}; + resume() { + if (!this.paused) return this; + this.paused = false; + // TODO: Store the current time, and seek back to the last position + return this.forEach('play'); + } + pause() { + if (this.paused) return this; + this.paused = true; + return this.forEach('pause'); + } + alternate() { + this.reversed = !this.reversed; + this.forEach('reverse'); + if (this.paused) this.forEach('pause'); + return this; + } -// Motion path animation + play() { + if (this.reversed) this.alternate(); + return this.resume(); + } -/** - * @param {SVGGeometryElement} $path - * @param {Number} totalLength - * @param {Number} progress - * @param {Number} lookup - * @param {Boolean} shouldClamp - * @return {DOMPoint} - */ -const getPathPoint = ($path, totalLength, progress, lookup, shouldClamp) => { - const point = progress + lookup; - const pointOnPath = shouldClamp - ? Math.max(0, Math.min(point, totalLength)) // Clamp between 0 and totalLength - : (point % totalLength + totalLength) % totalLength; // Wrap around - return $path.getPointAtLength(pointOnPath); -}; + reverse() { + if (!this.reversed) this.alternate(); + return this.resume(); + } -/** - * @param {SVGGeometryElement} $path - * @param {String} pathProperty - * @param {Number} [offset=0] - * @return {FunctionValue} - */ -const getPathProgess = ($path, pathProperty, offset = 0) => { - return $el => { - const totalLength = +($path.getTotalLength()); - const inSvg = $el[isSvgSymbol]; - const ctm = $path.getCTM(); - const shouldClamp = offset === 0; - /** @type {TweenObjectValue} */ - return { - from: 0, - to: totalLength, - /** @type {TweenModifier} */ - modifier: progress => { - const offsetLength = offset * totalLength; - const newProgress = progress + offsetLength; - if (pathProperty === 'a') { - const p0 = getPathPoint($path, totalLength, newProgress, -1, shouldClamp); - const p1 = getPathPoint($path, totalLength, newProgress, 1, shouldClamp); - return atan2(p1.y - p0.y, p1.x - p0.x) * 180 / PI; - } else { - const p = getPathPoint($path, totalLength, newProgress, 0, shouldClamp); - return pathProperty === 'x' ? - inSvg || !ctm ? p.x : p.x * ctm.a + p.y * ctm.c + ctm.e : - inSvg || !ctm ? p.y : p.x * ctm.b + p.y * ctm.d + ctm.f - } - } - } + /** + * @param {Number} time + * @param {Boolean} muteCallbacks + */ + seek(time, muteCallbacks = false) { + if (muteCallbacks) this.muteCallbacks = true; + if (time < this.duration) this.completed = false; + this.currentTime = time; + this.muteCallbacks = false; + if (this.paused) this.pause(); + return this; } -}; -/** - * @param {TargetsParam} path - * @param {Number} [offset=0] - */ -const createMotionPath = (path, offset = 0) => { - const $path = getPath(path); - if (!$path) return; - return { - translateX: getPathProgess($path, 'x', offset), - translateY: getPathProgess($path, 'y', offset), - rotate: getPathProgess($path, 'a', offset), + restart() { + this.completed = false; + return this.seek(0, true).resume(); } -}; + commitStyles() { + return this.forEach('commitStyles'); + } - -/** - * @param {SVGGeometryElement} [$el] - * @return {Number} - */ -const getScaleFactor = $el => { - let scaleFactor = 1; - if ($el && $el.getCTM) { - const ctm = $el.getCTM(); - if (ctm) { - const scaleX = sqrt(ctm.a * ctm.a + ctm.b * ctm.b); - const scaleY = sqrt(ctm.c * ctm.c + ctm.d * ctm.d); - scaleFactor = (scaleX + scaleY) / 2; - } + complete() { + return this.seek(this.duration); } - return scaleFactor; -}; -/** - * Creates a proxy that wraps an SVGGeometryElement and adds drawing functionality. - * @param {SVGGeometryElement} $el - The SVG element to transform into a drawable - * @param {number} start - Starting position (0-1) - * @param {number} end - Ending position (0-1) - * @return {DrawableSVGGeometry} - Returns a proxy that preserves the original element's type with additional 'draw' attribute functionality - */ -const createDrawableProxy = ($el, start, end) => { - const pathLength = K; - const computedStyles = getComputedStyle($el); - const strokeLineCap = computedStyles.strokeLinecap; - // @ts-ignore - const $scalled = computedStyles.vectorEffect === 'non-scaling-stroke' ? $el : null; - let currentCap = strokeLineCap; + cancel() { + this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise + this.commitStyles().forEach('cancel'); + this.animations.length = 0; // Needed to release all animations from memory + requestAnimationFrame(() => { + this.targets.forEach(($el) => { // Needed to avoid unecessary inline transorms + if ($el.style.transform === 'none') $el.style.removeProperty('transform'); + }); + }); + return this; + } - const proxy = new Proxy($el, { - get(target, property) { - const value = target[property]; - if (property === proxyTargetSymbol) return target; - if (property === 'setAttribute') { - return (...args) => { - if (args[0] === 'draw') { - const value = args[1]; - const values = value.split(' '); - const v1 = +values[0]; - const v2 = +values[1]; - // TOTO: Benchmark if performing two slices is more performant than one split - // const spaceIndex = value.indexOf(' '); - // const v1 = round(+value.slice(0, spaceIndex), precision); - // const v2 = round(+value.slice(spaceIndex + 1), precision); - const scaleFactor = getScaleFactor($scalled); - const os = v1 * -pathLength * scaleFactor; - const d1 = (v2 * pathLength * scaleFactor) + os; - const d2 = (pathLength * scaleFactor + - ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10 * scaleFactor) - d1); - if (strokeLineCap !== 'butt') { - const newCap = v1 === v2 ? 'butt' : strokeLineCap; - if (currentCap !== newCap) { - target.style.strokeLinecap = `${newCap}`; - currentCap = newCap; - } - } - target.setAttribute('stroke-dashoffset', `${os}`); - target.setAttribute('stroke-dasharray', `${d1} ${d2}`); - } - return Reflect.apply(value, target, args); - }; + revert() { + // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted, + // This means if you have multiple animations animating different transforms on the same target, + // reverting one of them will also override the transform property of the other animations. + // A better approach would be to store the original custom property values if they exist instead of the entire transform value, + // and update the CSS variables with the orignal value + this.cancel().targets.forEach(($el, i) => { + const targetStyle = $el.style; + const targetInlineStyles = this._inlineStyles[i]; + for (let name in targetInlineStyles) { + const originalInlinedValue = targetInlineStyles[name]; + if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) { + targetStyle.removeProperty(toLowerCase(name)); + } else { + $el.style[name] = originalInlinedValue; + } } + // Remove style attribute if empty + if ($el.getAttribute('style') === emptyString) $el.removeAttribute('style'); + }); + return this; + } - if (isFnc(value)) { - return (...args) => Reflect.apply(value, target, args); - } else { - return value; - } - } - }); + /** + * @typedef {this & {then: null}} ResolvedWAAPIAnimation + */ - if ($el.getAttribute('pathLength') !== `${pathLength}`) { - $el.setAttribute('pathLength', `${pathLength}`); - proxy.setAttribute('draw', `${start} ${end}`); + /** + * @param {Callback} [callback] + * @return Promise + */ + then(callback = noop) { + const then = this.then; + const onResolve = () => { + this.then = null; + callback(/** @type {ResolvedWAAPIAnimation} */(this)); + this.then = then; + this._resolve = noop; + }; + return new Promise(r => { + this._resolve = () => r(onResolve()); + if (this.completed) this._resolve(); + return this; + }); } +} - return /** @type {DrawableSVGGeometry} */(proxy); -}; - +const waapi = { /** - * Creates drawable proxies for multiple SVG elements. - * @param {TargetsParam} selector - CSS selector, SVG element, or array of elements and selectors - * @param {number} [start=0] - Starting position (0-1) - * @param {number} [end=0] - Ending position (0-1) - * @return {Array} - Array of proxied elements with drawing functionality + * @param {DOMTargetsParam} targets + * @param {WAAPIAnimationParams} params + * @return {WAAPIAnimation} */ -const createDrawable = (selector, start = 0, end = 0) => { - const els = parseTargets(selector); - return els.map($el => createDrawableProxy( - /** @type {SVGGeometryElement} */($el), - start, - end - )); + animate: (targets, params) => new WAAPIAnimation(targets, params), + convertEase: easingToLinear }; -/** - * @param {TargetsParam} path2 - * @param {Number} [precision] - * @return {FunctionValue} - */ -const morphTo = (path2, precision = .33) => ($path1) => { - const tagName1 = ($path1.tagName || '').toLowerCase(); - if (!tagName1.match(/^(path|polygon|polyline)$/)) { - throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use , or .`); - } - const $path2 = /** @type {SVGGeometryElement} */(getPath(path2)); - if (!$path2) { - throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing , or SVG element."); - } - const tagName2 = ($path2.tagName || '').toLowerCase(); - if (!tagName2.match(/^(path|polygon|polyline)$/)) { - throw new Error(`Can't morph a <${$path2.tagName}> SVG element. Use , or .`); - } - const isPath = $path1.tagName === 'path'; - const separator = isPath ? ' ' : ','; - const previousPoints = $path1[morphPointsSymbol]; - if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints); - let v1 = '', v2 = ''; - if (!precision) { - v1 = $path1.getAttribute(isPath ? 'd' : 'points'); - v2 = $path2.getAttribute(isPath ? 'd' : 'points'); - } else { - const length1 = /** @type {SVGGeometryElement} */($path1).getTotalLength(); - const length2 = $path2.getTotalLength(); - const maxPoints = Math.max(Math.ceil(length1 * precision), Math.ceil(length2 * precision)); - for (let i = 0; i < maxPoints; i++) { - const t = i / (maxPoints - 1); - const pointOnPath1 = /** @type {SVGGeometryElement} */($path1).getPointAtLength(length1 * t); - const pointOnPath2 = $path2.getPointAtLength(length2 * t); - const prefix = isPath ? (i === 0 ? 'M' : 'L') : ''; - v1 += prefix + round$1(pointOnPath1.x, 3) + separator + pointOnPath1.y + ' '; - v2 += prefix + round$1(pointOnPath2.x, 3) + separator + pointOnPath2.y + ' '; - } - } - $path1[morphPointsSymbol] = v2; - return [v1, v2]; -}; -var index$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - createDrawable: createDrawable, - createMotionPath: createMotionPath, - morphTo: morphTo -}); -const segmenter = (typeof Intl !== 'undefined') && Intl.Segmenter; -const valueRgx = /\{value\}/g; -const indexRgx = /\{i\}/g; -const whiteSpaceGroupRgx = /(\s+)/; -const whiteSpaceRgx = /^\s+$/; -const lineType = 'line'; -const wordType = 'word'; -const charType = 'char'; -const dataLine = `data-line`; + /** - * @typedef {Object} Segment - * @property {String} segment - * @property {Boolean} [isWordLike] + * @typedef {DOMTargetSelector|Array} LayoutChildrenParam */ /** - * @typedef {Object} Segmenter - * @property {function(String): Iterable} segment + * @typedef {Object} LayoutAnimationTimingsParams + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] */ -/** @type {Segmenter} */ -let wordSegmenter = null; -/** @type {Segmenter} */ -let graphemeSegmenter = null; -let $splitTemplate = null; +/** + * @typedef {Record} LayoutStateAnimationProperties + */ /** - * @param {Segment} seg - * @return {Boolean} + * @typedef {LayoutStateAnimationProperties & LayoutAnimationTimingsParams} LayoutStateParams */ -const isSegmentWordLike = seg => { - return seg.isWordLike || - seg.segment === ' ' || // Consider spaces as words first, then handle them diffrently later - isNum(+seg.segment); // Safari doesn't considers numbers as words -}; /** - * @param {HTMLElement} $el + * @typedef {Object} LayoutSpecificAnimationParams + * @property {Number|String} [id] + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] + * @property {EasingParam} [playbackEase] + * @property {LayoutStateParams} [swapAt] + * @property {LayoutStateParams} [enterFrom] + * @property {LayoutStateParams} [leaveTo] */ -const setAriaHidden = $el => $el.setAttribute('aria-hidden', 'true'); /** - * @param {DOMTarget} $el - * @param {String} type - * @return {Array} + * @typedef {LayoutSpecificAnimationParams & TimerParams & TickableCallbacks & RenderableCallbacks} LayoutAnimationParams */ -const getAllTopLevelElements = ($el, type) => [.../** @type {*} */($el.querySelectorAll(`[data-${type}]:not([data-${type}] [data-${type}])`))]; -const debugColors = { line: '#00D672', word: '#FF4B4B', char: '#5A87FF' }; +/** + * @typedef {Object} LayoutOptions + * @property {LayoutChildrenParam} [children] + * @property {Array} [properties] + */ /** - * @param {HTMLElement} $el + * @typedef {LayoutAnimationParams & LayoutOptions} AutoLayoutParams */ -const filterEmptyElements = $el => { - if (!$el.childElementCount && !$el.textContent.trim()) { - const $parent = $el.parentElement; - $el.remove(); - if ($parent) filterEmptyElements($parent); - } -}; /** - * @param {HTMLElement} $el - * @param {Number} lineIndex - * @param {Set} bin - * @returns {Set} + * @typedef {Record & { + * transform: String, + * x: Number, + * y: Number, + * left: Number, + * top: Number, + * clientLeft: Number, + * clientTop: Number, + * width: Number, + * height: Number, + * }} LayoutNodeProperties */ -const filterLineElements = ($el, lineIndex, bin) => { - const dataLineAttr = $el.getAttribute(dataLine); - if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') bin.add($el); - let i = $el.childElementCount; - while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin); - return bin; -}; /** - * @param {'line'|'word'|'char'} type - * @param {SplitTemplateParams} params - * @return {String} + * @typedef {Object} LayoutNode + * @property {String} id + * @property {DOMTarget} $el + * @property {Number} index + * @property {Array} targets + * @property {Number} delay + * @property {Number} duration + * @property {EasingParam} ease + * @property {DOMTarget} $measure + * @property {LayoutSnapshot} state + * @property {AutoLayout} layout + * @property {LayoutNode|null} parentNode + * @property {Boolean} isTarget + * @property {Boolean} isEntering + * @property {Boolean} isLeaving + * @property {Boolean} hasTransform + * @property {Array} inlineStyles + * @property {String|null} inlineTransforms + * @property {String|null} inlineTransition + * @property {Boolean} branchAdded + * @property {Boolean} branchRemoved + * @property {Boolean} branchNotRendered + * @property {Boolean} sizeChanged + * @property {Boolean} isInlined + * @property {Boolean} hasVisibilitySwap + * @property {Boolean} hasDisplayNone + * @property {Boolean} hasVisibilityHidden + * @property {String|null} measuredInlineTransform + * @property {String|null} measuredInlineTransition + * @property {String|null} measuredDisplay + * @property {String|null} measuredVisibility + * @property {String|null} measuredPosition + * @property {Boolean} measuredHasDisplayNone + * @property {Boolean} measuredHasVisibilityHidden + * @property {Boolean} measuredIsVisible + * @property {Boolean} measuredIsRemoved + * @property {Boolean} measuredIsInsideRoot + * @property {LayoutNodeProperties} properties + * @property {LayoutNode|null} _head + * @property {LayoutNode|null} _tail + * @property {LayoutNode|null} _prev + * @property {LayoutNode|null} _next + */ + +/** + * @callback LayoutNodeIterator + * @param {LayoutNode} node + * @param {Number} index + * @return {void} */ -const generateTemplate = (type, params = {}) => { - let template = ``; - const classString = isStr(params.class) ? ` class="${params.class}"` : ''; - const cloneType = setValue(params.clone, false); - const wrapType = setValue(params.wrap, false); - const overflow = wrapType ? wrapType === true ? 'clip' : wrapType : cloneType ? 'clip' : false; - if (wrapType) template += ``; - template += ``; - if (cloneType) { - const left = cloneType === 'left' ? '-100%' : cloneType === 'right' ? '100%' : '0'; - const top = cloneType === 'top' ? '-100%' : cloneType === 'bottom' ? '100%' : '0'; - template += `{value}`; - template += `{value}`; + +let layoutId = 0; +let nodeId = 0; + +/** + * @param {DOMTarget} root + * @param {DOMTarget} $el + * @return {Boolean} + */ +const isElementInRoot = (root, $el) => { + if (!root || !$el) return false; + return root === $el || root.contains($el); +}; + +/** + * @param {DOMTarget|null} $el + * @return {String|null} + */ +const muteElementTransition = $el => { + if (!$el) return null; + const style = $el.style; + const transition = style.transition || ''; + style.setProperty('transition', 'none', 'important'); + return transition; +}; + +/** + * @param {DOMTarget|null} $el + * @param {String|null} transition + */ +const restoreElementTransition = ($el, transition) => { + if (!$el) return; + const style = $el.style; + if (transition) { + style.transition = transition; } else { - template += `{value}`; + style.removeProperty('transition'); } - template += ``; - if (wrapType) template += ``; - return template; }; /** - * @param {String|SplitFunctionValue} htmlTemplate - * @param {Array} store - * @param {Node|HTMLElement} node - * @param {DocumentFragment} $parentFragment - * @param {'line'|'word'|'char'} type - * @param {Boolean} debug - * @param {Number} lineIndex - * @param {Number} [wordIndex] - * @param {Number} [charIndex] - * @return {HTMLElement} + * @param {LayoutNode} node */ -const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, debug, lineIndex, wordIndex, charIndex) => { - const isLine = type === lineType; - const isChar = type === charType; - const className = `_${type}_`; - const template = isFnc(htmlTemplate) ? htmlTemplate(node) : htmlTemplate; - const displayStyle = isLine ? 'block' : 'inline-block'; - $splitTemplate.innerHTML = template - .replace(valueRgx, ``) - .replace(indexRgx, `${isChar ? charIndex : isLine ? lineIndex : wordIndex}`); - const $content = $splitTemplate.content; - const $highestParent = /** @type {HTMLElement} */($content.firstElementChild); - const $split = /** @type {HTMLElement} */($content.querySelector(`[data-${type}]`)) || $highestParent; - const $replacables = /** @type {NodeListOf} */($content.querySelectorAll(`i.${className}`)); - const replacablesLength = $replacables.length; - if (replacablesLength) { - $highestParent.style.display = displayStyle; - $split.style.display = displayStyle; - $split.setAttribute(dataLine, `${lineIndex}`); - if (!isLine) { - $split.setAttribute('data-word', `${wordIndex}`); - if (isChar) $split.setAttribute('data-char', `${charIndex}`); +const muteNodeTransition = node => { + const store = node.layout.transitionMuteStore; + const $el = node.$el; + const $measure = node.$measure; + if ($el && !store.has($el)) store.set($el, muteElementTransition($el)); + if ($measure && !store.has($measure)) store.set($measure, muteElementTransition($measure)); +}; + +/** + * @param {Map} store + */ +const restoreLayoutTransition = store => { + store.forEach((value, $el) => restoreElementTransition($el, value)); + store.clear(); +}; + +const hiddenComputedStyle = /** @type {CSSStyleDeclaration} */({ + display: 'none', + visibility: 'hidden', + opacity: '0', + transform: 'none', + position: 'static', +}); + +/** + * @param {LayoutNode|null} node + */ +const detachNode = node => { + if (!node) return; + const parent = node.parentNode; + if (!parent) return; + if (parent._head === node) parent._head = node._next; + if (parent._tail === node) parent._tail = node._prev; + if (node._prev) node._prev._next = node._next; + if (node._next) node._next._prev = node._prev; + node._prev = null; + node._next = null; + node.parentNode = null; +}; + +/** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @param {LayoutSnapshot} state + * @param {LayoutNode} recycledNode + * @return {LayoutNode} + */ +const createNode = ($el, parentNode, state, recycledNode) => { + let dataId = $el.dataset.layoutId; + if (!dataId) dataId = $el.dataset.layoutId = `node-${nodeId++}`; + const node = recycledNode ? recycledNode : /** @type {LayoutNode} */({}); + node.$el = $el; + node.$measure = $el; + node.id = dataId; + node.index = 0; + node.targets = null; + node.delay = 0; + node.duration = 0; + node.ease = null; + node.state = state; + node.layout = state.layout; + node.parentNode = parentNode || null; + node.isTarget = false; + node.isEntering = false; + node.isLeaving = false; + node.isInlined = false; + node.hasTransform = false; + node.inlineStyles = []; + node.inlineTransforms = null; + node.inlineTransition = null; + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.sizeChanged = false; + node.hasVisibilitySwap = false; + node.hasDisplayNone = false; + node.hasVisibilityHidden = false; + node.measuredInlineTransform = null; + node.measuredInlineTransition = null; + node.measuredDisplay = null; + node.measuredVisibility = null; + node.measuredPosition = null; + node.measuredHasDisplayNone = false; + node.measuredHasVisibilityHidden = false; + node.measuredIsVisible = false; + node.measuredIsRemoved = false; + node.measuredIsInsideRoot = false; + node.properties = /** @type {LayoutNodeProperties} */({ + transform: 'none', + x: 0, + y: 0, + left: 0, + top: 0, + clientLeft: 0, + clientTop: 0, + width: 0, + height: 0, + }); + node.layout.properties.forEach(prop => node.properties[prop] = 0); + node._head = null; + node._tail = null; + node._prev = null; + node._next = null; + return node; +}; + +/** + * @param {LayoutNode} node + * @param {DOMTarget} $measure + * @param {CSSStyleDeclaration} computedStyle + * @param {Boolean} skipMeasurements + * @return {LayoutNode} + */ +const recordNodeState = (node, $measure, computedStyle, skipMeasurements) => { + const $el = node.$el; + const root = node.layout.root; + const isRoot = root === $el; + const properties = node.properties; + const rootNode = node.state.rootNode; + const parentNode = node.parentNode; + const computedTransforms = computedStyle.transform; + const inlineTransforms = $el.style.transform; + const parentNotRendered = parentNode ? parentNode.measuredIsRemoved : false; + const position = computedStyle.position; + if (isRoot) node.layout.absoluteCoords = position === 'fixed' || position === 'absolute'; + node.$measure = $measure; + node.inlineTransforms = inlineTransforms; + node.hasTransform = computedTransforms && computedTransforms !== 'none'; + node.measuredIsInsideRoot = isElementInRoot(root, $measure); + node.measuredInlineTransform = null; + node.measuredDisplay = computedStyle.display; + node.measuredVisibility = computedStyle.visibility; + node.measuredPosition = position; + node.measuredHasDisplayNone = computedStyle.display === 'none'; + node.measuredHasVisibilityHidden = computedStyle.visibility === 'hidden'; + node.measuredIsVisible = !(node.measuredHasDisplayNone || node.measuredHasVisibilityHidden); + node.measuredIsRemoved = node.measuredHasDisplayNone || node.measuredHasVisibilityHidden || parentNotRendered; + // Check if element has adjacent text that would reflow when taken out of flow + let hasAdjacentText = false; + let s = $el.previousSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.previousSibling; + if (s && s.nodeType === Node.TEXT_NODE) { + hasAdjacentText = true; + } else { + s = $el.nextSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.nextSibling; + hasAdjacentText = s !== null && s.nodeType === Node.TEXT_NODE; + } + node.isInlined = hasAdjacentText; + + // Mute transforms (and transition to avoid triggering an animation) before the position calculation + if (node.hasTransform && !skipMeasurements) { + const transitionMuteStore = node.layout.transitionMuteStore; + if (!transitionMuteStore.get($el)) node.inlineTransition = muteElementTransition($el); + if ($measure === $el) { + $el.style.transform = 'none'; + } else { + if (!transitionMuteStore.get($measure)) node.measuredInlineTransition = muteElementTransition($measure); + node.measuredInlineTransform = $measure.style.transform; + $measure.style.transform = 'none'; } - let i = replacablesLength; - while (i--) { - const $replace = $replacables[i]; - const $closestParent = $replace.parentElement; - $closestParent.style.display = displayStyle; - if (isLine) { - $closestParent.innerHTML = /** @type {HTMLElement} */(node).innerHTML; + } + + let left = 0; + let top = 0; + let width = 0; + let height = 0; + + if (!skipMeasurements) { + const rect = $measure.getBoundingClientRect(); + left = rect.left; + top = rect.top; + width = rect.width; + height = rect.height; + } + + for (let name in properties) { + const computedProp = name === 'transform' ? computedTransforms : computedStyle[name] || (computedStyle.getPropertyValue && computedStyle.getPropertyValue(name)); + if (!isUnd(computedProp)) properties[name] = computedProp; + } + + properties.left = left; + properties.top = top; + properties.clientLeft = skipMeasurements ? 0 : $measure.clientLeft; + properties.clientTop = skipMeasurements ? 0 : $measure.clientTop; + // Compute local x/y relative to parent + let absoluteLeft, absoluteTop; + if (isRoot) { + if (!node.layout.absoluteCoords) { + absoluteLeft = 0; + absoluteTop = 0; + } else { + absoluteLeft = left; + absoluteTop = top; + } + } else { + const p = parentNode || rootNode; + const parentLeft = p.properties.left; + const parentTop = p.properties.top; + const borderLeft = p.properties.clientLeft; + const borderTop = p.properties.clientTop; + if (!node.layout.absoluteCoords) { + if (p === rootNode) { + const rootLeft = rootNode.properties.left; + const rootTop = rootNode.properties.top; + const rootBorderLeft = rootNode.properties.clientLeft; + const rootBorderTop = rootNode.properties.clientTop; + absoluteLeft = left - rootLeft - rootBorderLeft; + absoluteTop = top - rootTop - rootBorderTop; } else { - $closestParent.replaceChild(node.cloneNode(true), $replace); + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; } + } else { + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; } - store.push($split); - $parentFragment.appendChild($content); - } else { - console.warn(`The expression "{value}" is missing from the provided template.`); } - if (debug) $highestParent.style.outline = `1px dotted ${debugColors[type]}`; - return $highestParent; + properties.x = absoluteLeft; + properties.y = absoluteTop; + properties.width = width; + properties.height = height; + return node; }; /** - * A class that splits text into words and wraps them in span elements while preserving the original HTML structure. - * @class + * @param {LayoutNode} node + * @param {LayoutStateAnimationProperties} [props] */ -class TextSplitter { - /** - * @param {HTMLElement|NodeList|String|Array} target - * @param {TextSplitterParams} [parameters] - */ - constructor(target, parameters = {}) { - // Only init segmenters when needed - if (!wordSegmenter) wordSegmenter = segmenter ? new segmenter([], { granularity: wordType }) : { - segment: (text) => { - const segments = []; - const words = text.split(whiteSpaceGroupRgx); - for (let i = 0, l = words.length; i < l; i++) { - const segment = words[i]; - segments.push({ - segment, - isWordLike: !whiteSpaceRgx.test(segment), // Consider non-whitespace as word-like - }); - } - return segments; - } - }; - if (!graphemeSegmenter) graphemeSegmenter = segmenter ? new segmenter([], { granularity: 'grapheme' }) : { - segment: text => [...text].map(char => ({ segment: char })) - }; - if (!$splitTemplate && isBrowser) $splitTemplate = doc.createElement('template'); - if (scope.current) scope.current.register(this); - const { words, chars, lines, accessible, includeSpaces, debug } = parameters; - const $target = /** @type {HTMLElement} */((target = isArr(target) ? target[0] : target) && /** @type {Node} */(target).nodeType ? target : (getNodeList(target) || [])[0]); - const lineParams = lines === true ? {} : lines; - const wordParams = words === true || isUnd(words) ? {} : words; - const charParams = chars === true ? {} : chars; - this.debug = setValue(debug, false); - this.includeSpaces = setValue(includeSpaces, false); - this.accessible = setValue(accessible, true); - this.linesOnly = lineParams && (!wordParams && !charParams); - /** @type {String|false|SplitFunctionValue} */ - this.lineTemplate = isObj(lineParams) ? generateTemplate(lineType, /** @type {SplitTemplateParams} */(lineParams)) : lineParams; - /** @type {String|false|SplitFunctionValue} */ - this.wordTemplate = isObj(wordParams) || this.linesOnly ? generateTemplate(wordType, /** @type {SplitTemplateParams} */(wordParams)) : wordParams; - /** @type {String|false|SplitFunctionValue} */ - this.charTemplate = isObj(charParams) ? generateTemplate(charType, /** @type {SplitTemplateParams} */(charParams)) : charParams; - this.$target = $target; - this.html = $target && $target.innerHTML; - this.lines = []; - this.words = []; - this.chars = []; - this.effects = []; - this.effectsCleanups = []; - this.cache = null; - this.ready = false; - this.width = 0; - this.resizeTimeout = null; - const handleSplit = () => this.html && (lineParams || wordParams || charParams) && this.split(); - // Make sure this is declared before calling handleSplit() in case revert() is called inside an effect callback - this.resizeObserver = new ResizeObserver(() => { - // Use a setTimeout instead of a Timer for better tree shaking - clearTimeout(this.resizeTimeout); - this.resizeTimeout = setTimeout(() => { - const currentWidth = /** @type {HTMLElement} */($target).offsetWidth; - if (currentWidth === this.width) return; - this.width = currentWidth; - handleSplit(); - }, 150); - }); - // Only declare the font ready promise when splitting by lines and not alreay split - if (this.lineTemplate && !this.ready) { - doc.fonts.ready.then(handleSplit); +const updateNodeProperties = (node, props) => { + if (!props) return; + for (let name in props) { + node.properties[name] = props[name]; + } +}; + +/** + * @param {LayoutNode} node + * @param {LayoutAnimationTimingsParams} params + */ +const updateNodeTimingParams = (node, params) => { + const easeFunctionResult = getFunctionValue(params.ease, node.$el, node.index, node.targets, null, null); + const keyEasing = isFnc(easeFunctionResult) ? easeFunctionResult : params.ease; + const hasSpring = !isUnd(keyEasing) && !isUnd(/** @type {Spring} */(keyEasing).ease); + node.ease = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; + node.duration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(params.duration, node.$el, node.index, node.targets, null, null); + node.delay = getFunctionValue(params.delay, node.$el, node.index, node.targets, null, null); +}; + +/** + * @param {LayoutNode} node + */ +const recordNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + stylesStore.length = 0; + node.layout.recordedProperties.forEach(prop => { + stylesStore.push(prop, style[prop] || ''); + }); +}; + +/** + * @param {LayoutNode} node + */ +const restoreNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + for (let i = 0, l = stylesStore.length; i < l; i += 2) { + const property = stylesStore[i]; + const styleValue = stylesStore[i + 1]; + if (styleValue && styleValue !== '') { + style[property] = styleValue; } else { - handleSplit(); + style[property] = ''; + style.removeProperty(property); + } + } +}; + +/** + * @param {LayoutNode} node + */ +const restoreNodeTransform = node => { + const inlineTransforms = node.inlineTransforms; + const nodeStyle = node.$el.style; + if (!node.hasTransform || !inlineTransforms || (node.hasTransform && nodeStyle.transform === 'none') || (inlineTransforms && inlineTransforms === 'none')) { + nodeStyle.removeProperty('transform'); + } else if (inlineTransforms) { + nodeStyle.transform = inlineTransforms; + } + const $measure = node.$measure; + if (node.hasTransform && $measure !== node.$el) { + const measuredStyle = $measure.style; + const measuredInline = node.measuredInlineTransform; + if (measuredInline && measuredInline !== '') { + measuredStyle.transform = measuredInline; + } else { + measuredStyle.removeProperty('transform'); + } + } + node.measuredInlineTransform = null; + if (node.inlineTransition !== null) { + restoreElementTransition(node.$el, node.inlineTransition); + node.inlineTransition = null; + } + if ($measure !== node.$el && node.measuredInlineTransition !== null) { + restoreElementTransition($measure, node.measuredInlineTransition); + node.measuredInlineTransition = null; + } +}; + +/** + * @param {LayoutNode} node + */ +const restoreNodeVisualState = node => { + if (node.measuredIsRemoved || node.hasVisibilitySwap) { + node.$el.style.removeProperty('display'); + node.$el.style.removeProperty('visibility'); + if (node.hasVisibilitySwap) { + node.$measure.style.removeProperty('display'); + node.$measure.style.removeProperty('visibility'); } - $target ? this.resizeObserver.observe($target) : console.warn('No Text Splitter target found.'); } + // if (node.measuredIsRemoved) { + node.layout.pendingRemoval.delete(node.$el); + // } +}; + +/** + * @param {LayoutNode} node + * @param {LayoutNode} targetNode + * @param {LayoutSnapshot} newState + * @return {LayoutNode} + */ +const cloneNodeProperties = (node, targetNode, newState) => { + targetNode.properties = /** @type {LayoutNodeProperties} */({ ...node.properties }); + targetNode.state = newState; + targetNode.isTarget = node.isTarget; + targetNode.hasTransform = node.hasTransform; + targetNode.inlineTransforms = node.inlineTransforms; + targetNode.measuredIsVisible = node.measuredIsVisible; + targetNode.measuredDisplay = node.measuredDisplay; + targetNode.measuredIsRemoved = node.measuredIsRemoved; + targetNode.measuredHasDisplayNone = node.measuredHasDisplayNone; + targetNode.measuredHasVisibilityHidden = node.measuredHasVisibilityHidden; + targetNode.hasDisplayNone = node.hasDisplayNone; + targetNode.isInlined = node.isInlined; + targetNode.hasVisibilityHidden = node.hasVisibilityHidden; + return targetNode; +}; +class LayoutSnapshot { /** - * @param {(...args: any[]) => Tickable | (() => void)} effect - * @return this + * @param {AutoLayout} layout */ - addEffect(effect) { - if (!isFnc(effect)) return console.warn('Effect must return a function.'); - const refreshableEffect = keepTime(effect); - this.effects.push(refreshableEffect); - if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this); - return this; + constructor(layout) { + /** @type {AutoLayout} */ + this.layout = layout; + /** @type {LayoutNode|null} */ + this.rootNode = null; + /** @type {Set} */ + this.rootNodes = new Set(); + /** @type {Map} */ + this.nodes = new Map(); + /** @type {Number} */ + this.scrollX = 0; + /** @type {Number} */ + this.scrollY = 0; } + /** + * @return {this} + */ revert() { - clearTimeout(this.resizeTimeout); - this.lines.length = this.words.length = this.chars.length = 0; - this.resizeObserver.disconnect(); - // Make sure to revert the effects after disconnecting the resizeObserver to avoid triggering it in the process - this.effectsCleanups.forEach(cleanup => isFnc(cleanup) ? cleanup(this) : cleanup.revert && cleanup.revert()); - this.$target.innerHTML = this.html; + this.forEachNode(node => { + this.layout.pendingRemoval.delete(node.$el); + node.$el.removeAttribute('data-layout-id'); + node.$measure.removeAttribute('data-layout-id'); + }); + this.rootNode = null; + this.rootNodes.clear(); + this.nodes.clear(); return this; } /** - * Recursively processes a node and its children - * @param {Node} node + * @param {DOMTarget} $el + * @return {LayoutNode} */ - splitNode(node) { - const wordTemplate = this.wordTemplate; - const charTemplate = this.charTemplate; - const includeSpaces = this.includeSpaces; - const debug = this.debug; - const nodeType = node.nodeType; - if (nodeType === 3) { - const nodeText = node.nodeValue; - // If the nodeText is only whitespace, leave it as is - if (nodeText.trim()) { - const tempWords = []; - const words = this.words; - const chars = this.chars; - const wordSegments = wordSegmenter.segment(nodeText); - const $wordsFragment = doc.createDocumentFragment(); - let prevSeg = null; - for (const wordSegment of wordSegments) { - const segment = wordSegment.segment; - const isWordLike = isSegmentWordLike(wordSegment); - // Determine if this segment should be a new word, first segment always becomes a new word - if (!prevSeg || (isWordLike && (prevSeg && (isSegmentWordLike(prevSeg))))) { - tempWords.push(segment); - } else { - // Only concatenate if both current and previous are non-word-like and don't contain spaces - const lastWordIndex = tempWords.length - 1; - const lastWord = tempWords[lastWordIndex]; - if (!lastWord.includes(' ') && !segment.includes(' ')) { - tempWords[lastWordIndex] += segment; - } else { - tempWords.push(segment); - } - } - prevSeg = wordSegment; - } + getNode($el) { + if (!$el || !$el.dataset) return; + return this.nodes.get($el.dataset.layoutId); + } - for (let i = 0, l = tempWords.length; i < l; i++) { - const word = tempWords[i]; - if (!word.trim()) { - // Preserve whitespace only if includeSpaces is false and if the current space is not the first node - if (i && includeSpaces) continue; - $wordsFragment.appendChild(doc.createTextNode(word)); - } else { - const nextWord = tempWords[i + 1]; - const hasWordFollowingSpace = includeSpaces && nextWord && !nextWord.trim(); - const wordToProcess = word; - const charSegments = charTemplate ? graphemeSegmenter.segment(wordToProcess) : null; - const $charsFragment = charTemplate ? doc.createDocumentFragment() : doc.createTextNode(hasWordFollowingSpace ? word + '\xa0' : word); - if (charTemplate) { - const charSegmentsArray = [...charSegments]; - for (let j = 0, jl = charSegmentsArray.length; j < jl; j++) { - const charSegment = charSegmentsArray[j]; - const isLastChar = j === jl - 1; - // If this is the last character and includeSpaces is true with a following space, append the space - const charText = isLastChar && hasWordFollowingSpace ? charSegment.segment + '\xa0' : charSegment.segment; - const $charNode = doc.createTextNode(charText); - processHTMLTemplate(charTemplate, chars, $charNode, /** @type {DocumentFragment} */($charsFragment), charType, debug, -1, words.length, chars.length); - } - } - if (wordTemplate) { - processHTMLTemplate(wordTemplate, words, $charsFragment, $wordsFragment, wordType, debug, -1, words.length, chars.length); - // Chars elements must be re-parsed in the split() method if both words and chars are parsed - } else if (charTemplate) { - $wordsFragment.appendChild($charsFragment); - } else { - $wordsFragment.appendChild(doc.createTextNode(word)); - } - // Skip the next iteration if we included a space - if (hasWordFollowingSpace) i++; - } + /** + * @param {DOMTarget} $el + * @param {String} prop + * @return {Number|String} + */ + getComputedValue($el, prop) { + const node = this.getNode($el); + if (!node) return; + return /** @type {Number|String} */(node.properties[prop]); + } + + /** + * @param {LayoutNode|null} rootNode + * @param {LayoutNodeIterator} cb + */ + forEach(rootNode, cb) { + let node = rootNode; + let i = 0; + while (node) { + cb(node, i++); + if (node._head) { + node = node._head; + } else if (node._next) { + node = node._next; + } else { + while (node && !node._next) { + node = node.parentNode; } - node.parentNode.replaceChild($wordsFragment, node); + if (node) node = node._next; } - } else if (nodeType === 1) { - // Converting to an array is necessary to work around childNodes pottential mutation - const childNodes = /** @type {Array} */([.../** @type {*} */(node.childNodes)]); - for (let i = 0, l = childNodes.length; i < l; i++) this.splitNode(childNodes[i]); } } /** - * @param {Boolean} clearCache - * @return {this} + * @param {LayoutNodeIterator} cb */ - split(clearCache = false) { - const $el = this.$target; - const isCached = !!this.cache && !clearCache; - const lineTemplate = this.lineTemplate; - const wordTemplate = this.wordTemplate; - const charTemplate = this.charTemplate; - const fontsReady = doc.fonts.status !== 'loading'; - const canSplitLines = lineTemplate && fontsReady; - this.ready = !lineTemplate || fontsReady; - if (canSplitLines || clearCache) { - // No need to revert effects animations here since it's already taken care by the refreshable - this.effectsCleanups.forEach(cleanup => isFnc(cleanup) && cleanup(this)); + forEachRootNode(cb) { + this.forEach(this.rootNode, cb); + } + + /** + * @param {LayoutNodeIterator} cb + */ + forEachNode(cb) { + for (const rootNode of this.rootNodes) { + this.forEach(rootNode, cb); } - if (!isCached) { - if (clearCache) { - $el.innerHTML = this.html; - this.words.length = this.chars.length = 0; + } + + /** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @return {LayoutNode|null} + */ + registerElement($el, parentNode) { + if (!$el || $el.nodeType !== 1) return null; + + if (!this.layout.transitionMuteStore.has($el)) this.layout.transitionMuteStore.set($el, muteElementTransition($el)); + + /** @type {Array} */ + const stack = [$el, parentNode]; + const root = this.layout.root; + let firstNode = null; + + while (stack.length) { + /** @type {LayoutNode|null} */ + const $parent = /** @type {LayoutNode|null} */(stack.pop()); + /** @type {DOMTarget|null} */ + const $current = /** @type {DOMTarget|null} */(stack.pop()); + + if (!$current || $current.nodeType !== 1 || isSvg($current)) continue; + + const skipMeasurements = $parent ? $parent.measuredIsRemoved : false; + const computedStyle = skipMeasurements ? hiddenComputedStyle : getComputedStyle($current); + const hasDisplayNone = skipMeasurements ? true : computedStyle.display === 'none'; + const hasVisibilityHidden = skipMeasurements ? true : computedStyle.visibility === 'hidden'; + const isVisible = !hasDisplayNone && !hasVisibilityHidden; + const existingId = $current.dataset.layoutId; + const isInsideRoot = isElementInRoot(root, $current); + + let node = existingId ? this.nodes.get(existingId) : null; + + if (node && node.$el !== $current) { + const nodeInsideRoot = isElementInRoot(root, node.$el); + const measuredVisible = node.measuredIsVisible; + const shouldReassignNode = !nodeInsideRoot && (isInsideRoot || (!isInsideRoot && !measuredVisible && isVisible)); + const shouldReuseMeasurements = nodeInsideRoot && !measuredVisible && isVisible; + // Rebind nodes that move into the root or whose detached twin just became visible + if (shouldReassignNode) { + detachNode(node); + node = createNode($current, $parent, this, node); + // for hidden element with in-root sibling, keep the hidden node but borrow measurements from its visible in-root twin element + } else if (shouldReuseMeasurements) { + recordNodeState(node, $current, computedStyle, skipMeasurements); + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; + } + if (!firstNode) firstNode = node; + continue; + // No reassignment needed so keep walking descendants under the current parent + } else { + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), $parent); + $child = $child.previousElementSibling; + } + if (!firstNode) firstNode = node; + continue; + } + } else { + node = createNode($current, $parent, this, node); } - this.splitNode($el); - this.cache = $el.innerHTML; - } - if (canSplitLines) { - if (isCached) $el.innerHTML = this.cache; - this.lines.length = 0; - if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); - } - // Always reparse characters after a line reset or if both words and chars are activated - if (charTemplate && (canSplitLines || wordTemplate)) { - this.chars = getAllTopLevelElements($el, charType); - } - // Words are used when lines only and prioritized over chars - const elementsArray = this.words.length ? this.words : this.chars; - let y, linesCount = 0; - for (let i = 0, l = elementsArray.length; i < l; i++) { - const $el = elementsArray[i]; - const { top, height } = $el.getBoundingClientRect(); - if (y && top - y > height * .5) linesCount++; - $el.setAttribute(dataLine, `${linesCount}`); - const nested = $el.querySelectorAll(`[${dataLine}]`); - let c = nested.length; - while (c--) nested[c].setAttribute(dataLine, `${linesCount}`); - y = top; - } - if (canSplitLines) { - const linesFragment = doc.createDocumentFragment(); - const parents = new Set(); - const clones = []; - for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) { - const $clone = /** @type {HTMLElement} */($el.cloneNode(true)); - filterLineElements($clone, lineIndex, new Set()).forEach($el => { - const $parent = $el.parentElement; - if ($parent) parents.add($parent); - $el.remove(); - }); - clones.push($clone); + + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.isTarget = false; + node.sizeChanged = false; + node.hasVisibilityHidden = hasVisibilityHidden; + node.hasDisplayNone = hasDisplayNone; + node.hasVisibilitySwap = (hasVisibilityHidden && !node.measuredHasVisibilityHidden) || (hasDisplayNone && !node.measuredHasDisplayNone); + + this.nodes.set(node.id, node); + + node.parentNode = $parent || null; + node._prev = null; + node._next = null; + + if ($parent) { + this.rootNodes.delete(node); + if (!$parent._head) { + $parent._head = node; + $parent._tail = node; + } else { + $parent._tail._next = node; + node._prev = $parent._tail; + $parent._tail = node; + } + } else { + // Each disconnected subtree becomes its own root in the snapshot graph + this.rootNodes.add(node); } - parents.forEach(filterEmptyElements); - for (let cloneIndex = 0, clonesLength = clones.length; cloneIndex < clonesLength; cloneIndex++) { - processHTMLTemplate(lineTemplate, this.lines, clones[cloneIndex], linesFragment, lineType, this.debug, cloneIndex); + + recordNodeState(node, node.$el, computedStyle, skipMeasurements); + + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; } - $el.innerHTML = ''; - $el.appendChild(linesFragment); - if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); - if (charTemplate) this.chars = getAllTopLevelElements($el, charType); + + if (!firstNode) firstNode = node; } - // Remove the word wrappers and clear the words array if lines split only - if (this.linesOnly) { - const words = this.words; - let w = words.length; - while (w--) { - const $word = words[w]; - $word.replaceWith($word.textContent); + + return firstNode; + } + + /** + * @param {DOMTarget} $el + * @param {Set} candidates + * @return {LayoutNode|null} + */ + ensureDetachedNode($el, candidates) { + if (!$el || $el === this.layout.root) return null; + const existingId = $el.dataset.layoutId; + const existingNode = existingId ? this.nodes.get(existingId) : null; + if (existingNode && existingNode.$el === $el) return existingNode; + let parentNode = null; + let $ancestor = $el.parentElement; + while ($ancestor && $ancestor !== this.layout.root) { + if (candidates.has($ancestor)) { + parentNode = this.ensureDetachedNode($ancestor, candidates); + break; } - words.length = 0; + $ancestor = $ancestor.parentElement; } - if (this.accessible && (canSplitLines || !isCached)) { - const $accessible = doc.createElement('span'); - // Make the accessible element visually-hidden (https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html) - $accessible.style.cssText = `position:absolute;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);width:1px;height:1px;white-space:nowrap;`; - // $accessible.setAttribute('tabindex', '-1'); - $accessible.innerHTML = this.html; - $el.insertBefore($accessible, $el.firstChild); - this.lines.forEach(setAriaHidden); - this.words.forEach(setAriaHidden); - this.chars.forEach(setAriaHidden); + return this.registerElement($el, parentNode); + } + + /** + * @return {this} + */ + record() { + const layout = this.layout; + const children = layout.children; + const root = layout.root; + const toParse = isArr(children) ? children : [children]; + const scoped = []; + const scopeRoot = children === '*' ? root : scope.root; + + // Mute transition and transforms of root ancestors before recording the state + + /** @type {Array} */ + const rootAncestorTransformStore = []; + let $ancestor = root.parentElement; + while ($ancestor && $ancestor.nodeType === 1) { + const computedStyle = getComputedStyle($ancestor); + if (computedStyle.transform && computedStyle.transform !== 'none') { + const inlineTransform = $ancestor.style.transform || ''; + const inlineTransition = muteElementTransition($ancestor); + rootAncestorTransformStore.push($ancestor, inlineTransform, inlineTransition); + $ancestor.style.transform = 'none'; + } + $ancestor = $ancestor.parentElement; } - this.width = /** @type {HTMLElement} */($el).offsetWidth; - if (canSplitLines || clearCache) { - this.effects.forEach((effect, i) => this.effectsCleanups[i] = effect(this)); + + for (let i = 0, l = toParse.length; i < l; i++) { + const child = toParse[i]; + scoped[i] = isStr(child) ? scopeRoot.querySelectorAll(child) : child; } - return this; - } - refresh() { - this.split(true); - } -} + const parsedChildren = registerTargets(scoped); -/** - * @param {HTMLElement|NodeList|String|Array} target - * @param {TextSplitterParams} [parameters] - * @return {TextSplitter} - */ -const splitText = (target, parameters) => new TextSplitter(target, parameters); + this.nodes.clear(); + this.rootNodes.clear(); -/** - * @deprecated text.split() is deprecated, import splitText() directly, or text.splitText() - * - * @param {HTMLElement|NodeList|String|Array} target - * @param {TextSplitterParams} [parameters] - * @return {TextSplitter} - */ -const split = (target, parameters) => { - console.warn('text.split() is deprecated, import splitText() directly, or text.splitText()'); - return new TextSplitter(target, parameters); -}; + const rootNode = this.registerElement(root, null); + // Root node are always targets + rootNode.isTarget = true; + this.rootNode = rootNode; -var index = /*#__PURE__*/Object.freeze({ - __proto__: null, - TextSplitter: TextSplitter, - split: split, - splitText: splitText -}); + const inRootNodeIds = new Set(); + // Update index and total for inital timing calculation + let index = 0; + const allNodeTargets = []; + this.nodes.forEach((node) => { allNodeTargets.push(node.$el); }); + this.nodes.forEach((node, id) => { + node.index = index++; + node.targets = allNodeTargets; + // Track ids of nodes that belong to the current root to filter detached matches + if (node && node.measuredIsInsideRoot) { + inRootNodeIds.add(id); + } + }); + // Elements with a layout id outside the root that match the children selector + const detachedElementsLookup = new Set(); + const orderedDetachedElements = []; + + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + if (!$el || $el.nodeType !== 1 || $el === root) continue; + const insideRoot = isElementInRoot(root, $el); + if (!insideRoot) { + const layoutNodeId = $el.dataset.layoutId; + if (!layoutNodeId || !inRootNodeIds.has(layoutNodeId)) continue; + } + if (!detachedElementsLookup.has($el)) { + detachedElementsLookup.add($el); + orderedDetachedElements.push($el); + } + } + for (let i = 0, l = orderedDetachedElements.length; i < l; i++) { + this.ensureDetachedNode(orderedDetachedElements[i], detachedElementsLookup); + } + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + const node = this.getNode($el); + if (node) { + let cur = node; + while (cur) { + if (cur.isTarget) break; + cur.isTarget = true; + cur = cur.parentNode; + } + } + } + this.scrollX = window.scrollX; + this.scrollY = window.scrollY; + this.forEachNode(restoreNodeTransform); + // Restore transition and transforms of root ancestors -/** - * Converts an easing function into a valid CSS linear() timing function string - * @param {EasingFunction} fn - * @param {number} [samples=100] - * @returns {string} CSS linear() timing function - */ -const easingToLinear = (fn, samples = 100) => { - const points = []; - for (let i = 0; i <= samples; i++) points.push(round$1(fn(i / samples), 4)); - return `linear(${points.join(', ')})`; -}; + for (let i = 0, l = rootAncestorTransformStore.length; i < l; i += 3) { + const $el = /** @type {DOMTarget} */(rootAncestorTransformStore[i]); + const inlineTransform = /** @type {String} */(rootAncestorTransformStore[i + 1]); + const inlineTransition = /** @type {String|null} */(rootAncestorTransformStore[i + 2]); + if (inlineTransform && inlineTransform !== '') { + $el.style.transform = inlineTransform; + } else { + $el.style.removeProperty('transform'); + } + restoreElementTransition($el, inlineTransition); + } -const WAAPIEasesLookups = {}; + return this; + } +} /** - * @param {EasingParam} ease - * @return {String} + * @param {LayoutStateParams} params + * @return {[LayoutStateAnimationProperties, LayoutAnimationTimingsParams]} */ -const parseWAAPIEasing = (ease) => { - let parsedEase = WAAPIEasesLookups[ease]; - if (parsedEase) return parsedEase; - parsedEase = 'linear'; - if (isStr(ease)) { - if ( - stringStartsWith(ease, 'linear') || - stringStartsWith(ease, 'cubic-') || - stringStartsWith(ease, 'steps') || - stringStartsWith(ease, 'ease') - ) { - parsedEase = ease; - } else if (stringStartsWith(ease, 'cubicB')) { - parsedEase = toLowerCase(ease); +function splitPropertiesFromParams(params) { + /** @type {LayoutStateAnimationProperties} */ + const properties = {}; + /** @type {LayoutAnimationTimingsParams} */ + const parameters = {}; + for (let name in params) { + const value = params[name]; + const isEase = name === 'ease'; + const isTiming = name === 'duration' || name === 'delay'; + if (isTiming || isEase) { + if (isEase) { + parameters[name] = /** @type {EasingParam} */(value); + } else { + parameters[name] = /** @type {Number|FunctionValue} */(value); + } } else { - const parsed = parseEaseString(ease); - if (isFnc(parsed)) parsedEase = parsed === none ? 'linear' : easingToLinear(parsed); + properties[name] = /** @type {Number|String} */(value); } - // Only cache string based easing name, otherwise function arguments get lost - WAAPIEasesLookups[ease] = parsedEase; - } else if (isFnc(ease)) { - const easing = easingToLinear(ease); - if (easing) parsedEase = easing; - } else if (/** @type {Spring} */(ease).ease) { - parsedEase = easingToLinear(/** @type {Spring} */(ease).ease); } - return parsedEase; -}; + return [properties, parameters]; +} -const transformsShorthands = ['x', 'y', 'z']; -const commonDefaultPXProperties = [ - 'perspective', - 'width', - 'height', - 'margin', - 'padding', - 'top', - 'right', - 'bottom', - 'left', - 'borderWidth', - 'fontSize', - 'borderRadius', - ...transformsShorthands -]; +class AutoLayout { + /** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + */ + constructor(root, params = {}) { + if (scope.current) scope.current.register(this); + const swapAtSplitParams = splitPropertiesFromParams(params.swapAt); + const enterFromSplitParams = splitPropertiesFromParams(params.enterFrom); + const leaveToSplitParams = splitPropertiesFromParams(params.leaveTo); + const transitionProperties = params.properties; + /** @type {Number|FunctionValue} */ + params.duration = setValue(params.duration, 350); + /** @type {Number|FunctionValue} */ + params.delay = setValue(params.delay, 0); + /** @type {EasingParam|FunctionValue} */ + params.ease = setValue(params.ease, 'inOut(3.5)'); + /** @type {AutoLayoutParams} */ + this.params = params; + /** @type {DOMTarget} */ + this.root = /** @type {DOMTarget} */(registerTargets(root)[0]); + /** @type {Number|String} */ + this.id = params.id || layoutId++; + /** @type {LayoutChildrenParam} */ + this.children = params.children || '*'; + /** @type {Boolean} */ + this.absoluteCoords = false; + /** @type {LayoutStateParams} */ + this.swapAtParams = mergeObjects(params.swapAt || { opacity: 0 }, { ease: 'inOut(1.75)' }); + /** @type {LayoutStateParams} */ + this.enterFromParams = params.enterFrom || { opacity: 0 }; + /** @type {LayoutStateParams} */ + this.leaveToParams = params.leaveTo || { opacity: 0 }; + /** @type {Set} */ + this.properties = new Set([ + 'opacity', + 'fontSize', + 'color', + 'backgroundColor', + 'borderRadius', + 'border', + 'filter', + 'clipPath', + ]); + if (swapAtSplitParams[0]) for (let name in swapAtSplitParams[0]) this.properties.add(name); + if (enterFromSplitParams[0]) for (let name in enterFromSplitParams[0]) this.properties.add(name); + if (leaveToSplitParams[0]) for (let name in leaveToSplitParams[0]) this.properties.add(name); + if (transitionProperties) for (let i = 0, l = transitionProperties.length; i < l; i++) this.properties.add(transitionProperties[i]); + /** @type {Set} */ + this.recordedProperties = new Set([ + 'display', + 'visibility', + 'translate', + 'position', + 'left', + 'top', + 'marginLeft', + 'marginTop', + 'width', + 'height', + 'maxWidth', + 'maxHeight', + 'minWidth', + 'minHeight', + ]); + this.properties.forEach(prop => this.recordedProperties.add(prop)); + /** @type {WeakSet} */ + this.pendingRemoval = new WeakSet(); + /** @type {Map} */ + this.transitionMuteStore = new Map(); + /** @type {LayoutSnapshot} */ + this.oldState = new LayoutSnapshot(this); + /** @type {LayoutSnapshot} */ + this.newState = new LayoutSnapshot(this); + /** @type {Timeline} */ + this.timeline = null; + /** @type {WAAPIAnimation} */ + this.transformAnimation = null; + /** @type {Array} */ + this.animating = []; + /** @type {Array} */ + this.swapping = []; + /** @type {Array} */ + this.leaving = []; + /** @type {Array} */ + this.entering = []; + // Record the current state as the old state to init the data attributes and allow imediate .animate() + this.oldState.record(); + // And all layout transition muted during the record + restoreLayoutTransition(this.transitionMuteStore); + } -const validIndividualTransforms = /*#__PURE__*/ (() => [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))])(); + /** + * @return {this} + */ + revert() { + this.root.classList.remove('is-animated'); + if (this.timeline) { + this.timeline.complete(); + this.timeline = null; + } + if (this.transformAnimation) { + this.transformAnimation.complete(); + this.transformAnimation = null; + } + this.animating.length = this.swapping.length = this.leaving.length = this.entering.length = 0; + this.oldState.revert(); + this.newState.revert(); + requestAnimationFrame(() => restoreLayoutTransition(this.transitionMuteStore)); + return this; + } -let transformsPropertiesRegistered = null; + /** + * @return {this} + */ + record() { + // Commit transforms before measuring + if (this.transformAnimation) { + this.transformAnimation.cancel(); + this.transformAnimation = null; + } + // Record the old state + this.oldState.record(); + // Cancel any running timeline + if (this.timeline) { + this.timeline.cancel(); + this.timeline = null; + } + // Restore previously captured inline styles + this.newState.forEachRootNode(restoreNodeInlineStyles); + return this; + } -/** - * @param {String} propName - * @param {WAAPIKeyframeValue} value - * @param {DOMTarget} $el - * @param {Number} i - * @param {Number} targetsLength - * @return {String} - */ -const normalizeTweenValue = (propName, value, $el, i, targetsLength) => { - // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables - let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, targetsLength); - if (!isNum(v)) return v; - if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`; - if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`; - return `${v}`; -}; + /** + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + animate(params = {}) { + /** @type { LayoutAnimationTimingsParams } */ + const animationTimings = { + ease: setValue(params.ease, this.params.ease), + delay: setValue(params.delay, this.params.delay), + duration: setValue(params.duration, this.params.duration), + }; + /** @type {TimelineParams} */ + const tlParams = { + id: this.id + }; + const onComplete = setValue(params.onComplete, this.params.onComplete); + const onPause = setValue(params.onPause, this.params.onPause); + for (let name in defaults) { + if (name !== 'ease' && name !== 'duration' && name !== 'delay') { + if (!isUnd(params[name])) { + tlParams[name] = params[name]; + } else if (!isUnd(this.params[name])) { + tlParams[name] = this.params[name]; + } + } + } + tlParams.onComplete = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const ed = globals.editor; + const isScrollControled = (ap && ap.linked) || (ed && ed.showPanel); + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + return; + } + // Make sure to call .cancel() after restoreNodeInlineStyles(node); otehrwise the commited styles get reverted + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(node => { + restoreNodeVisualState(node); + restoreNodeInlineStyles(node); + }); + for (let i = 0, l = transformed.length; i < l; i++) { + const $el = transformed[i]; + $el.style.transform = newState.getComputedValue($el, 'transform'); + } + if (this.root.classList.contains('is-animated')) { + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + } + // Avoid CSS transitions at the end of the animation by restoring them on the next frame + requestAnimationFrame(() => { + if (this.root.classList.contains('is-animated')) return; + restoreLayoutTransition(this.transitionMuteStore); + }); + }; + tlParams.onPause = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const isScrollControled = ap && ap.linked; + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + return; + } + if (!this.root.classList.contains('is-animated')) return; + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(restoreNodeVisualState); + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + }; + tlParams.composition = false; + + const swapAtParams = mergeObjects(mergeObjects(params.swapAt || {}, this.swapAtParams), animationTimings); + const enterFromParams = mergeObjects(mergeObjects(params.enterFrom || {}, this.enterFromParams), animationTimings); + const leaveToParams = mergeObjects(mergeObjects(params.leaveTo || {}, this.leaveToParams), animationTimings); + const [ swapAtProps, swapAtTimings ] = splitPropertiesFromParams(swapAtParams); + const [ enterFromProps, enterFromTimings ] = splitPropertiesFromParams(enterFromParams); + const [ leaveToProps, leaveToTimings ] = splitPropertiesFromParams(leaveToParams); + + const oldState = this.oldState; + const newState = this.newState; + const animating = this.animating; + const swapping = this.swapping; + const entering = this.entering; + const leaving = this.leaving; + const pendingRemoval = this.pendingRemoval; + + animating.length = swapping.length = entering.length = leaving.length = 0; + + // Mute old state CSS transitions to prevent wrong properties calculation + oldState.forEachRootNode(muteNodeTransition); + // Capture the new state before animation + newState.record(); + newState.forEachRootNode(recordNodeInlineStyles); + + const targets = []; + const animated = []; + const transformed = []; + const animatedSwap = []; + const rootNode = newState.rootNode; + const $root = rootNode.$el; + + newState.forEachRootNode(node => { + + const $el = node.$el; + const id = node.id; + const parent = node.parentNode; + const parentAdded = parent ? parent.branchAdded : false; + const parentRemoved = parent ? parent.branchRemoved : false; + const parentNotRendered = parent ? parent.branchNotRendered : false; + + let oldStateNode = oldState.nodes.get(id); + + const hasNoOldState = !oldStateNode; + + if (hasNoOldState) { + oldStateNode = cloneNodeProperties(node, /** @type {LayoutNode} */({}), oldState); + oldState.nodes.set(id, oldStateNode); + oldStateNode.measuredIsRemoved = true; + } else if (oldStateNode.measuredIsRemoved && !node.measuredIsRemoved) { + cloneNodeProperties(node, oldStateNode, oldState); + oldStateNode.measuredIsRemoved = true; + } -/** - * @param {DOMTarget} $el - * @param {String} propName - * @param {WAAPIKeyframeValue} from - * @param {WAAPIKeyframeValue} to - * @param {Number} i - * @param {Number} targetsLength - * @return {WAAPITweenValue} - */ -const parseIndividualTweenValue = ($el, propName, from, to, i, targetsLength) => { - /** @type {WAAPITweenValue} */ - let tweenValue = '0'; - const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, targetsLength) : getComputedStyle($el)[propName]; - if (!isUnd(from)) { - const computedFrom = normalizeTweenValue(propName, from, $el, i, targetsLength); - tweenValue = [computedFrom, computedTo]; - } else { - tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, targetsLength)) : computedTo; - } - return tweenValue; -}; + const oldParentNode = oldStateNode.parentNode; + const oldParentId = oldParentNode ? oldParentNode.id : null; + const newParentId = parent ? parent.id : null; + const parentChanged = oldParentId !== newParentId; + const elementChanged = oldStateNode.$el !== node.$el; + const wasRemovedBefore = oldStateNode.measuredIsRemoved; + const isRemovedNow = node.measuredIsRemoved; + + // Recalculate postion relative to their parent for elements that have been moved + if (!oldStateNode.measuredIsRemoved && !isRemovedNow && !hasNoOldState && (parentChanged || elementChanged)) { + const oldAbsoluteLeft = oldStateNode.properties.left; + const oldAbsoluteTop = oldStateNode.properties.top; + const newParent = parent || newState.rootNode; + const oldParent = newParent.id ? oldState.nodes.get(newParent.id) : null; + const parentLeft = oldParent ? oldParent.properties.left : newParent.properties.left; + const parentTop = oldParent ? oldParent.properties.top : newParent.properties.top; + const borderLeft = oldParent ? oldParent.properties.clientLeft : newParent.properties.clientLeft; + const borderTop = oldParent ? oldParent.properties.clientTop : newParent.properties.clientTop; + oldStateNode.properties.x = oldAbsoluteLeft - parentLeft - borderLeft; + oldStateNode.properties.y = oldAbsoluteTop - parentTop - borderTop; + } -class WAAPIAnimation { -/** - * @param {DOMTargetsParam} targets - * @param {WAAPIAnimationParams} params - */ - constructor(targets, params) { + if (node.hasVisibilitySwap) { + if (node.hasVisibilityHidden) { + node.$el.style.visibility = 'visible'; + node.$measure.style.visibility = 'hidden'; + } + if (node.hasDisplayNone) { + node.$el.style.display = oldStateNode.measuredDisplay || node.measuredDisplay || ''; + // Setting visibility 'hidden' instead of display none to avoid calculation issues + node.$measure.style.visibility = 'hidden'; + // @TODO: check why setting display here can cause calculation issues + // node.$measure.style.display = 'none'; + } + } - if (scope.current) scope.current.register(this); + const wasPendingRemoval = pendingRemoval.has($el); + const wasVisibleBefore = oldStateNode.measuredIsVisible; + const isVisibleNow = node.measuredIsVisible; + const becomeVisible = !wasVisibleBefore && isVisibleNow && !parentNotRendered; + const topLevelAdded = !isRemovedNow && (wasRemovedBefore || wasPendingRemoval) && !parentAdded; + const newlyRemoved = isRemovedNow && !wasRemovedBefore && !parentRemoved; + const topLevelRemoved = newlyRemoved || isRemovedNow && wasPendingRemoval && !parentRemoved; + + node.branchAdded = parentAdded || topLevelAdded; + node.branchRemoved = parentRemoved || topLevelRemoved; + node.branchNotRendered = parentNotRendered || isRemovedNow; + + if (isRemovedNow && wasVisibleBefore) { + node.$el.style.display = oldStateNode.measuredDisplay; + node.$el.style.visibility = 'visible'; + cloneNodeProperties(oldStateNode, node, newState); + } - // Skip the registration and fallback to no animation in case CSS.registerProperty is not supported - if (isNil(transformsPropertiesRegistered)) { - if (isBrowser && (isUnd(CSS) || !Object.hasOwnProperty.call(CSS, 'registerProperty'))) { - transformsPropertiesRegistered = false; + // Node is leaving + if (newlyRemoved) { + if (node.isTarget) { + leaving.push($el); + node.isLeaving = true; + } + pendingRemoval.add($el); + } else if (!isRemovedNow && wasPendingRemoval) { + pendingRemoval.delete($el); + } + + // Node is entering + if ((topLevelAdded && !parentNotRendered) || becomeVisible) { + updateNodeProperties(oldStateNode, enterFromProps); + if (node.isTarget) { + entering.push($el); + node.isEntering = true; + } + // Node is leaving + } else if (topLevelRemoved && !parentNotRendered) { + updateNodeProperties(node, leaveToProps); + } + + // Node is animating + // The animating array is used only to calculate delays and duration on root children + if (node !== rootNode && node.isTarget && !node.isEntering && !node.isLeaving) { + animating.push($el); + } + + targets.push($el); + + }); + + let enteringIndex = 0; + let leavingIndex = 0; + let animatingIndex = 0; + + newState.forEachRootNode(node => { + + const $el = node.$el; + const parent = node.parentNode; + const oldStateNode = oldState.nodes.get(node.id); + const nodeProperties = node.properties; + const oldStateNodeProperties = oldStateNode.properties; + + // Use closest animated parent index and total values so that children staggered delays are in sync with their parent + let animatedParent = parent !== rootNode && parent; + while (animatedParent && !animatedParent.isTarget && animatedParent !== rootNode) { + animatedParent = animatedParent.parentNode; + } + + // Root is always animated first in sync with the first child (animating.length is the total of children) + if (node === rootNode) { + node.index = 0; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); + } else if (node.isEntering) { + node.index = animatedParent ? animatedParent.index : enteringIndex; + node.targets = animatedParent ? animating : entering; + updateNodeTimingParams(node, enterFromTimings); + enteringIndex++; + } else if (node.isLeaving) { + node.index = animatedParent ? animatedParent.index : leavingIndex; + node.targets = animatedParent ? animating : leaving; + leavingIndex++; + updateNodeTimingParams(node, leaveToTimings); + } else if (node.isTarget) { + node.index = animatingIndex++; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); } else { - validTransforms.forEach(t => { - const isSkew = stringStartsWith(t, 'skew'); - const isScale = stringStartsWith(t, 'scale'); - const isRotate = stringStartsWith(t, 'rotate'); - const isTranslate = stringStartsWith(t, 'translate'); - const isAngle = isRotate || isSkew; - const syntax = isAngle ? '' : isScale ? "" : isTranslate ? "" : "*"; - try { - CSS.registerProperty({ - name: '--' + t, - syntax, - inherits: false, - initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0', - }); - } catch {} }); - transformsPropertiesRegistered = true; + node.index = animatedParent ? animatedParent.index : 0; + node.targets = animating; + updateNodeTimingParams(node, swapAtTimings); } - } - const parsedTargets = registerTargets(targets); - const targetsLength = parsedTargets.length; + // Make sure the old state node has its inex and total values up to date for valid "from" function values calculation + oldStateNode.index = node.index; + oldStateNode.targets = node.targets; - if (!targetsLength) { - console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`); + // Computes all values up front so we can check for changes and we don't have to re-compute them inside the animation props + for (let prop in nodeProperties) { + nodeProperties[prop] = getFunctionValue(nodeProperties[prop], $el, node.index, node.targets, null, null); + oldStateNodeProperties[prop] = getFunctionValue(oldStateNodeProperties[prop], $el, oldStateNode.index, oldStateNode.targets, null, null); + } + + // Use a 1px tolerance to detect dimensions changes to prevent width / height animations on barelly visible elements + const sizeTolerance = 1; + const widthChanged = Math.abs(nodeProperties.width - oldStateNodeProperties.width) > sizeTolerance; + const heightChanged = Math.abs(nodeProperties.height - oldStateNodeProperties.height) > sizeTolerance; + + node.sizeChanged = (widthChanged || heightChanged); + + // const hiddenStateChanged = (topLevelAdded || newlyRemoved) && wasRemovedBefore !== isRemovedNow; + + if (node.isTarget && (!node.measuredIsRemoved && oldStateNode.measuredIsVisible || node.measuredIsRemoved && node.measuredIsVisible)) { + if (nodeProperties.transform !== 'none' || oldStateNodeProperties.transform !== 'none') { + node.hasTransform = true; + transformed.push($el); + } + for (let prop in nodeProperties) { + // if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop] || hiddenStateChanged)) { + if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop])) { + animated.push($el); + break; + } + } + } + + if (!node.isTarget) { + swapping.push($el); + if (node.sizeChanged && parent && parent.isTarget && parent.sizeChanged) { + if (swapAtProps.transform) { + node.hasTransform = true; + transformed.push($el); + } + animatedSwap.push($el); + } + } + + }); + + const timingParams = { + delay: (/** @type {HTMLElement} */$el) => newState.getNode($el).delay, + duration: (/** @type {HTMLElement} */$el) => newState.getNode($el).duration, + ease: (/** @type {HTMLElement} */$el) => newState.getNode($el).ease, + }; + + tlParams.defaults = timingParams; + + this.timeline = createTimeline(tlParams); + + // Imediatly return the timeline if no layout changes detected + if (!animated.length && !transformed.length && !swapping.length) { + // Make sure to restore all CSS transition if no animation + restoreLayoutTransition(this.transitionMuteStore); + return this.timeline.complete(); } - const ease = setValue(params.ease, parseWAAPIEasing(globals.defaults.ease)); - const spring = /** @type {Spring} */(ease).ease && ease; - const autoplay = setValue(params.autoplay, globals.defaults.autoplay); - const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false; - const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true; - const reversed = params.reversed && /** @type {Boolean} */(params.reversed) === true; - const loop = setValue(params.loop, globals.defaults.loop); - const iterations = /** @type {Number} */((loop === true || loop === Infinity) ? Infinity : isNum(loop) ? loop + 1 : 1); - /** @type {PlaybackDirection} */ - const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal'; - /** @type {FillMode} */ - const fill = 'both'; // We use 'both' here because the animation can be reversed during playback - /** @type {String} */ - const easing = parseWAAPIEasing(ease); - const timeScale = (globals.timeScale === 1 ? 1 : K); + if (targets.length) { - /** @type {DOMTargetsArray}] */ - this.targets = parsedTargets; - /** @type {Array}] */ - this.animations = []; - /** @type {globalThis.Animation}] */ - this.controlAnimation = null; - /** @type {Callback} */ - this.onComplete = params.onComplete || /** @type {Callback} */(/** @type {unknown} */(globals.defaults.onComplete)); - /** @type {Number} */ - this.duration = 0; - /** @type {Boolean} */ - this.muteCallbacks = false; - /** @type {Boolean} */ - this.completed = false; - /** @type {Boolean} */ - this.paused = !autoplay || scroll !== false; - /** @type {Boolean} */ - this.reversed = reversed; - /** @type {Boolean} */ - this.persist = setValue(params.persist, globals.defaults.persist); - /** @type {Boolean|ScrollObserver} */ - this.autoplay = autoplay; - /** @type {Number} */ - this._speed = setValue(params.playbackRate, globals.defaults.playbackRate); - /** @type {Function} */ - this._resolve = noop; // Used by .then() - /** @type {Number} */ - this._completed = 0; - /** @type {Array.} */ - this._inlineStyles = []; + this.root.classList.add('is-animated'); - parsedTargets.forEach(($el, i) => { + for (let i = 0, l = targets.length; i < l; i++) { + const $el = targets[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; - const cachedTransforms = $el[transformsSymbol]; - const hasIndividualTransforms = validIndividualTransforms.some(t => params.hasOwnProperty(t)); - const elStyle = $el.style; - const inlineStyles = this._inlineStyles[i] = {}; + // muteNodeTransition(newNode); - /** @type {Number} */ - const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, targetsLength)) * timeScale; - /** @type {Number} */ - const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, targetsLength) * timeScale; - /** @type {CompositeOperation} */ - const composite = /** @type {CompositeOperation} */(setValue(params.composition, 'replace')); + // Don't animate positions of inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + // Display grid can mess with the absolute positioning, so set it to block during transition + if (oldNode.measuredDisplay === 'grid' || newNode.measuredDisplay === 'grid') $el.style.setProperty('display', 'block', 'important'); + // All children must be in position absolute or fixed + if ($el !== $root || this.absoluteCoords) { + $el.style.position = this.absoluteCoords ? 'fixed' : 'absolute'; + $el.style.left = '0px'; + $el.style.top = '0px'; + $el.style.marginLeft = '0px'; + $el.style.marginTop = '0px'; + $el.style.translate = `${oldNodeState.x}px ${oldNodeState.y}px`; + } + if ($el === $root && newNode.measuredPosition === 'static') { + $el.style.position = 'relative'; + // Cancel left / trop in case the static element had muted values now activated by potision relative + $el.style.left = '0px'; + $el.style.top = '0px'; + } + } + // Animate dimensions for all elements (including inlined) + $el.style.width = `${oldNodeState.width}px`; + $el.style.height = `${oldNodeState.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + } - for (let name in params) { - if (!isKey(name)) continue; - /** @type {PropertyIndexedKeyframes} */ - const keyframes = {}; - /** @type {KeyframeAnimationOptions} */ - const tweenParams = { iterations, direction, fill, easing, duration, delay, composite }; - const propertyValue = params[name]; - const individualTransformProperty = hasIndividualTransforms ? validTransforms.includes(name) ? name : shortTransforms.get(name) : false; + // Restore the scroll position if the oldState differs from the current state + if (oldState.scrollX !== window.scrollX || oldState.scrollY !== window.scrollY) { + // Restoring in the next frame avoids race conditions if for example a waapi animation commit styles that affect the root height + requestAnimationFrame(() => window.scrollTo(oldState.scrollX, oldState.scrollY)); + } - const styleName = individualTransformProperty ? 'transform' : name; - if (!inlineStyles[styleName]) { - inlineStyles[styleName] = elStyle[styleName]; + for (let i = 0, l = animated.length; i < l; i++) { + const $el = animated[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; + const newNodeState = newNode.properties; + let nodeHasChanged = false; + /** @type {AnimationParams} */ + const animatedProps = { + composition: 'none', + }; + if (oldNodeState.width !== newNodeState.width) { + animatedProps.width = [oldNodeState.width, newNodeState.width]; + nodeHasChanged = true; } + if (oldNodeState.height !== newNodeState.height) { + animatedProps.height = [oldNodeState.height, newNodeState.height]; + nodeHasChanged = true; + } + // If the node has transforms we handle the translate animation in waapi otherwise translate and other transforms can be out of sync + // And we don't animate the position of inlined elements + if (!newNode.hasTransform && !newNode.isInlined) { + animatedProps.translate = [`${oldNodeState.x}px ${oldNodeState.y}px`, `${newNodeState.x}px ${newNodeState.y}px`]; + nodeHasChanged = true; + } + this.properties.forEach(prop => { + const oldVal = oldNodeState[prop]; + const newVal = newNodeState[prop]; + if (prop !== 'transform' && oldVal !== newVal) { + animatedProps[prop] = [oldVal, newVal]; + nodeHasChanged = true; + } + }); + if (nodeHasChanged) { + this.timeline.add($el, animatedProps, 0); + } + } - let parsedPropertyValue; - if (isObj(propertyValue)) { - const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue); - const tweenOptionsEase = setValue(tweenOptions.ease, ease); - const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase; - const to = /** @type {WAAPITweenOptions} */(tweenOptions).to; - const from = /** @type {WAAPITweenOptions} */(tweenOptions).from; - /** @type {Number} */ - tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, targetsLength)) * timeScale; - /** @type {Number} */ - tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, targetsLength) * timeScale; - /** @type {CompositeOperation} */ - tweenParams.composite = /** @type {CompositeOperation} */(setValue(tweenOptions.composition, composite)); - /** @type {String} */ - tweenParams.easing = parseWAAPIEasing(tweenOptionsEase); - parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, targetsLength); - if (individualTransformProperty) { - keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; - cachedTransforms[individualTransformProperty] = parsedPropertyValue; - } else { - keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, targetsLength); + } + + if (swapping.length) { + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const oldNode = oldState.getNode($el); + const oldNodeProps = oldNode.properties; + $el.style.width = `${oldNodeProps.width}px`; + $el.style.height = `${oldNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // We don't animate the position of inlined elements + if (!oldNode.isInlined) { + $el.style.translate = `${oldNodeProps.x}px ${oldNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${oldState.getComputedValue($el, prop)}`; } - addWAAPIAnimation(this, $el, name, keyframes, tweenParams); - if (!isUnd(from)) { - if (!individualTransformProperty) { - elStyle[name] = keyframes[name][0]; - } else { - const key = `--${individualTransformProperty}`; - elStyle.setProperty(key, keyframes[key][0]); + }); + } + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const newNode = newState.getNode($el); + const newNodeProps = newNode.properties; + this.timeline.call(() => { + $el.style.width = `${newNodeProps.width}px`; + $el.style.height = `${newNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // Don't set translate for inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + $el.style.translate = `${newNodeProps.x}px ${newNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${newState.getComputedValue($el, prop)}`; + } + }); + }, newNode.delay + newNode.duration / 2); + } + + if (animatedSwap.length) { + const ease = parseEase(newState.nodes.get(animatedSwap[0].dataset.layoutId).ease); + const inverseEased = t => 1 - ease(1 - t); + const animatedSwapParams = /** @type {AnimationParams} */({}); + if (swapAtProps) { + for (let prop in swapAtProps) { + if (prop !== 'transform') { + animatedSwapParams[prop] = [ + { from: (/** @type {HTMLElement} */$el) => oldState.getComputedValue($el, prop), to: swapAtProps[prop] }, + { from: swapAtProps[prop], to: (/** @type {HTMLElement} */$el) => newState.getComputedValue($el, prop), ease: inverseEased } + ]; + } + } + } + this.timeline.add(animatedSwap, animatedSwapParams, 0); + } + + } + + const transformedLength = transformed.length; + + if (transformedLength) { + // We only need to set the transform property here since translate is already defined in the targets loop + for (let i = 0; i < transformedLength; i++) { + const $el = transformed[i]; + const node = newState.getNode($el); + // Don't set translate for inlined elements (to avoid text reflow) + if (!node.isInlined) { + $el.style.translate = `${oldState.getComputedValue($el, 'x')}px ${oldState.getComputedValue($el, 'y')}px`; + } + $el.style.transform = oldState.getComputedValue($el, 'transform'); + if (animatedSwap.includes($el)) { + node.ease = getFunctionValue(swapAtParams.ease, $el, node.index, node.targets, null, null); + node.duration = getFunctionValue(swapAtParams.duration, $el, node.index, node.targets, null, null); + } + } + this.transformAnimation = waapi.animate(transformed, { + translate: (/** @type {HTMLElement} */$el) => { + const node = newState.getNode($el); + // Don't animate translate for inlined elements (to avoid text reflow) + if (node.isInlined) return '0px 0px'; + return `${newState.getComputedValue($el, 'x')}px ${newState.getComputedValue($el, 'y')}px`; + }, + transform: (/** @type {HTMLElement} */$el) => { + const newValue = newState.getComputedValue($el, 'transform'); + if (!animatedSwap.includes($el)) return newValue; + const oldValue = oldState.getComputedValue($el, 'transform'); + const node = newState.getNode($el); + return [oldValue, getFunctionValue(swapAtProps.transform, $el, node.index, node.targets, null, null), newValue] + }, + autoplay: false, + // persist: true, + ...timingParams, + }); + this.timeline.sync(this.transformAnimation, 0); + } + + return this.timeline.init(); + } + + /** + * @param {(layout: this) => void} callback + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + update(callback, params = {}) { + this.record(); + callback(this); + return this.animate(params); + } +} + +/** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + * @return {AutoLayout} + */ +const createLayout = (root, params) => new AutoLayout(root, params); + +// Chain-able utilities + +const numberUtils = numberImports; // Needed to keep the import when bundling + +const chainables = {}; + +/** + * @callback UtilityFunction + * @param {...*} args + * @return {Number|String} + * + * @param {UtilityFunction} fn + * @param {Number} [last=0] + * @return {function(...(Number|String)): function(Number|String): (Number|String)} + */ +const curry = (fn, last = 0) => (...args) => last ? v => fn(...args, v) : v => fn(v, ...args); + +/** + * @param {Function} fn + * @return {function(...(Number|String))} + */ +const chain = fn => { + return (...args) => { + const result = fn(...args); + return new Proxy(noop, { + apply: (_, __, [v]) => result(v), + get: (_, prop) => { + if (!chainables[prop]) return undefined; + return chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { + const nextResult = chainables[prop](...nextArgs); + return (/**@type {Number|String} */v) => nextResult(result(v)); + }) + } + }); + } +}; + +/** + * @param {UtilityFunction} fn + * @param {String} name + * @param {Number} [right] + * @return {function(...(Number|String)): UtilityFunction} + */ +const makeChainable = (name, fn, right = 0) => { + const chained = (...args) => (args.length < fn.length ? chain(curry(fn, right)) : fn)(...args); + if (!chainables[name]) chainables[name] = chained; + return chained; +}; + +/** + * @typedef {Object} ChainablesMap + * @property {ChainedClamp} clamp + * @property {ChainedRound} round + * @property {ChainedSnap} snap + * @property {ChainedWrap} wrap + * @property {ChainedLerp} lerp + * @property {ChainedDamp} damp + * @property {ChainedMapRange} mapRange + * @property {ChainedRoundPad} roundPad + * @property {ChainedPadStart} padStart + * @property {ChainedPadEnd} padEnd + * @property {ChainedDegToRad} degToRad + * @property {ChainedRadToDeg} radToDeg + */ + +/** + * @callback ChainedUtilsResult + * @param {Number} value - The value to process through the chained operations + * @return {Number} The processed result + */ + +/** + * @typedef {ChainablesMap & ChainedUtilsResult} ChainableUtil + */ + +// Chainable + +/** + * @callback ChainedRoundPad + * @param {Number} decimalLength - Number of decimal places + * @return {ChainableUtil} + */ +const roundPad = /** @type {typeof numberUtils.roundPad & ChainedRoundPad} */(makeChainable('roundPad', numberUtils.roundPad)); + +/** + * @callback ChainedPadStart + * @param {Number} totalLength - Target length + * @param {String} padString - String to pad with + * @return {ChainableUtil} + */ +const padStart = /** @type {typeof numberUtils.padStart & ChainedPadStart} */(makeChainable('padStart', numberUtils.padStart)); + +/** + * @callback ChainedPadEnd + * @param {Number} totalLength - Target length + * @param {String} padString - String to pad with + * @return {ChainableUtil} + */ +const padEnd = /** @type {typeof numberUtils.padEnd & ChainedPadEnd} */(makeChainable('padEnd', numberUtils.padEnd)); + +/** + * @callback ChainedWrap + * @param {Number} min - Minimum boundary + * @param {Number} max - Maximum boundary + * @return {ChainableUtil} + */ +const wrap = /** @type {typeof numberUtils.wrap & ChainedWrap} */(makeChainable('wrap', numberUtils.wrap)); + +/** + * @callback ChainedMapRange + * @param {Number} inLow - Input range minimum + * @param {Number} inHigh - Input range maximum + * @param {Number} outLow - Output range minimum + * @param {Number} outHigh - Output range maximum + * @return {ChainableUtil} + */ +const mapRange = /** @type {typeof numberUtils.mapRange & ChainedMapRange} */(makeChainable('mapRange', numberUtils.mapRange)); + +/** + * @callback ChainedDegToRad + * @return {ChainableUtil} + */ +const degToRad = /** @type {typeof numberUtils.degToRad & ChainedDegToRad} */(makeChainable('degToRad', numberUtils.degToRad)); + +/** + * @callback ChainedRadToDeg + * @return {ChainableUtil} + */ +const radToDeg = /** @type {typeof numberUtils.radToDeg & ChainedRadToDeg} */(makeChainable('radToDeg', numberUtils.radToDeg)); + +/** + * @callback ChainedSnap + * @param {Number|Array} increment - Step size or array of snap points + * @return {ChainableUtil} + */ +const snap = /** @type {typeof numberUtils.snap & ChainedSnap} */(makeChainable('snap', numberUtils.snap)); + +/** + * @callback ChainedClamp + * @param {Number} min - Minimum boundary + * @param {Number} max - Maximum boundary + * @return {ChainableUtil} + */ +const clamp = /** @type {typeof numberUtils.clamp & ChainedClamp} */(makeChainable('clamp', numberUtils.clamp)); + +/** + * @callback ChainedRound + * @param {Number} decimalLength - Number of decimal places + * @return {ChainableUtil} + */ +const round = /** @type {typeof numberUtils.round & ChainedRound} */(makeChainable('round', numberUtils.round)); + +/** + * @callback ChainedLerp + * @param {Number} start - Starting value + * @param {Number} end - Ending value + * @return {ChainableUtil} + */ +const lerp = /** @type {typeof numberUtils.lerp & ChainedLerp} */(makeChainable('lerp', numberUtils.lerp, 1)); + +/** + * @callback ChainedDamp + * @param {Number} start - Starting value + * @param {Number} end - Target value + * @param {Number} deltaTime - Delta time in ms + * @return {ChainableUtil} + */ +const damp = /** @type {typeof numberUtils.damp & ChainedDamp} */(makeChainable('damp', numberUtils.damp, 1)); + +/** + * Generate a random number between optional min and max (inclusive) and decimal precision + * + * @callback RandomNumberGenerator + * @param {Number} [min=0] - The minimum value (inclusive) + * @param {Number} [max=1] - The maximum value (inclusive) + * @param {Number} [decimalLength=0] - Number of decimal places to round to + * @return {Number} A random number between min and max + */ + +/** + * Generates a random number between min and max (inclusive) with optional decimal precision + * + * @type {RandomNumberGenerator} + */ +const random = (min = 0, max = 1, decimalLength = 0) => { + const m = 10 ** decimalLength; + return Math.floor((Math.random() * (max - min + (1 / m)) + min) * m) / m; +}; + +let _seed = 0; + +/** + * Creates a seeded pseudorandom number generator function + * + * @param {Number} [seed] - The seed value for the random number generator + * @param {Number} [seededMin=0] - The minimum default value (inclusive) of the returned function + * @param {Number} [seededMax=1] - The maximum default value (inclusive) of the returned function + * @param {Number} [seededDecimalLength=0] - Default number of decimal places to round to of the returned function + * @return {RandomNumberGenerator} A function to generate a random number between optional min and max (inclusive) and decimal precision + */ +const createSeededRandom = (seed, seededMin = 0, seededMax = 1, seededDecimalLength = 0) => { + let t = seed === undefined ? _seed++ : seed; + return (min = seededMin, max = seededMax, decimalLength = seededDecimalLength) => { + t += 0x6D2B79F5; + t = Math.imul(t ^ t >>> 15, t | 1); + t ^= t + Math.imul(t ^ t >>> 7, t | 61); + const m = 10 ** decimalLength; + return Math.floor(((((t ^ t >>> 14) >>> 0) / 4294967296) * (max - min + (1 / m)) + min) * m) / m; + } +}; + +/** + * Picks a random element from an array or a string + * + * @template T + * @param {String|Array} items - The array or string to pick from + * @return {String|T} A random element from the array or character from the string + */ +const randomPick = items => items[random(0, items.length - 1)]; + +/** + * Shuffles an array in-place using the Fisher-Yates algorithm + * Adapted from https://bost.ocks.org/mike/shuffle/ + * + * @param {Array} items - The array to shuffle (will be modified in-place) + * @return {Array} The same array reference, now shuffled + */ +const shuffle = items => { + let m = items.length, t, i; + while (m) { i = random(0, --m); t = items[m]; items[m] = items[i]; items[i] = t; } + return items; +}; + + + + + +/** + * @overload + * @param {Number} val + * @param {StaggerParams} [params] + * @return {StaggerFunction} + */ + +/** + * @overload + * @param {String} val + * @param {StaggerParams} [params] + * @return {StaggerFunction} + */ + +/** + * @overload + * @param {[Number, Number]} val + * @param {StaggerParams} [params] + * @return {StaggerFunction} + */ + +/** + * @overload + * @param {[String, String]} val + * @param {StaggerParams} [params] + * @return {StaggerFunction} + */ + +/** + * @param {Number|String|[Number, Number]|[String, String]} val The staggered value or range + * @param {StaggerParams} [params] The stagger parameters + * @return {StaggerFunction} + */ +const stagger = (val, params = {}) => { + let values = []; + let maxValue = 0; + let cachedOffset; + const from = params.from; + const reversed = params.reversed; + const ease = params.ease; + const hasEasing = !isUnd(ease); + const hasSpring = hasEasing && !isUnd(/** @type {Spring} */(ease).ease); + const staggerEase = hasSpring ? /** @type {Spring} */(ease).ease : hasEasing ? parseEase(ease) : null; + const grid = params.grid; + const autoGrid = grid === true; + const axis = params.axis; + const customTotal = params.total; + const fromFirst = isUnd(from) || from === 0 || from === 'first'; + const fromCenter = from === 'center'; + const fromLast = from === 'last'; + const fromRandom = from === 'random'; + const fromArr = isArr(from); + const isRange = isArr(val); + const useProp = params.use; + const val1 = isRange ? parseNumber(val[0]) : parseNumber(val); + const val2 = isRange ? parseNumber(val[1]) : 0; + const unitMatch = unitsExecRgx.exec((isRange ? val[1] : val) + emptyString); + const start = params.start || 0 + (isRange ? val1 : 0); + let fromIndex = fromFirst ? 0 : isNum(from) ? from : 0; + return (target, i, t, _, tl) => { + const [ registeredTarget ] = registerTargets(target); + const total = isUnd(customTotal) ? t.length : customTotal; + const customIndex = !isUnd(useProp) ? isFnc(useProp) ? useProp(registeredTarget, i, total) : getOriginalAnimatableValue(registeredTarget, useProp) : false; + const staggerIndex = isNum(customIndex) || isStr(customIndex) && isNum(+customIndex) ? +customIndex : i; + if (fromCenter) fromIndex = (total - 1) / 2; + if (fromLast) fromIndex = total - 1; + if (!values.length) { + if (autoGrid) { + let hasPositions = true; + let minPosX = Infinity; + let minPosY = Infinity; + let maxPosX = -Infinity; + let maxPosY = -Infinity; + const pxArr = []; + const pyArr = []; + for (let index = 0; index < total; index++) { + const el = t[index]; + let px = 0; + let py = 0; + let found = false; + if (el && isFnc(el.getBoundingClientRect)) { + const rect = el.getBoundingClientRect(); + px = rect.left + rect.width / 2; + py = rect.top + rect.height / 2; + found = true; + } else { + const obj = /** @type {JSTarget} */(el); + if (obj && isNum(obj.x) && isNum(obj.y)) { + px = obj.x; + py = obj.y; + found = true; + } + } + if (!found) { + hasPositions = false; + break; + } + pxArr.push(px); + pyArr.push(py); + if (px < minPosX) minPosX = px; + if (py < minPosY) minPosY = py; + if (px > maxPosX) maxPosX = px; + if (py > maxPosY) maxPosY = py; + } + if (hasPositions) { + let fX = pxArr[0]; + let fY = pyArr[0]; + if (fromArr) { + fX = minPosX + from[0] * (maxPosX - minPosX); + fY = minPosY + from[1] * (maxPosY - minPosY); + } else if (fromCenter) { + fX = (minPosX + maxPosX) / 2; + fY = (minPosY + maxPosY) / 2; + } else if (fromLast) { + fX = pxArr[total - 1]; + fY = pyArr[total - 1]; + } else if (isNum(from)) { + fX = pxArr[from]; + fY = pyArr[from]; + } + for (let index = 0; index < total; index++) { + const distanceX = fX - pxArr[index]; + const distanceY = fY - pyArr[index]; + let value = sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values.push(value); + } + let minDist = Infinity; + for (let index = 0, l = values.length; index < l; index++) { + const absVal = abs(values[index]); + if (absVal > 0 && absVal < minDist) minDist = absVal; + } + if (minDist > 0 && minDist < Infinity) { + for (let index = 0, l = values.length; index < l; index++) { + values[index] = values[index] / minDist; + } + } + } else { + for (let index = 0; index < total; index++) { + values.push(abs(fromIndex - index)); + } + } + } else { + for (let index = 0; index < total; index++) { + if (!grid) { + values.push(abs(fromIndex - index)); + } else { + let fromX, fromY; + if (fromArr) { + fromX = from[0] * (grid[0] - 1); + fromY = from[1] * (grid[1] - 1); + } else if (fromCenter) { + fromX = (grid[0] - 1) / 2; + fromY = (grid[1] - 1) / 2; + } else { + fromX = fromIndex % grid[0]; + fromY = floor(fromIndex / grid[0]); + } + const toX = index % grid[0]; + const toY = floor(index / grid[0]); + const distanceX = fromX - toX; + const distanceY = fromY - toY; + let value = sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values.push(value); + } + } + } + maxValue = max(...values); + if (staggerEase) values = values.map(val => staggerEase(val / maxValue) * maxValue); + if (reversed) values = values.map(val => axis ? (val < 0) ? val * -1 : -val : abs(maxValue - val)); + if (fromRandom) values = shuffle(values); + } + const spacing = isRange ? (val2 - val1) / maxValue : val1; + if (isUnd(cachedOffset)) { + cachedOffset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); + } + /** @type {String|Number} */ + let output = cachedOffset + ((spacing * round$1(values[staggerIndex], 2)) || 0); + if (params.modifier) output = params.modifier(/** @type {Number} */(output)); + if (unitMatch) output = `${output}${unitMatch[2]}`; + return output; + } +}; + +var index$2 = /*#__PURE__*/Object.freeze({ + __proto__: null, + $: registerTargets, + addChild: addChild, + clamp: clamp, + cleanInlineStyles: cleanInlineStyles, + createSeededRandom: createSeededRandom, + damp: damp, + degToRad: degToRad, + forEachChildren: forEachChildren, + get: get, + keepTime: keepTime, + lerp: lerp, + mapRange: mapRange, + padEnd: padEnd, + padStart: padStart, + radToDeg: radToDeg, + random: random, + randomPick: randomPick, + remove: remove, + removeChild: removeChild, + round: round, + roundPad: roundPad, + set: set, + shuffle: shuffle, + snap: snap, + stagger: stagger, + sync: sync, + wrap: wrap +}); + + + +/** + * @param {TargetsParam} path + * @return {SVGGeometryElement|void} + */ +const getPath = path => { + const parsedTargets = parseTargets(path); + const $parsedSvg = /** @type {SVGGeometryElement} */(parsedTargets[0]); + if (!$parsedSvg || !isSvg($parsedSvg)) return console.warn(`${path} is not a valid SVGGeometryElement`); + return $parsedSvg; +}; + + + +// Motion path animation + +/** + * @param {SVGGeometryElement} $path + * @param {Number} totalLength + * @param {Number} progress + * @param {Number} lookup + * @param {Boolean} shouldClamp + * @return {DOMPoint} + */ +const getPathPoint = ($path, totalLength, progress, lookup, shouldClamp) => { + const point = progress + lookup; + const pointOnPath = shouldClamp + ? Math.max(0, Math.min(point, totalLength)) // Clamp between 0 and totalLength + : (point % totalLength + totalLength) % totalLength; // Wrap around + return $path.getPointAtLength(pointOnPath); +}; + +/** + * @param {SVGGeometryElement} $path + * @param {String} pathProperty + * @param {Number} [offset=0] + * @return {FunctionValue} + */ +const getPathProgess = ($path, pathProperty, offset = 0) => { + return $el => { + const totalLength = +($path.getTotalLength()); + const inSvg = $el[isSvgSymbol]; + const ctm = $path.getCTM(); + const shouldClamp = offset === 0; + /** @type {TweenObjectValue} */ + return { + from: 0, + to: totalLength, + /** @type {TweenModifier} */ + modifier: progress => { + const offsetLength = offset * totalLength; + const newProgress = progress + offsetLength; + if (pathProperty === 'a') { + const p0 = getPathPoint($path, totalLength, newProgress, -1, shouldClamp); + const p1 = getPathPoint($path, totalLength, newProgress, 1, shouldClamp); + return atan2(p1.y - p0.y, p1.x - p0.x) * 180 / PI; + } else { + const p = getPathPoint($path, totalLength, newProgress, 0, shouldClamp); + return pathProperty === 'x' ? + inSvg || !ctm ? p.x : p.x * ctm.a + p.y * ctm.c + ctm.e : + inSvg || !ctm ? p.y : p.x * ctm.b + p.y * ctm.d + ctm.f + } + } + } + } +}; + +/** + * @param {TargetsParam} path + * @param {Number} [offset=0] + */ +const createMotionPath = (path, offset = 0) => { + const $path = getPath(path); + if (!$path) return; + return { + translateX: getPathProgess($path, 'x', offset), + translateY: getPathProgess($path, 'y', offset), + rotate: getPathProgess($path, 'a', offset), + } +}; + + + +/** + * @param {SVGGeometryElement} [$el] + * @return {Number} + */ +const getScaleFactor = $el => { + let scaleFactor = 1; + if ($el && $el.getCTM) { + const ctm = $el.getCTM(); + if (ctm) { + const scaleX = sqrt(ctm.a * ctm.a + ctm.b * ctm.b); + const scaleY = sqrt(ctm.c * ctm.c + ctm.d * ctm.d); + scaleFactor = (scaleX + scaleY) / 2; + } + } + return scaleFactor; +}; + +/** + * Creates a proxy that wraps an SVGGeometryElement and adds drawing functionality. + * @param {SVGGeometryElement} $el - The SVG element to transform into a drawable + * @param {number} start - Starting position (0-1) + * @param {number} end - Ending position (0-1) + * @return {DrawableSVGGeometry} - Returns a proxy that preserves the original element's type with additional 'draw' attribute functionality + */ +const createDrawableProxy = ($el, start, end) => { + const pathLength = K; + const computedStyles = getComputedStyle($el); + const strokeLineCap = computedStyles.strokeLinecap; + // @ts-ignore + const $scalled = computedStyles.vectorEffect === 'non-scaling-stroke' ? $el : null; + let currentCap = strokeLineCap; + + const proxy = new Proxy($el, { + get(target, property) { + const value = target[property]; + if (property === proxyTargetSymbol) return target; + if (property === 'setAttribute') { + return (...args) => { + if (args[0] === 'draw') { + const value = args[1]; + const values = value.split(' '); + const v1 = +values[0]; + const v2 = +values[1]; + // TOTO: Benchmark if performing two slices is more performant than one split + // const spaceIndex = value.indexOf(' '); + // const v1 = round(+value.slice(0, spaceIndex), precision); + // const v2 = round(+value.slice(spaceIndex + 1), precision); + const scaleFactor = getScaleFactor($scalled); + const os = v1 * -pathLength * scaleFactor; + const d1 = (v2 * pathLength * scaleFactor) + os; + const d2 = (pathLength * scaleFactor + + ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10 * scaleFactor) - d1); + if (strokeLineCap !== 'butt') { + const newCap = v1 === v2 ? 'butt' : strokeLineCap; + if (currentCap !== newCap) { + target.style.strokeLinecap = `${newCap}`; + currentCap = newCap; + } + } + target.setAttribute('stroke-dashoffset', `${os}`); + target.setAttribute('stroke-dasharray', `${d1} ${d2}`); + } + return Reflect.apply(value, target, args); + }; + } + + if (isFnc(value)) { + return (...args) => Reflect.apply(value, target, args); + } else { + return value; + } + } + }); + + if ($el.getAttribute('pathLength') !== `${pathLength}`) { + $el.setAttribute('pathLength', `${pathLength}`); + proxy.setAttribute('draw', `${start} ${end}`); + } + + return /** @type {DrawableSVGGeometry} */(proxy); +}; + +/** + * Creates drawable proxies for multiple SVG elements. + * @param {TargetsParam} selector - CSS selector, SVG element, or array of elements and selectors + * @param {number} [start=0] - Starting position (0-1) + * @param {number} [end=0] - Ending position (0-1) + * @return {Array} - Array of proxied elements with drawing functionality + */ +const createDrawable = (selector, start = 0, end = 0) => { + const els = parseTargets(selector); + return els.map($el => createDrawableProxy( + /** @type {SVGGeometryElement} */($el), + start, + end + )); +}; + + + +/** + * @param {TargetsParam} path2 + * @param {Number} [precision] + * @return {FunctionValue} + */ +const morphTo = (path2, precision = .33) => ($path1, index, total, prevTween) => { + const tagName1 = ($path1.tagName || '').toLowerCase(); + if (!tagName1.match(/^(path|polygon|polyline)$/)) { + throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use , or .`); + } + const $path2 = /** @type {SVGGeometryElement} */(getPath(path2)); + if (!$path2) { + throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing , or SVG element."); + } + const tagName2 = ($path2.tagName || '').toLowerCase(); + if (!tagName2.match(/^(path|polygon|polyline)$/)) { + throw new Error(`Can't morph a <${$path2.tagName}> SVG element. Use , or .`); + } + const isPath = $path1.tagName === 'path'; + const separator = isPath ? ' ' : ','; + const previousPoints = prevTween ? prevTween._value : null; + if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints); + + let v1 = '', v2 = ''; + + if (!precision) { + v1 = $path1.getAttribute(isPath ? 'd' : 'points'); + v2 = $path2.getAttribute(isPath ? 'd' : 'points'); + } else { + const length1 = /** @type {SVGGeometryElement} */($path1).getTotalLength(); + const length2 = $path2.getTotalLength(); + const maxPoints = Math.max(Math.ceil(length1 * precision), Math.ceil(length2 * precision)); + for (let i = 0; i < maxPoints; i++) { + const t = i / (maxPoints - 1); + const pointOnPath1 = /** @type {SVGGeometryElement} */($path1).getPointAtLength(length1 * t); + const pointOnPath2 = $path2.getPointAtLength(length2 * t); + const prefix = isPath ? (i === 0 ? 'M' : 'L') : ''; + v1 += prefix + round$1(pointOnPath1.x, 3) + separator + pointOnPath1.y + ' '; + v2 += prefix + round$1(pointOnPath2.x, 3) + separator + pointOnPath2.y + ' '; + } + } + + return [v1, v2]; +}; + +var index$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + createDrawable: createDrawable, + createMotionPath: createMotionPath, + morphTo: morphTo +}); + + + +const segmenter = (typeof Intl !== 'undefined') && Intl.Segmenter; +const valueRgx = /\{value\}/g; +const indexRgx = /\{i\}/g; +const whiteSpaceGroupRgx = /(\s+)/; +const whiteSpaceRgx = /^\s+$/; +const lineType = 'line'; +const wordType = 'word'; +const charType = 'char'; +const dataLine = `data-line`; + +/** + * @typedef {Object} Segment + * @property {String} segment + * @property {Boolean} [isWordLike] + */ + +/** + * @typedef {Object} Segmenter + * @property {function(String): Iterable} segment + */ + +/** @type {Segmenter} */ +let wordSegmenter = null; +/** @type {Segmenter} */ +let graphemeSegmenter = null; +let $splitTemplate = null; + +/** + * @param {Segment} seg + * @return {Boolean} + */ +const isSegmentWordLike = seg => { + return seg.isWordLike || + seg.segment === ' ' || // Consider spaces as words first, then handle them diffrently later + isNum(+seg.segment); // Safari doesn't considers numbers as words +}; + +/** + * @param {HTMLElement} $el + */ +const setAriaHidden = $el => $el.setAttribute('aria-hidden', 'true'); + +/** + * @param {DOMTarget} $el + * @param {String} type + * @return {Array} + */ +const getAllTopLevelElements = ($el, type) => [.../** @type {*} */($el.querySelectorAll(`[data-${type}]:not([data-${type}] [data-${type}])`))]; + +const debugColors = { line: '#00D672', word: '#FF4B4B', char: '#5A87FF' }; + +/** + * @param {HTMLElement} $el + */ +const filterEmptyElements = $el => { + if (!$el.childElementCount && !$el.textContent.trim()) { + const $parent = $el.parentElement; + $el.remove(); + if ($parent) filterEmptyElements($parent); + } +}; + +/** + * @param {HTMLElement} $el + * @param {Number} lineIndex + * @param {Set} bin + * @returns {Set} + */ +const filterLineElements = ($el, lineIndex, bin) => { + const dataLineAttr = $el.getAttribute(dataLine); + if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') { + bin.add($el); + // Also remove adjacent whitespace-only text nodes + const prev = $el.previousSibling; + const next = $el.nextSibling; + if (prev && prev.nodeType === 3 && whiteSpaceRgx.test(prev.textContent)) { + bin.add(prev); + } + if (next && next.nodeType === 3 && whiteSpaceRgx.test(next.textContent)) { + bin.add(next); + } + } + let i = $el.childElementCount; + while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin); + return bin; +}; + +/** + * @param {'line'|'word'|'char'} type + * @param {SplitTemplateParams} params + * @return {String} + */ +const generateTemplate = (type, params = {}) => { + let template = ``; + const classString = isStr(params.class) ? ` class="${params.class}"` : ''; + const cloneType = setValue(params.clone, false); + const wrapType = setValue(params.wrap, false); + const overflow = wrapType ? wrapType === true ? 'clip' : wrapType : cloneType ? 'clip' : false; + if (wrapType) template += ``; + template += ``; + if (cloneType) { + const left = cloneType === 'left' ? '-100%' : cloneType === 'right' ? '100%' : '0'; + const top = cloneType === 'top' ? '-100%' : cloneType === 'bottom' ? '100%' : '0'; + template += `{value}`; + template += `{value}`; + } else { + template += `{value}`; + } + template += ``; + if (wrapType) template += ``; + return template; +}; + +/** + * @param {String|SplitFunctionValue} htmlTemplate + * @param {Array} store + * @param {Node|HTMLElement} node + * @param {DocumentFragment} $parentFragment + * @param {'line'|'word'|'char'} type + * @param {Boolean} debug + * @param {Number} lineIndex + * @param {Number} [wordIndex] + * @param {Number} [charIndex] + * @return {HTMLElement} + */ +const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, debug, lineIndex, wordIndex, charIndex) => { + const isLine = type === lineType; + const isChar = type === charType; + const className = `_${type}_`; + const template = isFnc(htmlTemplate) ? htmlTemplate(node) : htmlTemplate; + const displayStyle = isLine ? 'block' : 'inline-block'; + $splitTemplate.innerHTML = template + .replace(valueRgx, ``) + .replace(indexRgx, `${isChar ? charIndex : isLine ? lineIndex : wordIndex}`); + const $content = $splitTemplate.content; + const $highestParent = /** @type {HTMLElement} */($content.firstElementChild); + const $split = /** @type {HTMLElement} */($content.querySelector(`[data-${type}]`)) || $highestParent; + const $replacables = /** @type {NodeListOf} */($content.querySelectorAll(`i.${className}`)); + const replacablesLength = $replacables.length; + if (replacablesLength) { + $highestParent.style.display = displayStyle; + $split.style.display = displayStyle; + $split.setAttribute(dataLine, `${lineIndex}`); + if (!isLine) { + $split.setAttribute('data-word', `${wordIndex}`); + if (isChar) $split.setAttribute('data-char', `${charIndex}`); + } + let i = replacablesLength; + while (i--) { + const $replace = $replacables[i]; + const $closestParent = $replace.parentElement; + $closestParent.style.display = displayStyle; + if (isLine) { + $closestParent.innerHTML = /** @type {HTMLElement} */(node).innerHTML; + } else { + $closestParent.replaceChild(node.cloneNode(true), $replace); + } + } + store.push($split); + $parentFragment.appendChild($content); + } else { + console.warn(`The expression "{value}" is missing from the provided template.`); + } + if (debug) $highestParent.style.outline = `1px dotted ${debugColors[type]}`; + return $highestParent; +}; + +/** + * A class that splits text into words and wraps them in span elements while preserving the original HTML structure. + * @class + */ +class TextSplitter { + /** + * @param {Element|NodeList|String|Array} target + * @param {TextSplitterParams} [parameters] + */ + constructor(target, parameters = {}) { + // Only init segmenters when needed + if (!wordSegmenter) wordSegmenter = segmenter ? new segmenter([], { granularity: wordType }) : { + segment: (text) => { + const segments = []; + const words = text.split(whiteSpaceGroupRgx); + for (let i = 0, l = words.length; i < l; i++) { + const segment = words[i]; + segments.push({ + segment, + isWordLike: !whiteSpaceRgx.test(segment), // Consider non-whitespace as word-like + }); + } + return segments; + } + }; + if (!graphemeSegmenter) graphemeSegmenter = segmenter ? new segmenter([], { granularity: 'grapheme' }) : { + segment: text => [...text].map(char => ({ segment: char })) + }; + if (!$splitTemplate && isBrowser) $splitTemplate = doc.createElement('template'); + if (scope.current) scope.current.register(this); + const { words, chars, lines, accessible, includeSpaces, debug } = parameters; + const $target = /** @type {HTMLElement} */((target = isArr(target) ? target[0] : target) && /** @type {Node} */(target).nodeType ? target : (getNodeList(target) || [])[0]); + const lineParams = lines === true ? {} : lines; + const wordParams = words === true || isUnd(words) ? {} : words; + const charParams = chars === true ? {} : chars; + this.debug = setValue(debug, false); + this.includeSpaces = setValue(includeSpaces, false); + this.accessible = setValue(accessible, true); + this.linesOnly = lineParams && (!wordParams && !charParams); + /** @type {String|false|SplitFunctionValue} */ + this.lineTemplate = isObj(lineParams) ? generateTemplate(lineType, /** @type {SplitTemplateParams} */(lineParams)) : lineParams; + /** @type {String|false|SplitFunctionValue} */ + this.wordTemplate = isObj(wordParams) || this.linesOnly ? generateTemplate(wordType, /** @type {SplitTemplateParams} */(wordParams)) : wordParams; + /** @type {String|false|SplitFunctionValue} */ + this.charTemplate = isObj(charParams) ? generateTemplate(charType, /** @type {SplitTemplateParams} */(charParams)) : charParams; + this.$target = $target; + this.html = $target && $target.innerHTML; + this.lines = []; + this.words = []; + this.chars = []; + this.effects = []; + this.effectsCleanups = []; + this.cache = null; + this.ready = false; + this.width = 0; + this.resizeTimeout = null; + const handleSplit = () => this.html && (lineParams || wordParams || charParams) && this.split(); + // Make sure this is declared before calling handleSplit() in case revert() is called inside an effect callback + this.resizeObserver = new ResizeObserver(() => { + // Use a setTimeout instead of a Timer for better tree shaking + clearTimeout(this.resizeTimeout); + this.resizeTimeout = setTimeout(() => { + const currentWidth = /** @type {HTMLElement} */($target).offsetWidth; + if (currentWidth === this.width) return; + this.width = currentWidth; + handleSplit(); + }, 150); + }); + // Only declare the font ready promise when splitting by lines and not alreay split + if (this.lineTemplate && !this.ready) { + doc.fonts.ready.then(handleSplit); + } else { + handleSplit(); + } + $target ? this.resizeObserver.observe($target) : console.warn('No Text Splitter target found.'); + } + + /** + * @param {(...args: any[]) => Tickable | (() => void) | void} effect + * @return this + */ + addEffect(effect) { + if (!isFnc(effect)) { console.warn('Effect must return a function.'); return this; } + const refreshableEffect = keepTime(effect); + this.effects.push(refreshableEffect); + if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this); + return this; + } + + revert() { + clearTimeout(this.resizeTimeout); + this.lines.length = this.words.length = this.chars.length = 0; + this.resizeObserver.disconnect(); + // Make sure to revert the effects after disconnecting the resizeObserver to avoid triggering it in the process + this.effectsCleanups.forEach(cleanup => isFnc(cleanup) ? cleanup(this) : cleanup.revert && cleanup.revert()); + this.$target.innerHTML = this.html; + return this; + } + + /** + * Recursively processes a node and its children + * @param {Node} node + */ + splitNode(node) { + const wordTemplate = this.wordTemplate; + const charTemplate = this.charTemplate; + const includeSpaces = this.includeSpaces; + const debug = this.debug; + const nodeType = node.nodeType; + if (nodeType === 3) { + const nodeText = node.nodeValue; + // If the nodeText is only whitespace, leave it as is + if (nodeText.trim()) { + const tempWords = []; + const words = this.words; + const chars = this.chars; + const wordSegments = wordSegmenter.segment(nodeText); + const $wordsFragment = doc.createDocumentFragment(); + let prevSeg = null; + for (const wordSegment of wordSegments) { + const segment = wordSegment.segment; + const isWordLike = isSegmentWordLike(wordSegment); + // Determine if this segment should be a new word, first segment always becomes a new word + if (!prevSeg || (isWordLike && (prevSeg && (isSegmentWordLike(prevSeg))))) { + tempWords.push(segment); + } else { + // Only concatenate if both current and previous are non-word-like and don't contain spaces + const lastWordIndex = tempWords.length - 1; + const lastWord = tempWords[lastWordIndex]; + if (!whiteSpaceGroupRgx.test(lastWord) && !whiteSpaceGroupRgx.test(segment)) { + tempWords[lastWordIndex] += segment; + } else { + tempWords.push(segment); } } - } else { - parsedPropertyValue = isArr(propertyValue) ? - propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, targetsLength)) : - normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, targetsLength); - if (individualTransformProperty) { - keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; - cachedTransforms[individualTransformProperty] = parsedPropertyValue; + prevSeg = wordSegment; + } + + for (let i = 0, l = tempWords.length; i < l; i++) { + const word = tempWords[i]; + if (!word.trim()) { + // Preserve whitespace only if includeSpaces is false and if the current space is not the first node + if (i && includeSpaces) continue; + $wordsFragment.appendChild(doc.createTextNode(word)); } else { - keyframes[name] = parsedPropertyValue; + const nextWord = tempWords[i + 1]; + const hasWordFollowingSpace = includeSpaces && nextWord && !nextWord.trim(); + const wordToProcess = word; + const charSegments = charTemplate ? graphemeSegmenter.segment(wordToProcess) : null; + const $charsFragment = charTemplate ? doc.createDocumentFragment() : doc.createTextNode(hasWordFollowingSpace ? word + '\xa0' : word); + if (charTemplate) { + const charSegmentsArray = [...charSegments]; + for (let j = 0, jl = charSegmentsArray.length; j < jl; j++) { + const charSegment = charSegmentsArray[j]; + const isLastChar = j === jl - 1; + // If this is the last character and includeSpaces is true with a following space, append the space + const charText = isLastChar && hasWordFollowingSpace ? charSegment.segment + '\xa0' : charSegment.segment; + const $charNode = doc.createTextNode(charText); + processHTMLTemplate(charTemplate, chars, $charNode, /** @type {DocumentFragment} */($charsFragment), charType, debug, -1, words.length, chars.length); + } + } + if (wordTemplate) { + processHTMLTemplate(wordTemplate, words, $charsFragment, $wordsFragment, wordType, debug, -1, words.length, chars.length); + // Chars elements must be re-parsed in the split() method if both words and chars are parsed + } else if (charTemplate) { + $wordsFragment.appendChild($charsFragment); + } else { + $wordsFragment.appendChild(doc.createTextNode(word)); + } + // Skip the next iteration if we included a space + if (hasWordFollowingSpace) i++; + } + } + node.parentNode.replaceChild($wordsFragment, node); + } + } else if (nodeType === 1) { + // Converting to an array is necessary to work around childNodes pottential mutation + const childNodes = /** @type {Array} */([.../** @type {*} */(node.childNodes)]); + for (let i = 0, l = childNodes.length; i < l; i++) this.splitNode(childNodes[i]); + } + } + + /** + * @param {Boolean} clearCache + * @return {this} + */ + split(clearCache = false) { + const $el = this.$target; + const isCached = !!this.cache && !clearCache; + const lineTemplate = this.lineTemplate; + const wordTemplate = this.wordTemplate; + const charTemplate = this.charTemplate; + const fontsReady = doc.fonts.status !== 'loading'; + const canSplitLines = lineTemplate && fontsReady; + this.ready = !lineTemplate || fontsReady; + if (canSplitLines || clearCache) { + // No need to revert effects animations here since it's already taken care by the refreshable + this.effectsCleanups.forEach(cleanup => isFnc(cleanup) && cleanup(this)); + } + if (!isCached) { + if (clearCache) { + $el.innerHTML = this.html; + this.words.length = this.chars.length = 0; + } + this.splitNode($el); + this.cache = $el.innerHTML; + } + if (canSplitLines) { + if (isCached) $el.innerHTML = this.cache; + this.lines.length = 0; + if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); + } + // Always reparse characters after a line reset or if both words and chars are activated + if (charTemplate && (canSplitLines || wordTemplate)) { + this.chars = getAllTopLevelElements($el, charType); + } + // Words are used when lines only and prioritized over chars + const elementsArray = this.words.length ? this.words : this.chars; + let y, linesCount = 0; + for (let i = 0, l = elementsArray.length; i < l; i++) { + const $el = elementsArray[i]; + const { top, height } = $el.getBoundingClientRect(); + if (!isUnd(y) && top - y > height * .5) linesCount++; + $el.setAttribute(dataLine, `${linesCount}`); + const nested = $el.querySelectorAll(`[${dataLine}]`); + let c = nested.length; + while (c--) nested[c].setAttribute(dataLine, `${linesCount}`); + y = top; + } + if (canSplitLines) { + const linesFragment = doc.createDocumentFragment(); + const parents = new Set(); + const clones = []; + for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) { + const $clone = /** @type {HTMLElement} */($el.cloneNode(true)); + filterLineElements($clone, lineIndex, new Set()).forEach($el => { + const $parent = $el.parentNode; + if ($parent) { + if ($el.nodeType === 1) parents.add(/** @type {HTMLElement} */($parent)); + $parent.removeChild($el); } - addWAAPIAnimation(this, $el, name, keyframes, tweenParams); - } + }); + clones.push($clone); } - if (hasIndividualTransforms) { - let transforms = emptyString; - for (let t in cachedTransforms) { - transforms += `${transformsFragmentStrings[t]}var(--${t})) `; - } - elStyle.transform = transforms; + parents.forEach(filterEmptyElements); + for (let cloneIndex = 0, clonesLength = clones.length; cloneIndex < clonesLength; cloneIndex++) { + processHTMLTemplate(lineTemplate, this.lines, clones[cloneIndex], linesFragment, lineType, this.debug, cloneIndex); } - }); - - if (scroll) { - /** @type {ScrollObserver} */(this.autoplay).link(this); + $el.innerHTML = ''; + $el.appendChild(linesFragment); + if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); + if (charTemplate) this.chars = getAllTopLevelElements($el, charType); } - } - - /** - * @callback forEachCallback - * @param {globalThis.Animation} animation - */ - /** - * @param {forEachCallback|String} callback - * @return {this} - */ - forEach(callback) { - const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; - this.animations.forEach(cb); + // Remove the word wrappers and clear the words array if lines split only + if (this.linesOnly) { + const words = this.words; + let w = words.length; + while (w--) { + const $word = words[w]; + $word.replaceWith($word.textContent); + } + words.length = 0; + } + if (this.accessible && (canSplitLines || !isCached)) { + const $accessible = doc.createElement('span'); + // Make the accessible element visually-hidden (https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html) + $accessible.style.cssText = `position:absolute;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);width:1px;height:1px;white-space:nowrap;`; + // $accessible.setAttribute('tabindex', '-1'); + $accessible.innerHTML = this.html; + $el.insertBefore($accessible, $el.firstChild); + this.lines.forEach(setAriaHidden); + this.words.forEach(setAriaHidden); + this.chars.forEach(setAriaHidden); + } + this.width = /** @type {HTMLElement} */($el).offsetWidth; + if (canSplitLines || clearCache) { + this.effects.forEach((effect, i) => this.effectsCleanups[i] = effect(this)); + } return this; } - get speed() { - return this._speed; - } - - set speed(speed) { - this._speed = +speed; - this.forEach(anim => anim.playbackRate = speed); - } - - get currentTime() { - const controlAnimation = this.controlAnimation; - const timeScale = globals.timeScale; - return this.completed ? this.duration : controlAnimation ? +controlAnimation.currentTime * (timeScale === 1 ? 1 : timeScale) : 0; - } - - set currentTime(time) { - const t = time * (globals.timeScale === 1 ? 1 : K); - this.forEach(anim => { - // Make sure the animation playState is not 'paused' in order to properly trigger an onfinish callback. - // The "paused" play state supersedes the "finished" play state; if the animation is both paused and finished, the "paused" state is the one that will be reported. - // https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish_event - // This is not needed for persisting animations since they never finish. - if (!this.persist && t >= this.duration) anim.play(); - anim.currentTime = t; - }); - } - - get progress() { - return this.currentTime / this.duration; - } - - set progress(progress) { - this.forEach(anim => anim.currentTime = progress * this.duration || 0); - } - - resume() { - if (!this.paused) return this; - this.paused = false; - // TODO: Store the current time, and seek back to the last position - return this.forEach('play'); - } - - pause() { - if (this.paused) return this; - this.paused = true; - return this.forEach('pause'); - } - - alternate() { - this.reversed = !this.reversed; - this.forEach('reverse'); - if (this.paused) this.forEach('pause'); - return this; + refresh() { + this.split(true); } +} - play() { - if (this.reversed) this.alternate(); - return this.resume(); - } +/** + * @param {Element|NodeList|String|Array} target + * @param {TextSplitterParams} [parameters] + * @return {TextSplitter} + */ +const splitText = (target, parameters) => new TextSplitter(target, parameters); - reverse() { - if (!this.reversed) this.alternate(); - return this.resume(); - } +/** + * @deprecated text.split() is deprecated, import splitText() directly, or text.splitText() + * + * @param {HTMLElement|NodeList|String|Array} target + * @param {TextSplitterParams} [parameters] + * @return {TextSplitter} + */ +const split = (target, parameters) => { + console.warn('text.split() is deprecated, import splitText() directly, or text.splitText()'); + return new TextSplitter(target, parameters); +}; - /** - * @param {Number} time - * @param {Boolean} muteCallbacks - */ - seek(time, muteCallbacks = false) { - if (muteCallbacks) this.muteCallbacks = true; - if (time < this.duration) this.completed = false; - this.currentTime = time; - this.muteCallbacks = false; - if (this.paused) this.pause(); - return this; - } - restart() { - this.completed = false; - return this.seek(0, true).resume(); - } - commitStyles() { - return this.forEach('commitStyles'); +/** + * '-' is the range operator; place it at the start or end of the string to use it as a literal (e.g. '-abc' or 'abc-') + * @param {String} str + * @return {String} + */ +const expandCharRanges = (str) => { + let result = ''; + for (let i = 0, l = str.length; i < l; i++) { + if (i + 2 < l && str[i + 1] === '-' && str.charCodeAt(i) < str.charCodeAt(i + 2)) { + const start = str.charCodeAt(i); + const end = str.charCodeAt(i + 2); + for (let c = start; c <= end; c++) result += String.fromCharCode(c); + i += 2; + } else { + result += str[i]; + } } + return result; +}; - complete() { - return this.seek(this.duration); - } +const charSets = { + lowercase: 'a-z', + uppercase: 'A-Z', + numbers: '0-9', + symbols: '!%#_|*+=', + braille: 'â €-⣿', + blocks: 'â–€-â–Ÿ', + shades: 'â–‘-â–“', +}; - cancel() { - this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise - return this.commitStyles().forEach('cancel'); - } +const originalTexts = new WeakMap(); - revert() { - // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted, - // This means if you have multiple animations animating different transforms on the same target, - // reverting one of them will also override the transform property of the other animations. - // A better approach would be to store the original custom property values is they exist instead of the entire transform value, - // and update the CSS variables with the orignal value - this.cancel().targets.forEach(($el, i) => { - const targetStyle = $el.style; - const targetInlineStyles = this._inlineStyles[i]; - for (let name in targetInlineStyles) { - const originalInlinedValue = targetInlineStyles[name]; - if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) { - targetStyle.removeProperty(toLowerCase(name)); - } else { - targetStyle[name] = originalInlinedValue; +/** + * Returns a function-based tween value that scrambles the target's text content, + * progressively revealing the original text. + * + * @param {ScrambleTextParams} [params] + * @return {FunctionValue} + */ +const scrambleText = (params = {}) => { + if (!params) params = {}; + const charsParam = params.chars; + const easeFn = parseEase(params.ease || 'linear'); + const text = params.text; + const fromParam = params.from; + const reversed = params.reversed || false; + const perturbation = params.perturbation || 0; + const cursorParam = params.cursor; + const cursorChars = cursorParam === true ? '_' + : typeof cursorParam === 'number' ? String.fromCharCode(cursorParam) + : typeof cursorParam === 'string' ? cursorParam + : ''; + const cursorLen = cursorChars.length; + const seed = params.seed || 0; + const override = params.override !== undefined ? params.override : true; + const revealRate = params.revealRate || 60; + const interval = 1000 * globals.timeScale / revealRate; + const settleDuration = params.settleDuration || 300 * globals.timeScale; + const settleRate = params.settleRate || 30; + const durationParam = params.duration; + const revealDelayParam = params.revealDelay; + const delayParam = params.delay; + const onChange = params.onChange || noop; + + return (target, index, targets, prevTween) => { + const rawChars = typeof charsParam === 'function' ? charsParam(target, index, targets) : (charsParam || 'a-zA-Z0-9!%#_'); + const characters = expandCharRanges(charSets[rawChars] || rawChars); + const totalChars = characters.length - 1; + const duration = typeof durationParam === 'function' ? durationParam(target, index, targets) : durationParam; + const revealDelay = typeof revealDelayParam === 'function' ? revealDelayParam(target, index, targets) : (revealDelayParam || 0); + const delay = typeof delayParam === 'function' ? delayParam(target, index, targets) : (delayParam || 0); + const rng = seed ? createSeededRandom(seed) : createSeededRandom(); + if (!originalTexts.has(target)) originalTexts.set(target, target.textContent); + const startingText = prevTween ? prevTween._value : target.textContent; + const targetText = text !== undefined + ? (typeof text === 'function' ? text(target, index, targets) : text) + : prevTween ? prevTween._value + : originalTexts.get(target); + const settledText = targetText === ' ' || targetText === ' ' ? ' ' : targetText; + const startLength = startingText === ' ' ? 0 : startingText.length; + const endLength = settledText.length; + const overrideChars = override === true ? characters + : typeof override === 'string' && override.length > 0 ? expandCharRanges(charSets[/** @type {String} */(override)] || /** @type {String} */(override)) + : null; + const totalOverrideChars = overrideChars ? overrideChars.length - 1 : 0; + // Space override uses   so the browser doesn't collapse consecutive spaces in innerHTML + const overrideChar = override === ' ' ? ' ' : null; + // When starting from blank, only animate the target text length to avoid padding beyond it + const animLength = override === '' ? endLength : Math.max(startLength, endLength); + // Compute total duration from interval spacing and settle time, or use the explicit duration + const animDuration = duration > 0 ? duration : (animLength - 1) * interval + settleDuration; + const computedDuration = round$1((animDuration + revealDelay) / globals.timeScale, 0) * globals.timeScale; + const revealDelayRatio = revealDelay > 0 ? round$1(revealDelay / computedDuration, 12) : 0; + // Auto-resolve reveal direction: shrinking text reveals from right, growing from left + const resolvedFrom = fromParam === undefined || fromParam === 'auto' ? (endLength < startLength ? 'right' : 'left') : fromParam; + const charOrder = new Int32Array(animLength); + if (resolvedFrom === 'random') { + for (let i = 0; i < animLength; i++) charOrder[i] = i; + for (let i = animLength - 1; i > 0; i--) { + const j = rng(0, i); + const t = charOrder[i]; charOrder[i] = charOrder[j]; charOrder[j] = t; + } + } else { + const ref = resolvedFrom === 'right' ? (override === '' || !startLength ? animLength : startLength) - 1 + : resolvedFrom === 'center' ? ((override === '' || !startLength ? animLength : startLength) - 1) / 2 + : typeof resolvedFrom === 'number' ? resolvedFrom + : 0; + const abs = Math.abs; + const indices = new Array(animLength); + for (let i = 0; i < animLength; i++) indices[i] = i; + indices.sort((a, b) => abs(a - ref) - abs(b - ref)); + for (let i = 0; i < animLength; i++) charOrder[indices[i]] = i; + } + if (reversed) { + const last = animLength - 1; + for (let i = 0; i < animLength; i++) charOrder[i] = last - charOrder[i]; + } + // settleRatio is the fraction of the animation each character spends in the active scrambling zone + const settleRatio = round$1(settleDuration / animDuration, 12); + // settleSpacing is the time gap between consecutive characters entering the active zone + const settleSpacing = round$1((1 - settleRatio) / animLength, 12); + const cursorZone = cursorLen * settleSpacing; + // stepRatio controls how often scramble characters refresh (based on settleRate) + const stepRatio = round$1(1000 * globals.timeScale / (settleRate * computedDuration), 12); + // Pre-compute per-character start and settle times + const charStarts = new Float32Array(animLength); + const charEnds = new Float32Array(animLength); + const scale = perturbation > 0 ? perturbation * settleRatio : 0; + for (let c = 0; c < animLength; c++) { + const so = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + const eo = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + charStarts[c] = charOrder[c] * settleSpacing + so; + charEnds[c] = Math.ceil((charStarts[c] + settleRatio + eo) / stepRatio) * stepRatio; + } + // When text shrinks with non-sequential from modes, delay target settle times past all extras + if (endLength < animLength && resolvedFrom !== 'left' && resolvedFrom !== 'right' && resolvedFrom !== 'random') { + let maxExtraEnd = 0; + for (let c = endLength; c < animLength; c++) { + if (charEnds[c] > maxExtraEnd) maxExtraEnd = charEnds[c]; + } + const targets = new Array(endLength); + for (let c = 0; c < endLength; c++) targets[c] = c; + targets.sort((a, b) => charOrder[a] - charOrder[b]); + const targetSpacing = (1 - maxExtraEnd) / endLength; + for (let i = 0; i < endLength; i++) { + const revealTime = maxExtraEnd + i * targetSpacing; + if (revealTime > charEnds[targets[i]]) { + charEnds[targets[i]] = revealTime; } } - // Remove style attribute if empty - if ($el.getAttribute('style') === emptyString) $el.removeAttribute('style'); - }); - return this; - } + } + // charCache holds the current scramble character for each position, refreshed at settleRate + const charCache = new Array(animLength); + for (let c = 0; c < animLength; c++) { + charCache[c] = characters[rng(0, totalChars)]; + } + // overrideCache holds scramble characters for the starting text (override: true or custom string) + const overrideCache = overrideChars ? (overrideChars === characters ? charCache : new Array(animLength)) : null; + if (overrideCache && overrideCache !== charCache) { + for (let c = 0; c < animLength; c++) { + overrideCache[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, overrideChars.length - 1)]; + } + } + // Build the initial display text based on override mode + let fillStartText = startingText; + if (!prevTween) { + if (override === '') { + fillStartText = ''; + } else if (overrideChars) { + fillStartText = ''; + for (let c = 0; c < startLength; c++) { + fillStartText += startingText[c] === ' ' ? ' ' : /** @type {Array} */(overrideCache)[c]; + } + } + } - /** - * @typedef {this & {then: null}} ResolvedWAAPIAnimation - */ + let lastValue = -1; + let lastStep = -1; + let scrambled = ''; + const hasOverride = override !== ''; + const hasOverrideChars = !!overrideChars; + const hasCursor = cursorLen > 0; - /** - * @param {Callback} [callback] - * @return Promise - */ - then(callback = noop) { - const then = this.then; - const onResolve = () => { - this.then = null; - callback(/** @type {ResolvedWAAPIAnimation} */(this)); - this.then = then; - this._resolve = noop; - }; - return new Promise(r => { - this._resolve = () => r(onResolve()); - if (this.completed) this._resolve(); - return this; - }); + return { + from: 0, + to: 1, + duration: computedDuration, + delay: delay, + ease: 'linear', + modifier: (v) => { + if (v === lastValue) return scrambled; + lastValue = v; + if (delay > 0 && v <= 0) { scrambled = startingText; return startingText; } + if (v <= 0) { scrambled = fillStartText; return fillStartText; } + if (v >= 1) { scrambled = settledText; return settledText; } + scrambled = ''; + // Only refresh scramble characters when we cross a settleRate step boundary + const currentStep = (v / stepRatio) | 0; + const refreshChars = currentStep !== lastStep; + if (refreshChars) lastStep = currentStep; + // Subtract delay ratio to get the effective animation progress + const linear = revealDelayRatio > 0 ? (v - revealDelayRatio) / (1 - revealDelayRatio) : v; + const t = linear > 0 ? easeFn(linear) : 0; + for (let c = 0; c < animLength; c++) { + // Each character has its own start/end window based on its reveal order + const charStart = charStarts[c]; + const charEnd = charEnds[c]; + // Settled zone: character has finished its transition + if (t >= charEnd) { + if (c < endLength) scrambled += settledText[c]; + continue; + } + // Pre-transition zone: reveal wave hasn't reached this character yet + if (t <= 0 || t < charStart) { + if (hasOverride && c < startLength) { + if (hasOverrideChars) { + if (startingText[c] === ' ') { + scrambled += ' '; + } else { + if (refreshChars) /** @type {Array} */(overrideCache)[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, totalOverrideChars)]; + scrambled += /** @type {Array} */(overrideCache)[c]; + } + } else { + // Default (override: false): show the original starting text + scrambled += startingText[c]; + } + } + continue; + } + // Active zone: character is between charStart and charEnd + const isSpace = (c < endLength && settledText[c] === ' ') || (c < startLength && startingText[c] === ' '); + if (isSpace) { + scrambled += ' '; + } else if (hasCursor && t - charStart < cursorZone) { + // Cursor sub-zone: show cursor character based on position within cursor width + scrambled += cursorChars[cursorLen - 1 - (((t - charStart) / settleSpacing) | 0)]; + } else { + // Scramble zone: show cycling random characters + if (refreshChars) charCache[c] = characters[rng(0, totalChars)]; + scrambled += charCache[c]; + } + } + if (refreshChars) onChange(scrambled, t); + return scrambled; + } + } } -} - -const waapi = { -/** - * @param {DOMTargetsParam} targets - * @param {WAAPIAnimationParams} params - * @return {WAAPIAnimation} - */ - animate: (targets, params) => new WAAPIAnimation(targets, params), - convertEase: easingToLinear }; -export { registerTargets as $, Animatable, Draggable, JSAnimation, Scope, ScrollObserver, Spring, TextSplitter, Timeline, Timer, WAAPIAnimation, animate, clamp, cleanInlineStyles, createAnimatable, createDraggable, createDrawable, createMotionPath, createScope, createSeededRandom, createSpring, createTimeline, createTimer, cubicBezier, damp, degToRad, eases, index$3 as easings, engine, get, irregular, keepTime, lerp, linear, mapRange, morphTo, onScroll, padEnd, padStart, radToDeg, random, randomPick, remove, round, roundPad, scrollContainers, set, shuffle, snap, split, splitText, spring, stagger, steps, index$1 as svg, sync, index as text, index$2 as utils, waapi, wrap }; +var index = /*#__PURE__*/Object.freeze({ + __proto__: null, + TextSplitter: TextSplitter, + scrambleText: scrambleText, + split: split, + splitText: splitText +}); + +export { registerTargets as $, Animatable, AutoLayout, Draggable, JSAnimation, Scope, ScrollObserver, Spring, TextSplitter, Timeline, Timer, WAAPIAnimation, addChild, animate, clamp, cleanInlineStyles, createAnimatable, createDraggable, createDrawable, createLayout, createMotionPath, createScope, createSeededRandom, createSpring, createTimeline, createTimer, cubicBezier, damp, degToRad, eases, index$3 as easings, engine, forEachChildren, get, globals, irregular, keepTime, lerp, linear, mapRange, morphTo, onScroll, padEnd, padStart, radToDeg, random, randomPick, remove, removeChild, round, roundPad, scrambleText, scrollContainers, set, shuffle, snap, split, splitText, spring, stagger, steps, index$1 as svg, sync, index as text, index$2 as utils, waapi, wrap }; diff --git a/dist/bundles/anime.esm.min.js b/dist/bundles/anime.esm.min.js index 8bd669632..7390679e8 100644 --- a/dist/bundles/anime.esm.min.js +++ b/dist/bundles/anime.esm.min.js @@ -1,7 +1,7 @@ /** * Anime.js - ESM minified bundle - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -const t="undefined"!=typeof window,e=t?window:null,s=t?document:null,i={OBJECT:0,ATTRIBUTE:1,CSS:2,TRANSFORM:3,CSS_VAR:4},r={NUMBER:0,UNIT:1,COLOR:2,COMPLEX:3},n={NONE:0,AUTO:1,FORCE:2},o={replace:0,none:1,blend:2},a=Symbol(),h=Symbol(),l=Symbol(),c=Symbol(),d=Symbol(),u=Symbol(),p=1e-11,m=1e12,f=1e3,g=120,_="",y="var(",v=(()=>{const t=new Map;return t.set("x","translateX"),t.set("y","translateY"),t.set("z","translateZ"),t})(),b=["translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY","matrix","matrix3d","perspective"],T=b.reduce((t,e)=>({...t,[e]:e+"("}),{}),S=()=>{},w=/(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i,x=/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i,C=/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,E=/hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i,k=/hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,$=/[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi,B=/^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i,O=/([a-z])([A-Z])/g,N=/(\w+)(\([^)]+\)+)/g,R=/(\*=|\+=|-=)/,D=/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/,L={id:null,keyframes:null,playbackEase:null,playbackRate:1,frameRate:g,loop:0,reversed:!1,alternate:!1,autoplay:!0,persist:!1,duration:f,delay:0,loopDelay:0,ease:"out(2)",composition:o.replace,modifier:t=>t,onBegin:S,onBeforeUpdate:S,onUpdate:S,onLoop:S,onPause:S,onComplete:S,onRender:S},A={current:null,root:s},F={defaults:L,precision:4,timeScale:1,tickThreshold:200},M={version:"4.2.2",engine:null};t&&(e.AnimeJS||(e.AnimeJS=[]),e.AnimeJS.push(M));const P=t=>t.replace(O,"$1-$2").toLowerCase(),I=(t,e)=>0===t.indexOf(e),z=Date.now,U=Array.isArray,X=t=>t&&t.constructor===Object,Y=t=>"number"==typeof t&&!isNaN(t),V=t=>"string"==typeof t,W=t=>"function"==typeof t,H=t=>void 0===t,q=t=>H(t)||null===t,j=e=>t&&e instanceof SVGElement,G=t=>w.test(t),Q=t=>I(t,"rgb"),J=t=>I(t,"hsl"),Z=t=>G(t)||Q(t)||J(t),K=t=>!F.defaults.hasOwnProperty(t),tt=["opacity","rotate","overflow","color"],et=(t,e)=>{if(tt.includes(e))return!1;if(t.getAttribute(e)||e in t){if("scale"===e){const e=t.parentNode;return e&&"filter"===e.tagName}return!0}},st=t=>V(t)?parseFloat(t):t,it=Math.pow,rt=Math.sqrt,nt=Math.sin,ot=Math.cos,at=Math.abs,ht=Math.exp,lt=Math.ceil,ct=Math.floor,dt=Math.asin,ut=Math.max,pt=Math.atan2,mt=Math.PI,ft=Math.round,gt=(t,e,s)=>ts?s:t,_t={},yt=(t,e)=>{if(e<0)return t;if(!e)return ft(t);let s=_t[e];return s||(s=_t[e]=10**e),ft(t*s)/s},vt=(t,e)=>U(e)?e.reduce((e,s)=>at(s-t)t+(e-t)*s,Tt=t=>t===1/0?m:t===-1/0?-m:t,St=t=>t<=p?p:Tt(yt(t,11)),wt=t=>U(t)?[...t]:t,xt=(t,e)=>{const s={...t};for(let i in e){const r=t[i];s[i]=H(r)?e[i]:r}return s},Ct=(t,e,s,i="_prev",r="_next")=>{let n=t._head,o=r;for(s&&(n=t._tail,o=i);n;){const t=n[o];e(n),n=t}},Et=(t,e,s="_prev",i="_next")=>{const r=e[s],n=e[i];r?r[i]=n:t._head=n,n?n[s]=r:t._tail=r,e[s]=null,e[i]=null},kt=(t,e,s,i="_prev",r="_next")=>{let n=t._tail;for(;n&&s&&s(n,e);)n=n[i];const o=n?n[r]:t._head;n?n[r]=e:t._head=e,o?o[i]=e:t._tail=e,e[i]=n,e[r]=o},$t=(t,e,s)=>{const i=t.style.transform;let r;if(i){const n=t[c];let o;for(;o=N.exec(i);){const t=o[1],i=o[2].slice(1,-1);n[t]=i,t===e&&(r=i,s&&(s[e]=i))}}return i&&!H(r)?r:I(e,"scale")?"1":I(e,"rotate")||I(e,"skew")?"0deg":"0px"},Bt=t=>{const e=x.exec(t)||C.exec(t),s=H(e[4])?1:+e[4];return[+e[1],+e[2],+e[3],s]},Ot=t=>{const e=t.length,s=4===e||5===e;return[+("0x"+t[1]+t[s?1:2]),+("0x"+t[s?2:3]+t[s?2:4]),+("0x"+t[s?3:5]+t[s?3:6]),5===e||9===e?+(+("0x"+t[s?4:7]+t[s?4:8])/255).toFixed(3):1]},Nt=(t,e,s)=>(s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+(e-t)*(2/3-s)*6:t),Rt=t=>{const e=E.exec(t)||k.exec(t),s=+e[1]/360,i=+e[2]/100,r=+e[3]/100,n=H(e[4])?1:+e[4];let o,a,h;if(0===i)o=a=h=r;else{const t=r<.5?r*(1+i):r+i-r*i,e=2*r-t;o=yt(255*Nt(e,t,s+1/3),0),a=yt(255*Nt(e,t,s),0),h=yt(255*Nt(e,t,s-1/3),0)}return[o,a,h,n]},Dt=t=>Q(t)?Bt(t):G(t)?Ot(t):J(t)?Rt(t):[0,0,0,1],Lt=(t,e)=>H(t)?e:t,At=(t,e,s,i,r)=>{let n;if(W(t))n=()=>{const r=t(e,s,i);return isNaN(+r)?r||0:+r};else{if(!V(t)||!I(t,y))return t;n=()=>{const s=t.match(D),i=s[1],r=s[2];let n=getComputedStyle(e)?.getPropertyValue(i);return n&&n.trim()!==_||!r||(n=r.trim()),n||0}}return r&&(r.func=n),n()},Ft=(t,e)=>t[h]?t[l]&&et(t,e)?i.ATTRIBUTE:b.includes(e)||v.get(e)?i.TRANSFORM:I(e,"--")?i.CSS_VAR:e in t.style?i.CSS:e in t?i.OBJECT:i.ATTRIBUTE:i.OBJECT,Mt=(t,e,s)=>{const i=t.style[e];i&&s&&(s[e]=i);const r=i||getComputedStyle(t[u]||t).getPropertyValue(e);return"auto"===r?"0":r},Pt=(t,e,s,r)=>{const n=H(s)?Ft(t,e):s;return n===i.OBJECT?t[e]||0:n===i.ATTRIBUTE?t.getAttribute(e):n===i.TRANSFORM?$t(t,e,r):n===i.CSS_VAR?Mt(t,e,r).trimStart():Mt(t,e,r)},It=(t,e,s)=>"-"===s?t-e:"+"===s?t+e:t*e,zt=()=>({t:r.NUMBER,n:0,u:null,o:null,d:null,s:null}),Ut=(t,e)=>{if(e.t=r.NUMBER,e.n=0,e.u=null,e.o=null,e.d=null,e.s=null,!t)return e;const s=+t;if(isNaN(s)){let s=t;"="===s[1]&&(e.o=s[0],s=s.slice(2));const i=!s.includes(" ")&&B.exec(s);if(i)return e.t=r.UNIT,e.n=+i[1],e.u=i[2],e;if(e.o)return e.n=+s,e;if(Z(s))return e.t=r.COLOR,e.d=Dt(s),e;{const t=s.match($);return e.t=r.COMPLEX,e.d=t?t.map(Number):[],e.s=s.split($)||[],e}}return e.n=s,e},Xt=(t,e)=>(e.t=t._valueType,e.n=t._toNumber,e.u=t._unit,e.o=null,e.d=wt(t._toNumbers),e.s=wt(t._strings),e),Yt=zt(),Vt=(t,e,s,a,h)=>{const l=t.parent,d=t.duration,u=t.completed,m=t.iterationDuration,f=t.iterationCount,g=t._currentIteration,y=t._loopDelay,v=t._reversed,b=t._alternate,S=t._hasChildren,w=t._delay,x=t._currentTime,C=w+m,E=e-w,k=gt(x,-w,d),$=gt(E,-w,d),B=E-x,O=$>0,N=$>=d,R=d<=p,D=h===n.FORCE;let L=0,A=E,M=0;if(f>1){const e=~~($/(m+(N?0:y)));t._currentIteration=gt(e,0,f),N&&t._currentIteration--,L=t._currentIteration%2,A=$%(m+y)||0}const P=v^(b&&L),I=t._ease;let z=N?P?0:d:P?m-A:A;I&&(z=m*I(z/m)||0);const U=(l?l.backwards:E=w&&e<=C||e<=w&&k>w||e>=C&&k!==d)||z>=C&&k!==d||z<=w&&k>0||e<=k&&k===d&&u||N&&!u&&R){if(O&&(t.computeDeltaTime(k),s||t.onBeforeUpdate(t)),!S){const e=D||(U?-1*B:B)>=F.tickThreshold,n=t._offset+(l?l._offset:0)+w+z;let h,d,u,p,m=t._head,f=0;for(;m;){const t=m._composition,s=m._currentTime,l=m._changeDuration,g=m._absoluteStartTime+m._changeDuration,y=m._nextRep,v=m._prevRep,b=t!==o.none;if((e||(s!==l||n<=g+(y?y._delay:0))&&(0!==s||n>=m._absoluteStartTime))&&(!b||!m._isOverridden&&(!m._isOverlapped||n<=g)&&(!y||y._isOverridden||n<=y._absoluteStartTime)&&(!v||v._isOverridden||n>=v._absoluteStartTime+v._changeDuration+m._delay))){const e=m._currentTime=gt(z-m._startTime,0,l),s=m._ease(e/m._updateDuration),n=m._modifier,g=m._valueType,_=m._tweenType,y=_===i.OBJECT,v=g===r.NUMBER,T=v&&y||0===s||1===s?-1:F.precision;let S,w;if(v)S=w=n(yt(bt(m._fromNumber,m._toNumber,s),T));else if(g===r.UNIT)w=n(yt(bt(m._fromNumber,m._toNumber,s),T)),S=`${w}${m._unit}`;else if(g===r.COLOR){const t=m._fromNumbers,e=m._toNumbers,i=yt(gt(n(bt(t[0],e[0],s)),0,255),0),r=yt(gt(n(bt(t[1],e[1],s)),0,255),0),o=yt(gt(n(bt(t[2],e[2],s)),0,255),0),a=gt(n(yt(bt(t[3],e[3],s),T)),0,1);if(S=`rgba(${i},${r},${o},${a})`,b){const t=m._numbers;t[0]=i,t[1]=r,t[2]=o,t[3]=a}}else if(g===r.COMPLEX){S=m._strings[0];for(let t=0,e=m._toNumbers.length;t0&&!u||U&&E<=p&&u)&&(t.onComplete(t),t.completed=!U):O&&N?f===1/0?t._startTime+=t.duration:t._currentIteration>=f-1&&(t.paused=!0,u||S||(t.completed=!0,s||l&&(U||!l.began)||(t.onComplete(t),t._resolve(t)))):t.completed=!1,M},Wt=(t,e,s,i,r)=>{const o=t._currentIteration;if(Vt(t,e,s,i,r),t._hasChildren){const a=t,h=a.backwards,l=i?e:a._iterationTime,c=z();let d=0,u=!0;if(!i&&a._currentIteration!==o){const t=a.iterationDuration;Ct(a,e=>{if(h){const i=e.duration,r=e._offset+e._delay;s||!(i<=p)||r&&r+i!==t||e.onComplete(e)}else!e.completed&&!e.backwards&&e._currentTime{const e=yt((l-t._offset)*t._speed,12),n=t._fps=a.duration&&(a.paused=!0,a.completed||(a.completed=!0,s||(a.onComplete(a),a._resolve(a))))}},Ht={},qt=(t,e,s)=>{if(s===i.TRANSFORM)return v.get(t)||t;if(s===i.CSS||s===i.ATTRIBUTE&&j(e)&&t in e.style){const e=Ht[t];if(e)return e;{const e=t?P(t):t;return Ht[t]=e,e}}return t},jt=t=>{if(t._hasChildren)Ct(t,jt,!0);else{const e=t;e.pause(),Ct(e,t=>{const s=t.property,r=t.target;if(r[h]){const n=r.style,o=t._inlineValue,a=q(o)||o===_;if(t._tweenType===i.TRANSFORM){const e=r[c];if(a?delete e[s]:e[s]=o,t._renderTransforms)if(Object.keys(e).length){let t=_;for(let s in e)t+=T[s]+e[s]+") ";n.transform=t}else n.removeProperty("transform")}else a?n.removeProperty(P(s)):n[s]=o;e._tail===t&&e.targets.forEach(t=>{t.getAttribute&&t.getAttribute("style")===_&&t.removeAttribute("style")})}})}return t};class Gt{constructor(t=0){this.deltaTime=0,this._currentTime=t,this._elapsedTime=t,this._startTime=t,this._lastTime=t,this._scheduledTime=0,this._frameDuration=yt(f/g,0),this._fps=g,this._speed=1,this._hasChildren=!1,this._head=null,this._tail=null}get fps(){return this._fps}set fps(t){const e=this._frameDuration,s=+t,i=s{let e=Qt.animation;return e||(e={duration:p,computeDeltaTime:S,_offset:0,_delay:0,_head:null,_tail:null},Qt.animation=e,Qt.update=()=>{t.forEach(t=>{for(let e in t){const s=t[e],i=s._head;if(i){const t=i._valueType,e=t===r.COMPLEX||t===r.COLOR?wt(i._fromNumbers):null;let n=i._fromNumber,o=s._tail;for(;o&&o!==i;){if(e)for(let t=0,s=o._numbers.length;tt?requestAnimationFrame:setImmediate)(),Kt=(()=>t?cancelAnimationFrame:clearImmediate)();class te extends Gt{constructor(t){super(t),this.useDefaultMainLoop=!0,this.pauseOnDocumentHidden=!0,this.defaults=L,this.paused=!0,this.reqId=0}update(){const t=this._currentTime=z();if(this.requestTick(t)){this.computeDeltaTime(t);const e=this._speed,s=this._fps;let i=this._head;for(;i;){const r=i._next;i.paused?(Et(this,i),this._hasChildren=!!this._tail,i._running=!1,i.completed&&!i._cancelled&&i.cancel()):Wt(i,(t-i._startTime)*i._speed*e,0,0,i._fpst.resetTime()),this.wake()}get speed(){return this._speed*(1===F.timeScale?1:f)}set speed(t){this._speed=t*F.timeScale,Ct(this,t=>t.speed=t._speed)}get timeUnit(){return 1===F.timeScale?"ms":"s"}set timeUnit(t){const e="s"===t,s=e?.001:1;if(F.timeScale!==s){F.timeScale=s,F.tickThreshold=200*s;const t=e?.001:f;this.defaults.duration*=t,this._speed*=t}}get precision(){return F.precision}set precision(t){F.precision=t}}const ee=(()=>{const e=new te(z());return t&&(M.engine=e,s.addEventListener("visibilitychange",()=>{e.pauseOnDocumentHidden&&(s.hidden?e.pause():e.resume())})),e})(),se=()=>{ee._head?(ee.reqId=Zt(se),ee.update()):ee.reqId=0},ie=()=>(Kt(ee.reqId),ee.reqId=0,ee),re={_rep:new WeakMap,_add:new Map},ne=(t,e,s="_rep")=>{const i=re[s];let r=i.get(t);return r||(r={},i.set(t,r)),r[e]?r[e]:r[e]={_head:null,_tail:null}},oe=(t,e)=>t._isOverridden||t._absoluteStartTime>e._absoluteStartTime,ae=t=>{t._isOverlapped=1,t._isOverridden=1,t._changeDuration=p,t._currentTime=p},he=(t,e)=>{const s=t._composition;if(s===o.replace){const s=t._absoluteStartTime;kt(e,t,oe,"_prevRep","_nextRep");const i=t._prevRep;if(i){const e=i.parent,r=i._absoluteStartTime+i._changeDuration;if(t.parent.id!==e.id&&e.iterationCount>1&&r+(e.duration-e.iterationDuration)>s){ae(i);let t=i._prevRep;for(;t&&t.parent.id===e.id;)ae(t),t=t._prevRep}const n=s-t._delay;if(r>n){const t=i._startTime,e=r-(t+i._updateDuration),s=yt(n-e-t,12);i._changeDuration=s,i._currentTime=s,i._isOverlapped=1,s{t._isOverlapped||(o=!1)}),o){const t=e.parent;if(t){let s=!0;Ct(t,t=>{t!==e&&Ct(t,t=>{t._isOverlapped||(s=!1)})}),s&&t.cancel()}else e.cancel()}}}else if(s===o.blend){const e=ne(t.target,t.property,"_add"),s=Jt(re._add);let i=e._head;i||(i={...t},i._composition=o.replace,i._updateDuration=p,i._startTime=0,i._numbers=wt(t._fromNumbers),i._number=0,i._next=null,i._prev=null,kt(e,i),kt(s,i));const r=t._toNumber;if(t._fromNumber=i._fromNumber-r,t._toNumber=0,t._numbers=wt(t._fromNumbers),t._number=0,i._fromNumber=r,t._toNumbers){const e=wt(t._toNumbers);e&&e.forEach((e,s)=>{t._fromNumbers[s]=i._fromNumbers[s]-e,t._toNumbers[s]=0}),i._fromNumbers=e}kt(e,t,null,"_prevAdd","_nextAdd")}return t},le=t=>{const e=t._composition;if(e!==o.none){const s=t.target,i=t.property,r=re._rep.get(s)[i];if(Et(r,t,"_prevRep","_nextRep"),e===o.blend){const e=re._add,r=e.get(s);if(!r)return;const n=r[i],o=Qt.animation;Et(n,t,"_prevAdd","_nextAdd");const a=n._head;if(a&&a===n._tail){Et(n,a,"_prevAdd","_nextAdd"),Et(o,a);let t=!0;for(let e in r)if(r[e]._head){t=!1;break}t&&e.delete(s)}}}return t},ce=(t,e,s)=>{let r=!1;return Ct(e,n=>{const o=n.target;if(t.includes(o)){const t=n.property,a=n._tweenType,h=qt(s,o,a);(!h||h&&h===t)&&(n.parent._tail===n&&n._tweenType===i.TRANSFORM&&n._prev&&n._prev._tweenType===i.TRANSFORM&&(n._prev._renderTransforms=1),Et(e,n),le(n),r=!0)}},!0),r},de=(t,e,s)=>{const i=e||ee;let r;if(i._hasChildren){let e=0;Ct(i,n=>{if(!n._hasChildren)if(r=ce(t,n,s),r&&!n._head)n.cancel(),Et(i,n);else{const t=n._offset+n._delay+n.duration;t>e&&(e=t)}n._head?de(t,n,s):n._hasChildren=!1},!0),H(i.iterationDuration)||(i.iterationDuration=e)}else r=ce(t,i,s);r&&!i._head&&(i._hasChildren=!1,i.cancel&&i.cancel())},ue=t=>(t.paused=!0,t.began=!1,t.completed=!1,t),pe=t=>t._cancelled?(t._hasChildren?Ct(t,pe):Ct(t,t=>{t._composition!==o.none&&he(t,ne(t.target,t.property))}),t._cancelled=0,t):t;let me=0;class fe extends Gt{constructor(t={},e=null,s=0){super(0);const{id:i,delay:r,duration:n,reversed:o,alternate:a,loop:h,loopDelay:l,autoplay:c,frameRate:d,playbackRate:u,onComplete:m,onLoop:f,onPause:g,onBegin:_,onBeforeUpdate:y,onUpdate:v}=t;A.current&&A.current.register(this);const b=e?0:ee._elapsedTime,T=e?e.defaults:F.defaults,w=W(r)||H(r)?T.delay:+r,x=W(n)||H(n)?1/0:+n,C=Lt(h,T.loop),E=Lt(l,T.loopDelay),k=!0===C||C===1/0||C<0?1/0:C+1;let $=0;e?$=s:(ee.reqId||ee.requestTick(z()),$=(ee._elapsedTime-ee._startTime)*F.timeScale),this.id=H(i)?++me:i,this.parent=e,this.duration=Tt((x+E)*k-E)||p,this.backwards=!1,this.paused=!0,this.began=!1,this.completed=!1,this.onBegin=_||T.onBegin,this.onBeforeUpdate=y||T.onBeforeUpdate,this.onUpdate=v||T.onUpdate,this.onLoop=f||T.onLoop,this.onPause=g||T.onPause,this.onComplete=m||T.onComplete,this.iterationDuration=x,this.iterationCount=k,this._autoplay=!e&&Lt(c,T.autoplay),this._offset=$,this._delay=w,this._loopDelay=E,this._iterationTime=0,this._currentIteration=0,this._resolve=S,this._running=!1,this._reversed=+Lt(o,T.reversed),this._reverse=this._reversed,this._cancelled=0,this._alternate=Lt(a,T.alternate),this._prev=null,this._next=null,this._elapsedTime=b,this._startTime=b,this._lastTime=b,this._fps=Lt(d,T.frameRate),this._speed=Lt(u,T.playbackRate)}get cancelled(){return!!this._cancelled}set cancelled(t){t?this.cancel():this.reset(!0).play()}get currentTime(){return gt(yt(this._currentTime,F.precision),-this._delay,this.duration)}set currentTime(t){const e=this.paused;this.pause().seek(+t),e||this.resume()}get iterationCurrentTime(){return yt(this._iterationTime,F.precision)}set iterationCurrentTime(t){this.currentTime=this.iterationDuration*this._currentIteration+t}get progress(){return gt(yt(this._currentTime/this.duration,10),0,1)}set progress(t){this.currentTime=this.duration*t}get iterationProgress(){return gt(yt(this._iterationTime/this.iterationDuration,10),0,1)}set iterationProgress(t){const e=this.iterationDuration;this.currentTime=e*this._currentIteration+e*t}get currentIteration(){return this._currentIteration}set currentIteration(t){this.currentTime=this.iterationDuration*gt(+t,0,this.iterationCount-1)}get reversed(){return!!this._reversed}set reversed(t){t?this.reverse():this.play()}get speed(){return super.speed}set speed(t){super.speed=t,this.resetTime()}reset(t=!1){return pe(this),this._reversed&&!this._reverse&&(this.reversed=!1),this._iterationTime=this.iterationDuration,Wt(this,0,1,~~t,n.FORCE),ue(this),this._hasChildren&&Ct(this,ue),this}init(t=!1){this.fps=this._fps,this.speed=this._speed,!t&&this._hasChildren&&Wt(this,this.duration,1,~~t,n.FORCE),this.reset(t);const e=this._autoplay;return!0===e?this.resume():e&&!H(e.linked)&&e.link(this),this}resetTime(){const t=1/(this._speed*ee._speed);return this._startTime=z()-(this._currentTime+this._delay)*t,this}pause(){return this.paused||(this.paused=!0,this.onPause(this)),this}resume(){return this.paused?(this.paused=!1,this.duration<=p&&!this._hasChildren?Wt(this,p,0,0,n.FORCE):(this._running||(kt(ee,this),ee._hasChildren=!0,this._running=!0),this.resetTime(),this._startTime-=12,ee.wake()),this):this}restart(){return this.reset().resume()}seek(t,e=0,s=0){pe(this),this.completed=!1;const i=this.paused;return this.paused=!0,Wt(this,t+this._delay,~~e,~~s,n.AUTO),i?this:this.resume()}alternate(){const t=this._reversed,e=this.iterationCount,s=this.iterationDuration,i=e===1/0?ct(m/s):e;return this._reversed=+(!this._alternate||i%2?!t:t),e===1/0?this.iterationProgress=this._reversed?1-this.iterationProgress:this.iterationProgress:this.seek(s*i-this._currentTime),this.resetTime(),this}play(){return this._reversed&&this.alternate(),this.resume()}reverse(){return this._reversed||this.alternate(),this.resume()}cancel(){return this._hasChildren?Ct(this,t=>t.cancel(),!0):Ct(this,le),this._cancelled=1,this.pause()}stretch(t){const e=this.duration,s=St(t);if(e===s)return this;const i=t/e,r=t<=p;return this.duration=r?p:s,this.iterationDuration=r?p:St(this.iterationDuration*i),this._offset*=i,this._delay*=i,this._loopDelay*=i,this}revert(){Wt(this,0,1,0,n.AUTO);const t=this._autoplay;return t&&t.linked&&t.linked===this&&t.revert(),this.cancel()}complete(){return this.seek(this.duration).cancel()}then(t=S){const e=this.then,s=()=>{this.then=null,t(this),this.then=e,this._resolve=S};return new Promise(t=>(this._resolve=()=>t(s()),this.completed&&this._resolve(),this))}}const ge=t=>new fe(t,null,0).init();function _e(t){const e=V(t)?A.root.querySelectorAll(t):t;if(e instanceof NodeList||e instanceof HTMLCollection)return e}function ye(e){if(q(e))return[];if(!t)return U(e)&&e.flat(1/0)||[e];if(U(e)){const t=e.flat(1/0),s=[];for(let e=0,i=t.length;e{const o=e.u,a=e.n;if(e.t===r.UNIT&&o===i)return e;const h=a+o+i,l=Te[h];if(H(l)||n){let r;if(o in be)r=a*be[o]/be[i];else{const e=100,n=t.cloneNode(),h=t.parentNode,l=h&&h!==s?h:s.body;l.appendChild(n);const c=n.style;c.width=e+o;const d=n.offsetWidth||e;c.width=e+i;const u=d/(n.offsetWidth||e);l.removeChild(n),r=u*a}e.n=r,Te[h]=r}else e.n=l;return e.t,e.u=i,e},we=t=>t,xe=(t=1.68)=>e=>it(e,+t),Ce={in:t=>e=>t(e),out:t=>e=>1-t(1-e),inOut:t=>e=>e<.5?t(2*e)/2:1-t(-2*e+2)/2,outIn:t=>e=>e<.5?(1-t(1-2*e))/2:(t(2*e-1)+1)/2},Ee=mt/2,ke=2*mt,$e={[_]:xe,Quad:xe(2),Cubic:xe(3),Quart:xe(4),Quint:xe(5),Sine:t=>1-ot(t*Ee),Circ:t=>1-rt(1-t*t),Expo:t=>t?it(2,10*t-10):0,Bounce:t=>{let e,s=4;for(;t<((e=it(2,--s))-1)/11;);return 1/it(4,3-s)-7.5625*it((3*e-2)/22-t,2)},Back:(t=1.7)=>e=>(+t+1)*e*e*e-+t*e*e,Elastic:(t=1,e=.3)=>{const s=gt(+t,1,10),i=gt(+e,p,2),r=i/ke*dt(1/s),n=ke/i;return t=>0===t||1===t?t:-s*it(2,-10*(1-t))*nt((1-t-r)*n)}},Be=(()=>{const t={linear:we,none:we};for(let e in Ce)for(let s in $e){const i=$e[s],r=Ce[e];t[e+s]=s===_||"Back"===s||"Elastic"===s?(t,e)=>r(i(t,e)):r(i)}return t})(),Oe={linear:we,none:we},Ne=t=>{if(Oe[t])return Oe[t];if(t.indexOf("(")<=-1){const e=Ce[t]||t.includes("Back")||t.includes("Elastic")?Be[t]():Be[t];return e?Oe[t]=e:we}{const e=t.slice(0,-1).split("("),s=Be[e[0]];return s?Oe[t]=s(...e[1].split(",")):we}},Re=["steps(","irregular(","linear(","cubicBezier("],De=t=>{if(V(t))for(let e=0,s=Re.length;e{const s={};if(U(t)){const e=[].concat(...t.map(t=>Object.keys(t))).filter(K);for(let i=0,r=e.length;i{const e={};for(let s in t){const i=t[s];K(s)?s===r&&(e.to=i):e[s]=i}return e});s[r]=n}}else{const i=Lt(e.duration,F.defaults.duration),r=Object.keys(t).map(e=>({o:parseFloat(e)/100,p:t[e]})).sort((t,e)=>t.o-e.o);r.forEach(t=>{const e=t.o,r=t.p;for(let t in r)if(K(t)){let n=s[t];n||(n=s[t]=[]);const o=e*i;let a=n.length,h=n[a-1];const l={to:r[t]};let c=0;for(let t=0;t=f?o.none:H(T)?w.composition:T,R=this._offset+(s?s._offset:0);E&&(y.parent=this);let D=NaN,L=NaN,A=0,M=0;for(let t=0;t2&&e?(Ue=[],c.forEach((t,e)=>{e?1===e?(Ie[1]=t,Ue.push(Ie)):Ue.push(t):Ie[0]=t})):Ue=c}else Pe[0]=c,Ue=Pe;let _=null,y=null,v=NaN,b=0,T=0;for(let t=Ue.length;T1?At($,e,n,u)/t:$),e,n,u),w=At(Lt(Xe.delay,T?0:B),e,n,u),x=At(Lt(Xe.composition,N),e,n,u),C=Y(x)?x:o[x],E=Xe.modifier||O,D=!H(d),L=!H(c),F=U(c),P=F||D&&L,I=y?b+w:w,z=yt(R+I,12);M||!D&&!F||(M=1);let V=y;if(C!==o.none){_||(_=ne(e,l));let t=_._head;for(;t&&!t._isOverridden&&t._absoluteStartTime<=z;)if(V=t,t=t._nextRep,t&&t._absoluteStartTime>=z)for(;t;)ae(t),t=t._nextRep}if(P?(Ut(F?At(c[0],e,n,u):d,Le),Ut(F?At(c[1],e,n,u,Me):c,Ae),Le.t===r.NUMBER&&(V?V._valueType===r.UNIT&&(Le.t=r.UNIT,Le.u=V._unit):(Ut(Pt(e,l,h,Fe),Yt),Yt.t===r.UNIT&&(Le.t=r.UNIT,Le.u=Yt.u)))):(L?Ut(c,Ae):y?Xt(y,Ae):Ut(s&&V&&V.parent.parent===s?V._value:Pt(e,l,h,Fe),Ae),D?Ut(d,Le):y?Xt(y,Le):Ut(s&&V&&V.parent.parent===s?V._value:Pt(e,l,h,Fe),Le)),Le.o&&(Le.n=It(V?V._toNumber:Ut(Pt(e,l,h,Fe),Yt).n,Le.n,Le.o)),Ae.o&&(Ae.n=It(Le.n,Ae.n,Ae.o)),Le.t!==Ae.t)if(Le.t===r.COMPLEX||Ae.t===r.COMPLEX){const t=Le.t===r.COMPLEX?Le:Ae,e=Le.t===r.COMPLEX?Ae:Le;e.t=r.COMPLEX,e.s=wt(t.s),e.d=t.d.map(()=>e.n)}else if(Le.t===r.UNIT||Ae.t===r.UNIT){const t=Le.t===r.UNIT?Le:Ae,e=Le.t===r.UNIT?Ae:Le;e.t=r.UNIT,e.u=t.u}else if(Le.t===r.COLOR||Ae.t===r.COLOR){const t=Le.t===r.COLOR?Le:Ae,e=Le.t===r.COLOR?Ae:Le;e.t=r.COLOR,e.s=t.s,e.d=[0,0,0,1]}if(Le.u!==Ae.u){let t=Ae.u?Le:Ae;t=Se(e,t,Ae.u?Ae.u:Le.u,!1)}if(Ae.d&&Le.d&&Ae.d.length!==Le.d.length){const t=Le.d.length>Ae.d.length?Le:Ae,e=t===Le?Ae:Le;e.d=t.d.map((t,s)=>H(e.d[s])?0:e.d[s]),e.s=wt(t.s)}const W=yt(+S||p,12);let j=Fe[l];q(j)||(Fe[l]=null);const G={parent:this,id:Ye++,property:l,target:e,_value:null,_func:Me.func,_ease:De(g),_fromNumbers:wt(Le.d),_toNumbers:wt(Ae.d),_strings:wt(Ae.s),_fromNumber:Le.n,_toNumber:Ae.n,_numbers:wt(Le.d),_number:Le.n,_unit:Ae.u,_modifier:E,_currentTime:0,_startTime:I,_delay:+w,_updateDuration:W,_changeDuration:W,_absoluteStartTime:z,_tweenType:h,_valueType:Ae.t,_composition:C,_isOverlapped:0,_isOverridden:0,_renderTransforms:0,_inlineValue:j,_prevRep:null,_nextRep:null,_prevAdd:null,_nextAdd:null,_prev:null,_next:null};C!==o.none&&he(G,_),isNaN(v)&&(v=G._startTime),b=yt(I+W,12),y=G,A++,kt(this,G)}(isNaN(L)||vD)&&(D=b),h===i.TRANSFORM&&(f=A-T,g=A)}if(!isNaN(f)){let t=0;Ct(this,e=>{t>=f&&t{t.id===e.id&&(t._renderTransforms=1)})),t++})}}d||console.warn("No target found. Make sure the element you're trying to animate is accessible before creating your animation."),L?(Ct(this,t=>{t._startTime-t._delay||(t._delay-=L),t._startTime-=L}),D-=L):L=0,D||(D=p,this.iterationCount=0),this.targets=c,this.duration=D===p?p:Tt((D+this._loopDelay)*this.iterationCount-this._loopDelay)||p,this.onRender=S||w.onRender,this._ease=C,this._delay=L,this.iterationDuration=D,!this._autoplay&&M&&this.onRender(this)}stretch(t){const e=this.duration;if(e===St(t))return this;const s=t/e;return Ct(this,t=>{t._updateDuration=St(t._updateDuration*s),t._changeDuration=St(t._changeDuration*s),t._currentTime*=s,t._startTime*=s,t._absoluteStartTime*=s}),super.stretch(t)}refresh(){return Ct(this,t=>{const e=t._func;if(e){const s=Pt(t.target,t.property,t._tweenType);Ut(s,Yt),Ut(e(),Ae),t._fromNumbers=wt(Yt.d),t._fromNumber=Yt.n,t._toNumbers=wt(Ae.d),t._strings=wt(Ae.s),t._toNumber=Ae.o?It(Yt.n,Ae.n,Ae.o):Ae.n}}),this.duration===p&&this.restart(),this}revert(){return super.revert(),jt(this)}then(t){return super.then(t)}}const He=(t,e)=>new We(t,e,null,0,!1).init(),qe={_head:null,_tail:null},je=(t,e,s)=>{let i,r=qe._head;for(;r;){const n=r._next,o=r.$el===t,a=!e||r.property===e,h=!s||r.parent===s;if(o&&a&&h){i=r.animation;try{i.commitStyles()}catch{}i.cancel(),Et(qe,r);const t=r.parent;t&&(t._completed++,t.animations.length===t._completed&&(t.completed=!0,t.paused=!0,t.muteCallbacks||(t.onComplete(t),t._resolve(t))))}r=n}return i},Ge=(t,e,s,i,r)=>{const n=e.animate(i,r),o=r.delay+ +r.duration*r.iterations;n.playbackRate=t._speed,t.paused&&n.pause(),t.duration{je(e,s,t)};return n.oncancel=a,n.onremove=a,t.persist||(n.onfinish=a),n};function Qe(t,e,s){const i=ve(t);if(!i.length)return;const[n]=i,o=Ft(n,e),a=qt(e,n,o);let h=Pt(n,a);if(H(s))return h;if(Ut(h,Yt),Yt.t===r.NUMBER||Yt.t===r.UNIT){if(!1===s)return Yt.n;{const t=Se(n,Yt,s,!1);return`${yt(t.n,F.precision)}${t.u}`}}}const Je=(t,e)=>{if(!H(e))return e.duration=p,e.composition=Lt(e.composition,o.none),new We(t,e,null,0,!0).resume()},Ze=(t,e,s)=>{const i=ye(t);for(let t=0,r=i.length;t{if(I(e,"<")){const s="<"===e[1],i=t._tail,r=i?i._offset+i._delay:0;return s?r:r+i.duration}},ts=(t,e)=>{let s=t.iterationDuration;if(s===p&&(s=0),H(e))return s;if(Y(+e))return+e;const i=e,r=t?t.labels:null,n=!q(r),o=Ke(t,i),a=!H(o),h=R.exec(i);if(h){const t=h[0],e=i.split(t),l=n&&e[0]?r[e[0]]:s,c=a?o:n?l:s,d=+e[1];return It(c,d,t[0])}return a?o:n?H(r[i])?s:r[i]:s};function es(t){return Tt((t.iterationDuration+t._loopDelay)*t.iterationCount-t._loopDelay)||p}function ss(t,e,s,i,r,o){const a=Y(t.duration)&&t.duration<=p?s-p:s;Wt(e,a,1,1,n.AUTO);const h=i?new We(i,t,e,a,!1,r,o):new fe(t,e,a);return h.init(!0),kt(e,h),Ct(e,t=>{const s=t._offset+t._delay+t.duration;s>e.iterationDuration&&(e.iterationDuration=s)}),e.duration=es(e),e}class is extends fe{constructor(t={}){super(t,null,0),this.duration=0,this.labels={};const e=t.defaults,s=F.defaults;this.defaults=e?xt(e,s):s,this.onRender=t.onRender||s.onRender;const i=Lt(t.playbackEase,s.playbackEase);this._ease=i?De(i):null,this.iterationDuration=0}add(t,e,s){const i=X(e),r=X(t);if(i||r){if(this._hasChildren=!0,i){const i=e;if(W(s)){const e=s,r=ye(t),n=this.duration,o=this.iterationDuration,a=i.id;let h=0;const l=r.length;r.forEach(t=>{const s={...i};this.duration=n,this.iterationDuration=o,H(a)||(s.id=a+"-"+h),ss(s,this,ts(this,e(t,h,l,this)),t,h,l),h++})}else ss(i,this,ts(this,s),t)}else ss(t,this,ts(this,e));return this.init(!0)}}sync(t,e){if(H(t)||t&&H(t.pause))return this;t.pause();const s=+(t.effect?t.effect.getTiming().duration:t.duration);return this.add(t,{currentTime:[0,s],duration:s,ease:"linear"},e)}set(t,e,s){return H(e)?this:(e.duration=p,e.composition=o.replace,this.add(t,e,s))}call(t,e){return H(t)||t&&!W(t)?this:this.add({duration:0,onComplete:()=>t(this)},e)}label(t,e){return H(t)||t&&!V(t)||(this.labels[t]=ts(this,e)),this}remove(t,e){return de(ye(t),this,e),this}stretch(t){const e=this.duration;if(e===St(t))return this;const s=t/e,i=this.labels;Ct(this,t=>t.stretch(t.duration*s));for(let t in i)i[t]*=s;return super.stretch(t)}refresh(){return Ct(this,t=>{t.refresh&&t.refresh()}),this}revert(){return super.revert(),Ct(this,t=>t.revert,!0),jt(this)}then(t){return super.then(t)}}const rs=t=>new is(t).init();class ns{constructor(t,e){A.current&&A.current.register(this);const s=()=>{if(this.callbacks.completed)return;let t=!0;for(let e in this.animations)if(!this.animations[e].paused&&t){t=!1;break}t&&this.callbacks.complete()},i={onBegin:()=>{this.callbacks.completed&&this.callbacks.reset(),this.callbacks.play()},onComplete:s,onPause:s},r={v:1,autoplay:!1},n={};if(this.targets=[],this.animations={},this.callbacks=null,!H(t)&&!H(e)){for(let t in e){const s=e[t];K(t)?n[t]=s:I(t,"on")?r[t]=s:i[t]=s}this.callbacks=new We({v:0},r);for(let e in n){const s=n[e],r=X(s);let a={},h="+=0";if(r){const t=s.unit;V(t)&&(h+=t)}else a.duration=s;a[e]=r?xt({to:h},s):h;const l=xt(i,a);l.composition=o.replace,l.autoplay=!1;const c=this.animations[e]=new We(t,l,null,0,!1).init();this.targets.length||this.targets.push(...c.targets),this[e]=(t,e,s)=>{const i=c._head;if(H(t)&&i){const t=i._numbers;return t&&t.length?t:i._modifier(i._number)}return Ct(c,e=>{if(U(t))for(let s=0,i=t.length;snew ns(t,e),as=(t,e)=>(+t).toFixed(e),hs=(t,e,s)=>`${t}`.padStart(e,s),ls=(t,e,s)=>`${t}`.padEnd(e,s),cs=(t,e,s)=>((t-e)%(s-e)+(s-e))%(s-e)+e,ds=(t,e,s,i,r)=>i+(t-e)/(s-e)*(r-i),us=t=>t*Math.PI/180,ps=t=>180*t/Math.PI,ms=(t,e,s,i)=>i?1===i?e:bt(t,e,1-Math.exp(-i*s*.1)):t;var fs=Object.freeze({__proto__:null,clamp:gt,damp:ms,degToRad:us,lerp:bt,mapRange:ds,padEnd:ls,padStart:hs,radToDeg:ps,round:yt,roundPad:as,snap:vt,wrap:cs});const gs=10*f;class _s{constructor(t={}){const e=!H(t.bounce)||!H(t.duration);this.timeStep=.02,this.restThreshold=5e-4,this.restDuration=200,this.maxDuration=6e4,this.maxRestSteps=this.restDuration/this.timeStep/f,this.maxIterations=this.maxDuration/this.timeStep/f,this.bn=gt(Lt(t.bounce,.5),-1,1),this.pd=gt(Lt(t.duration,628),10*F.timeScale,gs*F.timeScale),this.m=gt(Lt(t.mass,1),1,gs),this.s=gt(Lt(t.stiffness,100),p,gs),this.d=gt(Lt(t.damping,10),p,gs),this.v=gt(Lt(t.velocity,0),-1e4,gs),this.w0=0,this.zeta=0,this.wd=0,this.b=0,this.completed=!1,this.solverDuration=0,this.settlingDuration=0,this.parent=null,this.onComplete=t.onComplete||S,e&&this.calculateSDFromBD(),this.compute(),this.ease=t=>{const e=t*this.settlingDuration,s=this.completed,i=this.pd;return e>=i&&!s&&(this.completed=!0,this.onComplete(this.parent)),e=0?this.d=4*(1-this.bn)*mt/t:this.d=4*mt/(t*(1+this.bn)),this.s=yt(gt(this.s,p,gs),3),this.d=yt(gt(this.d,p,300),3)}calculateBDFromSD(){const t=2*mt/rt(this.s);this.pd=t*(1===F.timeScale?f:1);const e=this.d/(2*rt(this.s));this.bn=e<=1?1-this.d*t/(4*mt):4*mt/(this.d*t)-1,this.bn=yt(gt(this.bn,-1,1),3),this.pd=yt(gt(this.pd,10*F.timeScale,gs*F.timeScale),3)}compute(){const{maxRestSteps:t,maxIterations:e,restThreshold:s,timeStep:i,m:r,d:n,s:o,v:a}=this,h=this.w0=gt(rt(o/r),p,f),l=this.zeta=n/(2*rt(o*r));l<1?(this.wd=h*rt(1-l*l),this.b=(l*h-a)/this.wd):1===l?(this.wd=0,this.b=-a+h):(this.wd=h*rt(l*l-1),this.b=(l*h-a)/this.wd);let c=0,d=0,u=0;for(;d<=t&&u<=e;)at(1-this.solve(c))new _s(t),vs=t=>(console.warn("createSpring() is deprecated use spring() instead"),new _s(t)),bs=t=>{t.cancelable&&t.preventDefault()};class Ts{constructor(t){this.el=t,this.zIndex=0,this.parentElement=null,this.classList={add:S,remove:S}}get x(){return this.el.x||0}set x(t){this.el.x=t}get y(){return this.el.y||0}set y(t){this.el.y=t}get width(){return this.el.width||0}set width(t){this.el.width=t}get height(){return this.el.height||0}set height(t){this.el.height=t}getBoundingClientRect(){return{top:this.y,right:this.x,bottom:this.y+this.height,left:this.x+this.width}}}class Ss{constructor(t){this.$el=t,this.inlineTransforms=[],this.point=new DOMPoint,this.inversedMatrix=this.getMatrix().inverse()}normalizePoint(t,e){return this.point.x=t,this.point.y=e,this.point.matrixTransform(this.inversedMatrix)}traverseUp(t){let e=this.$el.parentElement,i=0;for(;e&&e!==s;)t(e,i),e=e.parentElement,i++}getMatrix(){const t=new DOMMatrix;return this.traverseUp(e=>{const s=getComputedStyle(e).transform;if(s){const e=new DOMMatrix(s);t.preMultiplySelf(e)}}),t}remove(){this.traverseUp((t,e)=>{this.inlineTransforms[e]=t.style.transform,t.style.transform="none"})}revert(){this.traverseUp((t,e)=>{const s=this.inlineTransforms[e];""===s?t.style.removeProperty("transform"):t.style.transform=s})}}const ws=(t,e)=>t&&W(t)?t(e):t;let xs=0;class Cs{constructor(t,i={}){if(!t)return;A.current&&A.current.register(this);const r=i.x,n=i.y,o=i.trigger,a=i.modifier,h=i.releaseEase,l=h&&De(h),c=!H(h)&&!H(h.ease),d=X(r)&&!H(r.mapTo)?r.mapTo:"translateX",u=X(n)&&!H(n.mapTo)?n.mapTo:"translateY",p=ws(i.container,this);this.containerArray=U(p)?p:null,this.$container=p&&!this.containerArray?ye(p)[0]:s.body,this.useWin=this.$container===s.body,this.$scrollContainer=this.useWin?e:this.$container,this.$target=X(t)?new Ts(t):ye(t)[0],this.$trigger=ye(o||t)[0],this.fixed="fixed"===Qe(this.$target,"position"),this.isFinePointer=!0,this.containerPadding=[0,0,0,0],this.containerFriction=0,this.releaseContainerFriction=0,this.snapX=0,this.snapY=0,this.scrollSpeed=0,this.scrollThreshold=0,this.dragSpeed=0,this.dragThreshold=3,this.maxVelocity=0,this.minVelocity=0,this.velocityMultiplier=0,this.cursor=!1,this.releaseXSpring=c?h:ys({mass:Lt(i.releaseMass,1),stiffness:Lt(i.releaseStiffness,80),damping:Lt(i.releaseDamping,20)}),this.releaseYSpring=c?h:ys({mass:Lt(i.releaseMass,1),stiffness:Lt(i.releaseStiffness,80),damping:Lt(i.releaseDamping,20)}),this.releaseEase=l||Be.outQuint,this.hasReleaseSpring=c,this.onGrab=i.onGrab||S,this.onDrag=i.onDrag||S,this.onRelease=i.onRelease||S,this.onUpdate=i.onUpdate||S,this.onSettle=i.onSettle||S,this.onSnap=i.onSnap||S,this.onResize=i.onResize||S,this.onAfterResize=i.onAfterResize||S,this.disabled=[0,0];const f={};if(a&&(f.modifier=a),H(r)||!0===r)f[d]=0;else if(X(r)){const t=r,e={};t.modifier&&(e.modifier=t.modifier),t.composition&&(e.composition=t.composition),f[d]=e}else!1===r&&(f[d]=0,this.disabled[0]=1);if(H(n)||!0===n)f[u]=0;else if(X(n)){const t=n,e={};t.modifier&&(e.modifier=t.modifier),t.composition&&(e.composition=t.composition),f[u]=e}else!1===n&&(f[u]=0,this.disabled[1]=1);this.animate=new ns(this.$target,f),this.xProp=d,this.yProp=u,this.destX=0,this.destY=0,this.deltaX=0,this.deltaY=0,this.scroll={x:0,y:0},this.coords=[this.x,this.y,0,0],this.snapped=[0,0],this.pointer=[0,0,0,0,0,0,0,0],this.scrollView=[0,0],this.dragArea=[0,0,0,0],this.containerBounds=[-m,m,m,-m],this.scrollBounds=[0,0,0,0],this.targetBounds=[0,0,0,0],this.window=[0,0],this.velocityStack=[0,0,0],this.velocityStackIndex=0,this.velocityTime=z(),this.velocity=0,this.angle=0,this.cursorStyles=null,this.triggerStyles=null,this.bodyStyles=null,this.targetStyles=null,this.touchActionStyles=null,this.transforms=new Ss(this.$target),this.overshootCoords={x:0,y:0},this.overshootTicker=new fe({autoplay:!1,onUpdate:()=>{this.updated=!0,this.manual=!0,this.disabled[0]||this.animate[this.xProp](this.overshootCoords.x,1),this.disabled[1]||this.animate[this.yProp](this.overshootCoords.y,1)},onComplete:()=>{this.manual=!1,this.disabled[0]||this.animate[this.xProp](this.overshootCoords.x,0),this.disabled[1]||this.animate[this.yProp](this.overshootCoords.y,0)}},null,0).init(),this.updateTicker=new fe({autoplay:!1,onUpdate:()=>this.update()},null,0).init(),this.contained=!H(p),this.manual=!1,this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.enabled=!1,this.initialized=!1,this.activeProp=this.disabled[1]?d:u,this.animate.callbacks.onRender=()=>{const t=this.updated,e=!(this.grabbed&&t)&&this.released,s=this.x,i=this.y,r=s-this.coords[2],n=i-this.coords[3];this.deltaX=r,this.deltaY=n,this.coords[2]=s,this.coords[3]=i,t&&(r||n)&&this.onUpdate(this),e?(this.computeVelocity(r,n),this.angle=pt(n,r)):this.updated=!1},this.animate.callbacks.onComplete=()=>{!this.grabbed&&this.released&&(this.released=!1),this.manual||(this.deltaX=0,this.deltaY=0,this.velocity=0,this.velocityStack[0]=0,this.velocityStack[1]=0,this.velocityStack[2]=0,this.velocityStackIndex=0,this.onSettle(this))},this.resizeTicker=new fe({autoplay:!1,duration:150*F.timeScale,onComplete:()=>{this.onResize(this),this.refresh(),this.onAfterResize(this)}}).init(),this.parameters=i,this.resizeObserver=new ResizeObserver(()=>{this.initialized?this.resizeTicker.restart():this.initialized=!0}),this.enable(),this.refresh(),this.resizeObserver.observe(this.$container),X(t)||this.resizeObserver.observe(this.$target)}computeVelocity(t,e){const s=this.velocityTime,i=z(),r=i-s;if(r<17)return this.velocity;this.velocityTime=i;const n=this.velocityStack,o=this.velocityMultiplier,a=this.minVelocity,h=this.maxVelocity,l=this.velocityStackIndex;n[l]=yt(gt(rt(t*t+e*e)/r*o,a,h),5);const c=ut(n[0],n[1],n[2]);return this.velocity=c,this.velocityStackIndex=(l+1)%3,c}setX(t,e=!1){if(this.disabled[0])return;const s=yt(t,5);return this.overshootTicker.pause(),this.manual=!0,this.updated=!e,this.destX=s,this.snapped[0]=vt(s,this.snapX),this.animate[this.xProp](s,0),this.manual=!1,this}setY(t,e=!1){if(this.disabled[1])return;const s=yt(t,5);return this.overshootTicker.pause(),this.manual=!0,this.updated=!e,this.destY=s,this.snapped[1]=vt(s,this.snapY),this.animate[this.yProp](s,0),this.manual=!1,this}get x(){return yt(this.animate[this.xProp](),F.precision)}set x(t){this.setX(t,!1)}get y(){return yt(this.animate[this.yProp](),F.precision)}set y(t){this.setY(t,!1)}get progressX(){return ds(this.x,this.containerBounds[3],this.containerBounds[1],0,1)}set progressX(t){this.setX(ds(t,0,1,this.containerBounds[3],this.containerBounds[1]),!1)}get progressY(){return ds(this.y,this.containerBounds[0],this.containerBounds[2],0,1)}set progressY(t){this.setY(ds(t,0,1,this.containerBounds[0],this.containerBounds[2]),!1)}updateScrollCoords(){const t=yt(this.useWin?e.scrollX:this.$container.scrollLeft,0),s=yt(this.useWin?e.scrollY:this.$container.scrollTop,0),[i,r,n,o]=this.containerPadding,a=this.scrollThreshold;this.scroll.x=t,this.scroll.y=s,this.scrollBounds[0]=s-this.targetBounds[0]+i-a,this.scrollBounds[1]=t-this.targetBounds[1]-r+a,this.scrollBounds[2]=s-this.targetBounds[2]-n+a,this.scrollBounds[3]=t-this.targetBounds[3]+o-a}updateBoundingValues(){const t=this.$container;if(!t)return;const i=this.x,r=this.y,n=this.coords[2],o=this.coords[3];this.coords[2]=0,this.coords[3]=0,this.setX(0,!0),this.setY(0,!0),this.transforms.remove();const a=this.window[0]=e.innerWidth,h=this.window[1]=e.innerHeight,l=this.useWin,c=t.scrollWidth,d=t.scrollHeight,u=this.fixed,p=t.getBoundingClientRect(),[m,f,g,_]=this.containerPadding;this.dragArea[0]=l?0:p.left,this.dragArea[1]=l?0:p.top,this.scrollView[0]=l?gt(c,a,c):c,this.scrollView[1]=l?gt(d,h,d):d,this.updateScrollCoords();const{width:y,height:v,left:b,top:T,right:S,bottom:w}=t.getBoundingClientRect();this.dragArea[2]=yt(l?gt(y,a,a):y,0),this.dragArea[3]=yt(l?gt(v,h,h):v,0);const x=Qe(t,"overflow"),C="visible"===x,E="hidden"===x;if(this.canScroll=!u&&this.contained&&(t===s.body&&C||!E&&!C)&&(c>this.dragArea[2]+_-f||d>this.dragArea[3]+m-g)&&(!this.containerArray||this.containerArray&&!U(this.containerArray)),this.contained){const e=this.scroll.x,s=this.scroll.y,i=this.canScroll,r=this.$target.getBoundingClientRect(),n=i?l?0:t.scrollLeft:0,o=i?l?0:t.scrollTop:0,c=i?this.scrollView[0]-n-y:0,d=i?this.scrollView[1]-o-v:0;this.targetBounds[0]=yt(r.top+s-(l?0:T),0),this.targetBounds[1]=yt(r.right+e-(l?a:S),0),this.targetBounds[2]=yt(r.bottom+s-(l?h:w),0),this.targetBounds[3]=yt(r.left+e-(l?0:b),0),this.containerArray?(this.containerBounds[0]=this.containerArray[0]+m,this.containerBounds[1]=this.containerArray[1]-f,this.containerBounds[2]=this.containerArray[2]-g,this.containerBounds[3]=this.containerArray[3]+_):(this.containerBounds[0]=-yt(r.top-(u?gt(T,0,h):T)+o-m,0),this.containerBounds[1]=-yt(r.right-(u?gt(S,0,a):S)-c+f,0),this.containerBounds[2]=-yt(r.bottom-(u?gt(w,0,h):w)-d+g,0),this.containerBounds[3]=-yt(r.left-(u?gt(b,0,a):b)+n-_,0))}this.transforms.revert(),this.coords[2]=n,this.coords[3]=o,this.setX(i,!0),this.setY(r,!0)}isOutOfBounds(t,e,s){if(!this.contained)return 0;const[i,r,n,o]=t,[a,h]=this.disabled,l=!a&&er,c=!h&&sn;return l&&!c?1:!l&&c?2:l&&c?3:0}refresh(){const t=this.parameters,i=t.x,r=t.y,n=ws(t.container,this),o=ws(t.containerPadding,this)||0,a=U(o)?o:[o,o,o,o],h=this.x,l=this.y,c=ws(t.cursor,this),d={onHover:"grab",onGrab:"grabbing"};if(c){const{onHover:t,onGrab:e}=c;t&&(d.onHover=t),e&&(d.onGrab=e)}const u=ws(t.dragThreshold,this),p={mouse:3,touch:7};if(Y(u))p.mouse=u,p.touch=u;else if(u){const{mouse:t,touch:e}=u;H(t)||(p.mouse=t),H(e)||(p.touch=e)}this.containerArray=U(n)?n:null,this.$container=n&&!this.containerArray?ye(n)[0]:s.body,this.useWin=this.$container===s.body,this.$scrollContainer=this.useWin?e:this.$container,this.isFinePointer=matchMedia("(pointer:fine)").matches,this.containerPadding=Lt(a,[0,0,0,0]),this.containerFriction=gt(Lt(ws(t.containerFriction,this),.8),0,1),this.releaseContainerFriction=gt(Lt(ws(t.releaseContainerFriction,this),this.containerFriction),0,1),this.snapX=ws(X(i)&&!H(i.snap)?i.snap:t.snap,this),this.snapY=ws(X(r)&&!H(r.snap)?r.snap:t.snap,this),this.scrollSpeed=Lt(ws(t.scrollSpeed,this),1.5),this.scrollThreshold=Lt(ws(t.scrollThreshold,this),20),this.dragSpeed=Lt(ws(t.dragSpeed,this),1),this.dragThreshold=this.isFinePointer?p.mouse:p.touch,this.minVelocity=Lt(ws(t.minVelocity,this),0),this.maxVelocity=Lt(ws(t.maxVelocity,this),50),this.velocityMultiplier=Lt(ws(t.velocityMultiplier,this),1),this.cursor=!1!==c&&d,this.updateBoundingValues();const[m,f,g,_]=this.containerBounds;this.setX(gt(h,_,f),!0),this.setY(gt(l,m,g),!0)}update(){if(this.updateScrollCoords(),this.canScroll){const[t,e,s,i]=this.containerPadding,[r,n]=this.scrollView,o=this.dragArea[2],a=this.dragArea[3],h=this.scroll.x,l=this.scroll.y,c=this.$container.scrollWidth,d=this.$container.scrollHeight,u=this.useWin?gt(c,this.window[0],c):c,p=this.useWin?gt(d,this.window[1],d):d,m=r-u,f=n-p;this.dragged&&m>0&&(this.coords[0]-=m,this.scrollView[0]=u),this.dragged&&f>0&&(this.coords[1]-=f,this.scrollView[1]=p);const g=10*this.scrollSpeed,_=this.scrollThreshold,[y,v]=this.coords,[b,T,S,w]=this.scrollBounds,x=yt(gt((v-b+t)/_,-1,0)*g,0),C=yt(gt((y-T-e)/_,0,1)*g,0),E=yt(gt((v-S-s)/_,0,1)*g,0),k=yt(gt((y-w+i)/_,-1,0)*g,0);if(x||E||k||C){const[t,e]=this.disabled;let s=h,i=l;t||(s=yt(gt(h+(k||C),0,r-o),0),this.coords[0]-=h-s),e||(i=yt(gt(l+(x||E),0,n-a),0),this.coords[1]-=l-i),this.useWin?this.$scrollContainer.scrollBy(-(h-s),-(l-i)):this.$scrollContainer.scrollTo(s,i)}}const[t,e,s,i]=this.containerBounds,[r,n,o,a,h,l]=this.pointer;this.coords[0]+=(r-h)*this.dragSpeed,this.coords[1]+=(n-l)*this.dragSpeed,this.pointer[4]=r,this.pointer[5]=n;const[c,d]=this.coords,[u,p]=this.snapped,m=(1-this.containerFriction)*this.dragSpeed;this.setX(c>e?e+(c-e)*m:cs?s+(d-s)*m:d{this.canScroll=!1,this.$scrollContainer.scrollTo(n.x,n.y)}}).init().then(()=>{this.canScroll=a})}return this}handleHover(){this.isFinePointer&&this.cursor&&!this.cursorStyles&&(this.cursorStyles=Je(this.$trigger,{cursor:this.cursor.onHover}))}animateInView(t,e=0,s=Be.inOutQuad){this.stop(),this.updateBoundingValues();const i=this.x,r=this.y,[n,o,a,h]=this.containerPadding,l=this.scroll.y-this.targetBounds[0]+n+e,c=this.scroll.x-this.targetBounds[1]-o-e,d=this.scroll.y-this.targetBounds[2]-a-e,u=this.scroll.x-this.targetBounds[3]+h+e,p=this.isOutOfBounds([l,c,d,u],i,r);if(p){const[e,n]=this.disabled,o=gt(vt(i,this.snapX),u,c),a=gt(vt(r,this.snapY),l,d),h=H(t)?350*F.timeScale:t;e||1!==p&&3!==p||this.animate[this.xProp](o,h,s),n||2!==p&&3!==p||this.animate[this.yProp](a,h,s)}return this}handleDown(t){const e=t.target;if(this.grabbed||"range"===e.type)return;t.stopPropagation(),this.grabbed=!0,this.released=!1,this.stop(),this.updateBoundingValues();const i=t.changedTouches,r=i?i[0].clientX:t.clientX,n=i?i[0].clientY:t.clientY,{x:o,y:a}=this.transforms.normalizePoint(r,n),[h,l,c,d]=this.containerBounds,u=(1-this.containerFriction)*this.dragSpeed,p=this.x,m=this.y;this.coords[0]=this.coords[2]=u?p>l?l+(p-l)/u:pc?c+(m-c)/u:mxs?f:xs)+1,this.targetStyles=Je(this.$target,{zIndex:xs}),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.cursorStyles&&(this.cursorStyles.revert(),this.cursorStyles=null),this.isFinePointer&&this.cursor&&(this.bodyStyles=Je(s.body,{cursor:this.cursor.onGrab})),this.scrollInView(100,0,Be.out(3)),this.onGrab(this),s.addEventListener("touchmove",this),s.addEventListener("touchend",this),s.addEventListener("touchcancel",this),s.addEventListener("mousemove",this),s.addEventListener("mouseup",this),s.addEventListener("selectstart",this)}handleMove(t){if(!this.grabbed)return;const e=t.changedTouches,s=e?e[0].clientX:t.clientX,i=e?e[0].clientY:t.clientY,{x:r,y:n}=this.transforms.normalizePoint(s,i),o=r-this.pointer[6],a=n-this.pointer[7];let h=t.target,l=!1,c=!1,d=!1;for(;e&&h&&h!==this.$trigger;){const t=Qe(h,"overflow-y");if("hidden"!==t&&"visible"!==t){const{scrollTop:t,scrollHeight:e,clientHeight:s}=h;if(e>s){d=!0,l=t<=3,c=t>=e-s-3;break}}h=h.parentElement}d&&(!l&&!c||l&&a<0||c&&a>0)?(this.pointer[0]=r,this.pointer[1]=n,this.pointer[2]=r,this.pointer[3]=n,this.pointer[4]=r,this.pointer[5]=n,this.pointer[6]=r,this.pointer[7]=n):(bs(t),this.triggerStyles||(this.triggerStyles=Je(this.$trigger,{pointerEvents:"none"})),this.$trigger.addEventListener("touchstart",bs,{passive:!1}),this.$trigger.addEventListener("touchmove",bs,{passive:!1}),this.$trigger.addEventListener("touchend",bs),(this.dragged||!this.disabled[0]&&at(o)>this.dragThreshold||!this.disabled[1]&&at(a)>this.dragThreshold)&&(this.updateTicker.resume(),this.pointer[2]=this.pointer[0],this.pointer[3]=this.pointer[1],this.pointer[0]=r,this.pointer[1]=n,this.dragged=!0,this.released=!1,this.onDrag(this)))}handleUp(){if(!this.grabbed)return;this.updateTicker.pause(),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.bodyStyles&&(this.bodyStyles.revert(),this.bodyStyles=null);const[t,e]=this.disabled,[i,r,n,a,h,l]=this.pointer,[c,d,u,p]=this.containerBounds,[m,f]=this.snapped,g=this.releaseXSpring,_=this.releaseYSpring,y=this.releaseEase,v=this.hasReleaseSpring,b=this.overshootCoords,T=this.x,S=this.y,w=this.computeVelocity(i-h,r-l),x=this.angle=pt(r-a,i-n),C=150*w,E=(1-this.releaseContainerFriction)*this.dragSpeed,k=T+ot(x)*C,$=S+nt(x)*C,B=k>d?d+(k-d)*E:ku?u+($-u)*E:$d?-1:1:TI&&(I=L)}if(!e){const e=R===u?S>u?-1:1:SI&&(I=A)}if(!v&&D&&E&&(L||A)){const t=o.blend;new We(b,{x:{to:B,duration:.65*L},y:{to:O,duration:.65*A},ease:y,composition:t}).init(),new We(b,{x:{to:N,duration:L},y:{to:R,duration:A},ease:y,composition:t}).init(),this.overshootTicker.stretch(ut(L,A)).restart()}else t||this.animate[this.xProp](N,L,M),e||this.animate[this.yProp](R,A,P);this.scrollInView(I,this.scrollThreshold,y);let z=!1;N!==m&&(this.snapped[0]=N,this.snapX&&(z=!0)),R!==f&&this.snapY&&(this.snapped[1]=R,this.snapY&&(z=!0)),z&&this.onSnap(this),this.grabbed=!1,this.dragged=!1,this.updated=!0,this.released=!0,this.onRelease(this),this.$trigger.removeEventListener("touchstart",bs),this.$trigger.removeEventListener("touchmove",bs),this.$trigger.removeEventListener("touchend",bs),s.removeEventListener("touchmove",this),s.removeEventListener("touchend",this),s.removeEventListener("touchcancel",this),s.removeEventListener("mousemove",this),s.removeEventListener("mouseup",this),s.removeEventListener("selectstart",this)}reset(){return this.stop(),this.resizeTicker.pause(),this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.setX(0,!0),this.setY(0,!0),this.coords[0]=0,this.coords[1]=0,this.pointer[0]=0,this.pointer[1]=0,this.pointer[2]=0,this.pointer[3]=0,this.pointer[4]=0,this.pointer[5]=0,this.pointer[6]=0,this.pointer[7]=0,this.velocity=0,this.velocityStack[0]=0,this.velocityStack[1]=0,this.velocityStack[2]=0,this.velocityStackIndex=0,this.angle=0,this}enable(){return this.enabled||(this.enabled=!0,this.$target.classList.remove("is-disabled"),this.touchActionStyles=Je(this.$trigger,{touchAction:this.disabled[0]?"pan-x":this.disabled[1]?"pan-y":"none"}),this.$trigger.addEventListener("touchstart",this,{passive:!0}),this.$trigger.addEventListener("mousedown",this,{passive:!0}),this.$trigger.addEventListener("mouseenter",this)),this}disable(){return this.enabled=!1,this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.touchActionStyles.revert(),this.cursorStyles&&(this.cursorStyles.revert(),this.cursorStyles=null),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.bodyStyles&&(this.bodyStyles.revert(),this.bodyStyles=null),this.targetStyles&&(this.targetStyles.revert(),this.targetStyles=null),this.$target.classList.add("is-disabled"),this.$trigger.removeEventListener("touchstart",this),this.$trigger.removeEventListener("mousedown",this),this.$trigger.removeEventListener("mouseenter",this),s.removeEventListener("touchmove",this),s.removeEventListener("touchend",this),s.removeEventListener("touchcancel",this),s.removeEventListener("mousemove",this),s.removeEventListener("mouseup",this),s.removeEventListener("selectstart",this),this}revert(){return this.reset(),this.disable(),this.$target.classList.remove("is-disabled"),this.updateTicker.revert(),this.overshootTicker.revert(),this.resizeTicker.revert(),this.animate.revert(),this.resizeObserver.disconnect(),this}handleEvent(t){switch(t.type){case"mousedown":case"touchstart":this.handleDown(t);break;case"mousemove":case"touchmove":this.handleMove(t);break;case"mouseup":case"touchend":case"touchcancel":this.handleUp();break;case"mouseenter":this.handleHover();break;case"selectstart":bs(t)}}}const Es=(t,e)=>new Cs(t,e),ks=(t=S)=>new fe({duration:1*F.timeScale,onComplete:t},null,0).resume(),$s=t=>{let e;return(...s)=>{let i,r,n,o;e&&(i=e.currentIteration,r=e.iterationProgress,n=e.reversed,o=e._alternate,e.revert());const a=t(...s);return a&&!W(a)&&a.revert&&(e=a),H(r)||(e.currentIteration=i,e.iterationProgress=(o&&i%2?!n:n)?1-r:r),a||S}};class Bs{constructor(t={}){A.current&&A.current.register(this);const i=t.root;let r=s;i&&(r=i.current||i.nativeElement||ye(i)[0]||s);const n=t.defaults,o=F.defaults,a=t.mediaQueries;if(this.defaults=n?xt(n,o):o,this.root=r,this.constructors=[],this.revertConstructors=[],this.revertibles=[],this.constructorsOnce=[],this.revertConstructorsOnce=[],this.revertiblesOnce=[],this.once=!1,this.onceIndex=0,this.methods={},this.matches={},this.mediaQueryLists={},this.data={},a)for(let t in a){const s=e.matchMedia(a[t]);this.mediaQueryLists[t]=s,s.addEventListener("change",this)}}register(t){(this.once?this.revertiblesOnce:this.revertibles).push(t)}execute(t){let e=A.current,s=A.root,i=F.defaults;A.current=this,A.root=this.root,F.defaults=this.defaults;const r=this.mediaQueryLists;for(let t in r)this.matches[t]=r[t].matches;const n=t(this);return A.current=e,A.root=s,F.defaults=i,n}refresh(){return this.onceIndex=0,this.execute(()=>{let t=this.revertibles.length,e=this.revertConstructors.length;for(;t--;)this.revertibles[t].revert();for(;e--;)this.revertConstructors[e](this);this.revertibles.length=0,this.revertConstructors.length=0,this.constructors.forEach(t=>{const e=t(this);W(e)&&this.revertConstructors.push(e)})}),this}add(t,e){if(this.once=!1,W(t)){const e=t;this.constructors.push(e),this.execute(()=>{const t=e(this);W(t)&&this.revertConstructors.push(t)})}else this.methods[t]=(...t)=>this.execute(()=>e(...t));return this}addOnce(t){if(this.once=!0,W(t)){const e=this.onceIndex++;if(this.constructorsOnce[e])return this;const s=t;this.constructorsOnce[e]=s,this.execute(()=>{const t=s(this);W(t)&&this.revertConstructorsOnce.push(t)})}return this}keepTime(t){this.once=!0;const e=this.onceIndex++,s=this.constructorsOnce[e];if(W(s))return s(this);const i=$s(t);let r;return this.constructorsOnce[e]=i,this.execute(()=>{r=i(this)}),r}handleEvent(t){"change"===t.type&&this.refresh()}revert(){const t=this.revertibles,e=this.revertConstructors,s=this.revertiblesOnce,i=this.revertConstructorsOnce,r=this.mediaQueryLists;let n=t.length,o=e.length,a=s.length,h=i.length;for(;n--;)t[n].revert();for(;o--;)e[o](this);for(;a--;)s[a].revert();for(;h--;)i[h](this);for(let t in r)r[t].removeEventListener("change",this);t.length=0,e.length=0,this.constructors.length=0,s.length=0,i.length=0,this.constructorsOnce.length=0,this.onceIndex=0,this.matches={},this.methods={},this.mediaQueryLists={},this.data={}}}const Os=t=>new Bs(t),Ns=()=>{const t=s.createElement("div");s.body.appendChild(t),t.style.height="100lvh";const e=t.offsetHeight;return s.body.removeChild(t),e},Rs=(t,e)=>t&&W(t)?t(e):t,Ds=new Map;class Ls{constructor(t){this.element=t,this.useWin=this.element===s.body,this.winWidth=0,this.winHeight=0,this.width=0,this.height=0,this.left=0,this.top=0,this.scale=1,this.zIndex=0,this.scrollX=0,this.scrollY=0,this.prevScrollX=0,this.prevScrollY=0,this.scrollWidth=0,this.scrollHeight=0,this.velocity=0,this.backwardX=!1,this.backwardY=!1,this.scrollTicker=new fe({autoplay:!1,onBegin:()=>this.dataTimer.resume(),onUpdate:()=>{const t=this.backwardX||this.backwardY;Ct(this,t=>t.handleScroll(),t)},onComplete:()=>this.dataTimer.pause()}).init(),this.dataTimer=new fe({autoplay:!1,frameRate:30,onUpdate:t=>{const e=t.deltaTime,s=this.prevScrollX,i=this.prevScrollY,r=this.scrollX,n=this.scrollY,o=s-r,a=i-n;this.prevScrollX=r,this.prevScrollY=n,o&&(this.backwardX=s>r),a&&(this.backwardY=i>n),this.velocity=yt(e>0?Math.sqrt(o*o+a*a)/e:0,5)}}).init(),this.resizeTicker=new fe({autoplay:!1,duration:250*F.timeScale,onComplete:()=>{this.updateWindowBounds(),this.refreshScrollObservers(),this.handleScroll()}}).init(),this.wakeTicker=new fe({autoplay:!1,duration:500*F.timeScale,onBegin:()=>{this.scrollTicker.resume()},onComplete:()=>{this.scrollTicker.pause()}}).init(),this._head=null,this._tail=null,this.updateScrollCoords(),this.updateWindowBounds(),this.updateBounds(),this.refreshScrollObservers(),this.handleScroll(),this.resizeObserver=new ResizeObserver(()=>this.resizeTicker.restart()),this.resizeObserver.observe(this.element),(this.useWin?e:this.element).addEventListener("scroll",this,!1)}updateScrollCoords(){const t=this.useWin,s=this.element;this.scrollX=yt(t?e.scrollX:s.scrollLeft,0),this.scrollY=yt(t?e.scrollY:s.scrollTop,0)}updateWindowBounds(){this.winWidth=e.innerWidth,this.winHeight=Ns()}updateBounds(){const t=getComputedStyle(this.element),e=this.element;let s,i;if(this.scrollWidth=e.scrollWidth+parseFloat(t.marginLeft)+parseFloat(t.marginRight),this.scrollHeight=e.scrollHeight+parseFloat(t.marginTop)+parseFloat(t.marginBottom),this.updateWindowBounds(),this.useWin)s=this.winWidth,i=this.winHeight;else{const t=e.getBoundingClientRect();s=e.clientWidth,i=e.clientHeight,this.top=t.top,this.left=t.left,this.scale=t.width?s/t.width:t.height?i/t.height:1}this.width=s,this.height=i}refreshScrollObservers(){Ct(this,t=>{t._debug&&t.removeDebug()}),this.updateBounds(),Ct(this,t=>{t.refresh(),t._debug&&t.debug()})}refresh(){this.updateWindowBounds(),this.updateBounds(),this.refreshScrollObservers(),this.handleScroll()}handleScroll(){this.updateScrollCoords(),this.wakeTicker.restart()}handleEvent(t){"scroll"===t.type&&this.handleScroll()}revert(){this.scrollTicker.cancel(),this.dataTimer.cancel(),this.resizeTicker.cancel(),this.wakeTicker.cancel(),this.resizeObserver.disconnect(),(this.useWin?e:this.element).removeEventListener("scroll",this),Ds.delete(this.element)}}const As=t=>{const e=t&&ye(t)[0]||s.body;let i=Ds.get(e);return i||(i=new Ls(e),Ds.set(e,i)),i},Fs=(t,e,s,i,r)=>{const n="min"===e,o="max"===e,a="top"===e||"left"===e||"start"===e||n?0:"bottom"===e||"right"===e||"end"===e||o?"100%":"center"===e?"50%":e,{n:h,u:l}=Ut(a,Yt);let c=h;return"%"===l?c=h/100*s:l&&(c=Se(t,Yt,"px",!0).n),o&&i<0&&(c+=i),n&&r>0&&(c+=r),c},Ms=(t,e,s,i,r)=>{let n;if(V(e)){const o=R.exec(e);if(o){const a=o[0],h=a[0],l=e.split(a),c="min"===l[0],d="max"===l[0],u=Fs(t,l[0],s,i,r),p=Fs(t,l[1],s,i,r);if(c){const e=It(Fs(t,"min",s),p,h);n=eu?u:e}else n=It(u,p,h)}else n=Fs(t,e,s,i,r)}else n=e;return yt(n,0)},Ps=t=>{let e;const s=t.targets;for(let t=0,i=s.length;t()=>{const e=this.linked;return e&&e[t]?e[t]():null}):null,l=a&&h.length>2;this.index=Is++,this.id=H(t.id)?this.index:t.id,this.container=As(t.container),this.target=null,this.linked=null,this.repeat=null,this.horizontal=null,this.enter=null,this.leave=null,this.sync=n||o||!!h,this.syncEase=n?i:null,this.syncSmooth=o?!0===e||r?1:e:null,this.onSyncEnter=h&&!l&&h[0]?h[0]:S,this.onSyncLeave=h&&!l&&h[1]?h[1]:S,this.onSyncEnterForward=h&&l&&h[0]?h[0]:S,this.onSyncLeaveForward=h&&l&&h[1]?h[1]:S,this.onSyncEnterBackward=h&&l&&h[2]?h[2]:S,this.onSyncLeaveBackward=h&&l&&h[3]?h[3]:S,this.onEnter=t.onEnter||S,this.onLeave=t.onLeave||S,this.onEnterForward=t.onEnterForward||S,this.onLeaveForward=t.onLeaveForward||S,this.onEnterBackward=t.onEnterBackward||S,this.onLeaveBackward=t.onLeaveBackward||S,this.onUpdate=t.onUpdate||S,this.onSyncComplete=t.onSyncComplete||S,this.reverted=!1,this.ready=!1,this.completed=!1,this.began=!1,this.isInView=!1,this.forceEnter=!1,this.hasEntered=!1,this.offset=0,this.offsetStart=0,this.offsetEnd=0,this.distance=0,this.prevProgress=0,this.thresholds=["start","end","end","start"],this.coords=[0,0,0,0],this.debugStyles=null,this.$debug=null,this._params=t,this._debug=Lt(t.debug,!1),this._next=null,this._prev=null,kt(this.container,this),ks(()=>{if(!this.reverted){if(!this.target){const e=ye(t.target)[0];this.target=e||s.body,this.refresh()}this._debug&&this.debug()}})}link(t){if(t&&(t.pause(),this.linked=t,H(t)||(t.persist=!0),!this._params.target)){let e;H(t.targets)?Ct(t,t=>{t.targets&&!e&&(e=Ps(t))}):e=Ps(t),this.target=e||s.body,this.refresh()}return this}get velocity(){return this.container.velocity}get backward(){return this.horizontal?this.container.backwardX:this.container.backwardY}get scroll(){return this.horizontal?this.container.scrollX:this.container.scrollY}get progress(){const t=(this.scroll-this.offsetStart)/this.distance;return t===1/0||isNaN(t)?0:yt(gt(t,0,1),6)}refresh(){this.ready=!0,this.reverted=!1;const t=this._params;return this.repeat=Lt(Rs(t.repeat,this),!0),this.horizontal="x"===Lt(Rs(t.axis,this),"y"),this.enter=Lt(Rs(t.enter,this),"end start"),this.leave=Lt(Rs(t.leave,this),"start end"),this.updateBounds(),this.handleScroll(),this}removeDebug(){return this.$debug&&(this.$debug.parentNode.removeChild(this.$debug),this.$debug=null),this.debugStyles&&(this.debugStyles.revert(),this.$debug=null),this}debug(){this.removeDebug();const t=this.container,e=this.horizontal,i=t.element.querySelector(":scope > .animejs-onscroll-debug"),r=s.createElement("div"),n=s.createElement("div"),o=s.createElement("div"),a=zs[this.index%zs.length],h=t.useWin,l=h?t.winWidth:t.width,c=h?t.winHeight:t.height,d=t.scrollWidth,u=t.scrollHeight,p=this.container.width>360?320:260,m=e?0:10,f=e?10:0,g=e?24:p/2,_=e?g:15,y=e?60:g,v=e?y:_,b=e?"repeat-x":"repeat-y",T=t=>e?"0px "+t+"px":t+"px 2px",S=t=>`linear-gradient(${e?90:0}deg, ${t} 2px, transparent 1px)`,w=(t,e,s,i,r)=>`position:${t};left:${e}px;top:${s}px;width:${i}px;height:${r}px;`;r.style.cssText=`${w("absolute",m,f,e?d:p,e?p:u)}\n pointer-events: none;\n z-index: ${this.container.zIndex++};\n display: flex;\n flex-direction: ${e?"column":"row"};\n filter: drop-shadow(0px 1px 0px rgba(0,0,0,.75));\n `,n.style.cssText=`${w("sticky",0,0,e?l:g,e?g:c)}`,i||(n.style.cssText+=`background:\n ${S("#FFFF")}${T(g-10)} / 100px 100px ${b},\n ${S("#FFF8")}${T(g-10)} / 10px 10px ${b};\n `),o.style.cssText=`${w("relative",0,0,e?d:g,e?g:u)}`,i||(o.style.cssText+=`background:\n ${S("#FFFF")}${T(0)} / ${e?"100px 10px":"10px 100px"} ${b},\n ${S("#FFF8")}${T(0)} / ${e?"10px 0px":"0px 10px"} ${b};\n `);const x=[" enter: "," leave: "];this.coords.forEach((t,i)=>{const r=i>1,h=(r?0:this.offset)+t,m=i%2,f=h(r?e?l:c:e?d:u)-v,b=(r?m&&!f:!m&&!f)||g,T=s.createElement("div"),S=s.createElement("div"),C=e?b?"right":"left":b?"bottom":"top",E=b?(e?y:_)+(r?e?-1:g?0:-2:e?-1:-2):e?1:0;S.innerHTML=`${this.id}${x[m]}${this.thresholds[i]}`,T.style.cssText=`${w("absolute",0,0,y,_)}\n display: flex;\n flex-direction: ${e?"column":"row"};\n justify-content: flex-${r?"start":"end"};\n align-items: flex-${b?"end":"start"};\n border-${C}: 2px solid ${a};\n `,S.style.cssText=`\n overflow: hidden;\n max-width: ${p/2-10}px;\n height: ${_};\n margin-${e?b?"right":"left":b?"bottom":"top"}: -2px;\n padding: 1px;\n font-family: ui-monospace, monospace;\n font-size: 10px;\n letter-spacing: -.025em;\n line-height: 9px;\n font-weight: 600;\n text-align: ${e&&b||!e&&!r?"right":"left"};\n white-space: pre;\n text-overflow: ellipsis;\n color: ${m?a:"rgba(0,0,0,.75)"};\n background-color: ${m?"rgba(0,0,0,.65)":a};\n border: 2px solid ${m?a:"transparent"};\n border-${e?b?"top-left":"top-right":b?"top-left":"bottom-left"}-radius: 5px;\n border-${e?b?"bottom-left":"bottom-right":b?"top-right":"bottom-right"}-radius: 5px;\n `,T.appendChild(S);let k=h-E+(e?1:0);T.style[e?"left":"top"]=`${k}px`,(r?n:o).appendChild(T)}),r.appendChild(n),r.appendChild(o),t.element.appendChild(r),i||r.classList.add("animejs-onscroll-debug"),this.$debug=r,"static"===Qe(t.element,"position")&&(this.debugStyles=Je(t.element,{position:"relative "}))}updateBounds(){let t;this._debug&&this.removeDebug();const e=this.target,i=this.container,r=this.horizontal,n=this.linked;let o,a=e;for(n&&(o=n.currentTime,n.seek(0,!0)),a.parentElement;a&&a!==i.element&&a!==s.body;){const e="sticky"===Qe(a,"position")&&Je(a,{position:"static"});a=a.parentElement,e&&(t||(t=[]),t.push(e))}const h=e.getBoundingClientRect(),l=i.scale,c=(r?h.left+i.scrollX-i.left:h.top+i.scrollY-i.top)*l,d=(r?h.width:h.height)*l,u=r?i.width:i.height,p=(r?i.scrollWidth:i.scrollHeight)-u,m=this.enter,f=this.leave;let g="start",_="end",y="end",v="start";if(V(m)){const t=m.split(" ");y=t[0],g=t.length>1?t[1]:g}else if(X(m)){const t=m;H(t.container)||(y=t.container),H(t.target)||(g=t.target)}else Y(m)&&(y=m);if(V(f)){const t=f.split(" ");v=t[0],_=t.length>1?t[1]:_}else if(X(f)){const t=f;H(t.container)||(v=t.container),H(t.target)||(_=t.target)}else Y(f)&&(v=f);const b=Ms(e,g,d),T=Ms(e,_,d),S=b+c-u,w=T+c-p,x=Ms(e,y,u,S,w),C=Ms(e,v,u,S,w),E=b+c-x,k=T+c-C,$=k-E;this.offset=c,this.offsetStart=E,this.offsetEnd=k,this.distance=$<=0?0:$,this.thresholds=[g,_,y,v],this.coords=[b,T,x,C],t&&t.forEach(t=>t.revert()),n&&n.seek(o,!0),this._debug&&this.debug()}handleScroll(){if(!this.ready)return;const t=this.linked,e=this.sync,s=this.syncEase,i=this.syncSmooth,r=t&&(s||i),n=this.horizontal,o=this.container,a=this.scroll,h=a<=this.offsetStart,l=a>=this.offsetEnd,c=!h&&!l,d=a===this.offsetStart||a===this.offsetEnd,u=!this.hasEntered&&d,p=this._debug&&this.$debug;let m=!1,f=!1,g=this.progress;if(h&&this.began&&(this.began=!1),g>0&&!this.began&&(this.began=!0),r){const e=t.progress;if(i&&Y(i)){if(i<1){const t=1e-4,s=eg&&!g?-t:0;g=yt(bt(e,g,bt(.01,.2,i))+s,6)}}else s&&(g=s(g));m=g!==this.prevProgress,f=1===e,m&&!f&&i&&e&&o.wakeTicker.restart()}if(p){const t=n?o.scrollY:o.scrollX;p.style[n?"top":"left"]=t+10+"px"}(c&&!this.isInView||u&&!this.forceEnter&&!this.hasEntered)&&(c&&(this.isInView=!0),this.forceEnter&&this.hasEntered?c&&(this.forceEnter=!1):(p&&c&&(p.style.zIndex=""+this.container.zIndex++),this.onSyncEnter(this),this.onEnter(this),this.backward?(this.onSyncEnterBackward(this),this.onEnterBackward(this)):(this.onSyncEnterForward(this),this.onEnterForward(this)),this.hasEntered=!0,u&&(this.forceEnter=!0))),(c||!c&&this.isInView)&&(m=!0),m&&(r&&t.seek(t.duration*g),this.onUpdate(this)),!c&&this.isInView&&(this.isInView=!1,this.onSyncLeave(this),this.onLeave(this),this.backward?(this.onSyncLeaveBackward(this),this.onLeaveBackward(this)):(this.onSyncLeaveForward(this),this.onLeaveForward(this)),e&&!i&&(f=!0)),g>=1&&this.began&&!this.completed&&(e&&f||!e)&&(e&&this.onSyncComplete(this),this.completed=!0,(!this.repeat&&!t||!this.repeat&&t&&t.completed)&&this.revert()),g<1&&this.completed&&(this.completed=!1),this.prevProgress=g}revert(){if(this.reverted)return;const t=this.container;return Et(t,this),t._head||t.revert(),this._debug&&this.removeDebug(),this.reverted=!0,this.ready=!1,this}}const Xs=(t={})=>new Us(t),Ys=(t,e,s)=>(((1-3*s+3*e)*t+(3*s-6*e))*t+3*e)*t,Vs=(t,e,s)=>{let i,r,n=0,o=1,a=0;do{r=n+(o-n)/2,i=Ys(r,e,s)-t,i>0?o=r:n=r}while(at(i)>1e-7&&++a<100);return r},Ws=(t=.5,e=0,s=.5,i=1)=>t===e&&s===i?we:r=>0===r||1===r?r:Ys(Vs(r,t,s),e,i),Hs=(t=10,e)=>{const s=e?lt:ct;return e=>s(gt(e,0,1)*t)*(1/t)},qs=(...t)=>{const e=t.length;if(!e)return we;const s=e-1,i=t[0],r=t[s],n=[0],o=[st(i)];for(let e=1;e{const s=[0],i=t-1;for(let t=1;t(...s)=>e?e=>t(...s,e):e=>t(e,...s),Ks=t=>(...e)=>{const s=t(...e);return new Proxy(S,{apply:(t,e,[i])=>s(i),get:(t,e)=>Ks((...t)=>{const i=Js[e](...t);return t=>i(s(t))})})},ti=(t,e,s=0)=>{const i=(...t)=>(t.length{const i=10**s;return Math.floor((Math.random()*(e-t+1/i)+t)*i)/i};let mi=0;const fi=(t,e=0,s=1,i=0)=>{let r=void 0===t?mi++:t;return(t=e,n=s,o=i)=>{r+=1831565813,r=Math.imul(r^r>>>15,1|r),r^=r+Math.imul(r^r>>>7,61|r);const a=10**o;return Math.floor((((r^r>>>14)>>>0)/4294967296*(n-t+1/a)+t)*a)/a}},gi=t=>t[pi(0,t.length-1)],_i=t=>{let e,s,i=t.length;for(;i;)s=pi(0,--i),e=t[i],t[i]=t[s],t[s]=e;return t},yi=(t,e={})=>{let s=[],i=0;const r=e.from,n=e.reversed,o=e.ease,a=!H(o),h=a&&!H(o.ease)?o.ease:a?De(o):null,l=e.grid,c=e.axis,d=e.total,u=H(r)||0===r||"first"===r,p="center"===r,m="last"===r,f="random"===r,g=U(t),y=e.use,v=st(g?t[0]:t),b=g?st(t[1]):0,T=B.exec((g?t[1]:t)+_),S=e.start||0+(g?v:0);let w=u?0:Y(r)?r:0;return(t,r,o,a)=>{const[u]=ve(t),_=H(d)?o:d,x=!H(y)&&(W(y)?y(u,r,_):Pt(u,y)),C=Y(x)||V(x)&&Y(+x)?+x:r;if(p&&(w=(_-1)/2),m&&(w=_-1),!s.length){for(let t=0;t<_;t++){if(l){const e=p?(l[0]-1)/2:w%l[0],i=p?(l[1]-1)/2:ct(w/l[0]),r=e-t%l[0],n=i-ct(t/l[0]);let o=rt(r*r+n*n);"x"===c&&(o=-r),"y"===c&&(o=-n),s.push(o)}else s.push(at(w-t));i=ut(...s)}h&&(s=s.map(t=>h(t/i)*i)),n&&(s=s.map(t=>c?t<0?-1*t:-t:at(i-t))),f&&(s=_i(s))}const E=g?(b-v)/i:v;let k=(a?ts(a,H(e.start)?a.iterationDuration:S):S)+(E*yt(s[C],2)||0);return e.modifier&&(k=e.modifier(k)),T&&(k=`${k}${T[2]}`),k}};var vi=Object.freeze({__proto__:null,$:ve,clamp:li,cleanInlineStyles:jt,createSeededRandom:fi,damp:ui,degToRad:oi,get:Qe,keepTime:$s,lerp:di,mapRange:ni,padEnd:ii,padStart:si,radToDeg:ai,random:pi,randomPick:gi,remove:Ze,round:ci,roundPad:ei,set:Je,shuffle:_i,snap:hi,stagger:yi,sync:ks,wrap:ri});const bi=t=>{const e=ye(t)[0];return e&&j(e)?e:console.warn(`${t} is not a valid SVGGeometryElement`)},Ti=(t,e,s,i,r)=>{const n=s+i,o=r?Math.max(0,Math.min(n,e)):(n%e+e)%e;return t.getPointAtLength(o)},Si=(t,e,s=0)=>i=>{const r=+t.getTotalLength(),n=i[l],o=t.getCTM(),a=0===s;return{from:0,to:r,modifier:i=>{const h=i+s*r;if("a"===e){const e=Ti(t,r,h,-1,a),s=Ti(t,r,h,1,a);return 180*pt(s.y-e.y,s.x-e.x)/mt}{const s=Ti(t,r,h,0,a);return"x"===e?n||!o?s.x:s.x*o.a+s.y*o.c+o.e:n||!o?s.y:s.x*o.b+s.y*o.d+o.f}}}},wi=(t,e=0)=>{const s=bi(t);if(s)return{translateX:Si(s,"x",e),translateY:Si(s,"y",e),rotate:Si(s,"a",e)}},xi=t=>{let e=1;if(t&&t.getCTM){const s=t.getCTM();s&&(e=(rt(s.a*s.a+s.b*s.b)+rt(s.c*s.c+s.d*s.d))/2)}return e},Ci=(t,e,s)=>{const i=f,r=getComputedStyle(t),n=r.strokeLinecap,o="non-scaling-stroke"===r.vectorEffect?t:null;let a=n;const h=new Proxy(t,{get(t,e){const s=t[e];return e===u?t:"setAttribute"===e?(...e)=>{if("draw"===e[0]){const s=e[1].split(" "),r=+s[0],h=+s[1],l=xi(o),c=-1e3*r*l,d=h*i*l+c,u=i*l+(0===r&&1===h||1===r&&0===h?0:10*l)-d;if("butt"!==n){const e=r===h?"butt":n;a!==e&&(t.style.strokeLinecap=`${e}`,a=e)}t.setAttribute("stroke-dashoffset",`${c}`),t.setAttribute("stroke-dasharray",`${d} ${u}`)}return Reflect.apply(s,t,e)}:W(s)?(...e)=>Reflect.apply(s,t,e):s}});return"1000"!==t.getAttribute("pathLength")&&(t.setAttribute("pathLength","1000"),h.setAttribute("draw",`${e} ${s}`)),h},Ei=(t,e=0,s=0)=>ye(t).map(t=>Ci(t,e,s)),ki=(t,e=.33)=>s=>{if(!(s.tagName||"").toLowerCase().match(/^(path|polygon|polyline)$/))throw new Error(`Can't morph a <${s.tagName}> SVG element. Use , or .`);const i=bi(t);if(!i)throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing , or SVG element.");if(!(i.tagName||"").toLowerCase().match(/^(path|polygon|polyline)$/))throw new Error(`Can't morph a <${i.tagName}> SVG element. Use , or .`);const r="path"===s.tagName,n=r?" ":",",o=s[d];o&&s.setAttribute(r?"d":"points",o);let a="",h="";if(e){const t=s.getTotalLength(),o=i.getTotalLength(),l=Math.max(Math.ceil(t*e),Math.ceil(o*e));for(let e=0;et.isWordLike||" "===t.segment||Y(+t.segment),Xi=t=>t.setAttribute("aria-hidden","true"),Yi=(t,e)=>[...t.querySelectorAll(`[data-${e}]:not([data-${e}] [data-${e}])`)],Vi={line:"#00D672",word:"#FF4B4B",char:"#5A87FF"},Wi=t=>{if(!t.childElementCount&&!t.textContent.trim()){const e=t.parentElement;t.remove(),e&&Wi(e)}},Hi=(t,e,s)=>{const i=t.getAttribute(Mi);(null!==i&&+i!==e||"BR"===t.tagName)&&s.add(t);let r=t.childElementCount;for(;r--;)Hi(t.children[r],e,s);return s},qi=(t,e={})=>{let s="";const i=V(e.class)?` class="${e.class}"`:"",r=Lt(e.clone,!1),n=Lt(e.wrap,!1),o=n?!0===n?"clip":n:!!r&&"clip";return n&&(s+=``),s+=``,r?(s+="{value}",s+=`{value}`):s+="{value}",s+="",n&&(s+=""),s},ji=(t,e,s,i,r,n,o,a,h)=>{const l=r===Li,c=r===Fi,d=`_${r}_`,u=W(t)?t(s):t,p=l?"block":"inline-block";zi.innerHTML=u.replace(Oi,``).replace(Ni,`${c?h:l?o:a}`);const m=zi.content,f=m.firstElementChild,g=m.querySelector(`[data-${r}]`)||f,_=m.querySelectorAll(`i.${d}`),y=_.length;if(y){f.style.display=p,g.style.display=p,g.setAttribute(Mi,`${o}`),l||(g.setAttribute("data-word",`${a}`),c&&g.setAttribute("data-char",`${h}`));let t=y;for(;t--;){const e=_[t],i=e.parentElement;i.style.display=p,l?i.innerHTML=s.innerHTML:i.replaceChild(s.cloneNode(!0),e)}e.push(g),i.appendChild(m)}else console.warn('The expression "{value}" is missing from the provided template.');return n&&(f.style.outline=`1px dotted ${Vi[r]}`),f};class Gi{constructor(e,i={}){Pi||(Pi=Bi?new Bi([],{granularity:Ai}):{segment:t=>{const e=[],s=t.split(Ri);for(let t=0,i=s.length;t[...t].map(t=>({segment:t}))}),!zi&&t&&(zi=s.createElement("template")),A.current&&A.current.register(this);const{words:r,chars:n,lines:o,accessible:a,includeSpaces:h,debug:l}=i,c=(e=U(e)?e[0]:e)&&e.nodeType?e:(_e(e)||[])[0],d=!0===o?{}:o,u=!0===r||H(r)?{}:r,p=!0===n?{}:n;this.debug=Lt(l,!1),this.includeSpaces=Lt(h,!1),this.accessible=Lt(a,!0),this.linesOnly=d&&!u&&!p,this.lineTemplate=X(d)?qi(Li,d):d,this.wordTemplate=X(u)||this.linesOnly?qi(Ai,u):u,this.charTemplate=X(p)?qi(Fi,p):p,this.$target=c,this.html=c&&c.innerHTML,this.lines=[],this.words=[],this.chars=[],this.effects=[],this.effectsCleanups=[],this.cache=null,this.ready=!1,this.width=0,this.resizeTimeout=null;const m=()=>this.html&&(d||u||p)&&this.split();this.resizeObserver=new ResizeObserver(()=>{clearTimeout(this.resizeTimeout),this.resizeTimeout=setTimeout(()=>{const t=c.offsetWidth;t!==this.width&&(this.width=t,m())},150)}),this.lineTemplate&&!this.ready?s.fonts.ready.then(m):m(),c?this.resizeObserver.observe(c):console.warn("No Text Splitter target found.")}addEffect(t){if(!W(t))return console.warn("Effect must return a function.");const e=$s(t);return this.effects.push(e),this.ready&&(this.effectsCleanups[this.effects.length-1]=e(this)),this}revert(){return clearTimeout(this.resizeTimeout),this.lines.length=this.words.length=this.chars.length=0,this.resizeObserver.disconnect(),this.effectsCleanups.forEach(t=>W(t)?t(this):t.revert&&t.revert()),this.$target.innerHTML=this.html,this}splitNode(t){const e=this.wordTemplate,i=this.charTemplate,r=this.includeSpaces,n=this.debug,o=t.nodeType;if(3===o){const o=t.nodeValue;if(o.trim()){const a=[],h=this.words,l=this.chars,c=Pi.segment(o),d=s.createDocumentFragment();let u=null;for(const t of c){const e=t.segment,s=Ui(t);if(!u||s&&u&&Ui(u))a.push(e);else{const t=a.length-1;a[t].includes(" ")||e.includes(" ")?a.push(e):a[t]+=e}u=t}for(let t=0,o=a.length;tW(t)&&t(this)),i||(t&&(e.innerHTML=this.html,this.words.length=this.chars.length=0),this.splitNode(e),this.cache=e.innerHTML),h&&(i&&(e.innerHTML=this.cache),this.lines.length=0,n&&(this.words=Yi(e,Ai))),o&&(h||n)&&(this.chars=Yi(e,Fi));const l=this.words.length?this.words:this.chars;let c,d=0;for(let t=0,e=l.length;t.5*i&&d++,e.setAttribute(Mi,`${d}`);const r=e.querySelectorAll(`[${Mi}]`);let n=r.length;for(;n--;)r[n].setAttribute(Mi,`${d}`);c=s}if(h){const t=s.createDocumentFragment(),i=new Set,a=[];for(let t=0;t{const e=t.parentElement;e&&i.add(e),t.remove()}),a.push(s)}i.forEach(Wi);for(let e=0,s=a.length;ethis.effectsCleanups[e]=t(this)),this}refresh(){this.split(!0)}}const Qi=(t,e)=>new Gi(t,e),Ji=(t,e)=>(console.warn("text.split() is deprecated, import splitText() directly, or text.splitText()"),new Gi(t,e));var Zi=Object.freeze({__proto__:null,TextSplitter:Gi,split:Ji,splitText:Qi});const Ki=(t,e=100)=>{const s=[];for(let i=0;i<=e;i++)s.push(yt(t(i/e),4));return`linear(${s.join(", ")})`},tr={},er=t=>{let e=tr[t];if(e)return e;if(e="linear",V(t)){if(I(t,"linear")||I(t,"cubic-")||I(t,"steps")||I(t,"ease"))e=t;else if(I(t,"cubicB"))e=P(t);else{const s=Ne(t);W(s)&&(e=s===we?"linear":Ki(s))}tr[t]=e}else if(W(t)){const s=Ki(t);s&&(e=s)}else t.ease&&(e=Ki(t.ease));return e},sr=["x","y","z"],ir=["perspective","width","height","margin","padding","top","right","bottom","left","borderWidth","fontSize","borderRadius",...sr],rr=(()=>[...sr,...b.filter(t=>["X","Y","Z"].some(e=>t.endsWith(e)))])();let nr=null;const or=(t,e,s,i,r)=>{let n=V(e)?e:At(e,s,i,r);return Y(n)?ir.includes(t)||I(t,"translate")?`${n}px`:I(t,"rotate")||I(t,"skew")?`${n}deg`:`${n}`:n},ar=(t,e,s,i,r,n)=>{let o="0";const a=H(i)?getComputedStyle(t)[e]:or(e,i,t,r,n);return o=H(s)?U(i)?i.map(s=>or(e,s,t,r,n)):a:[or(e,s,t,r,n),a],o};class hr{constructor(e,s){A.current&&A.current.register(this),q(nr)&&(!t||!H(CSS)&&Object.hasOwnProperty.call(CSS,"registerProperty")?(b.forEach(t=>{const e=I(t,"skew"),s=I(t,"scale"),i=I(t,"rotate"),r=I(t,"translate"),n=i||e,o=n?"":s?"":r?"":"*";try{CSS.registerProperty({name:"--"+t,syntax:o,inherits:!1,initialValue:r?"0px":n?"0deg":s?"1":"0"})}catch{}}),nr=!0):nr=!1);const i=ve(e),r=i.length;r||console.warn("No target found. Make sure the element you're trying to animate is accessible before creating your animation.");const n=Lt(s.ease,er(F.defaults.ease)),o=n.ease&&n,a=Lt(s.autoplay,F.defaults.autoplay),h=!(!a||!a.link)&&a,l=s.alternate&&!0===s.alternate,d=s.reversed&&!0===s.reversed,u=Lt(s.loop,F.defaults.loop),p=!0===u||u===1/0?1/0:Y(u)?u+1:1,m=l?d?"alternate-reverse":"alternate":d?"reverse":"normal",g=er(n),y=1===F.timeScale?1:f;this.targets=i,this.animations=[],this.controlAnimation=null,this.onComplete=s.onComplete||F.defaults.onComplete,this.duration=0,this.muteCallbacks=!1,this.completed=!1,this.paused=!a||!1!==h,this.reversed=d,this.persist=Lt(s.persist,F.defaults.persist),this.autoplay=a,this._speed=Lt(s.playbackRate,F.defaults.playbackRate),this._resolve=S,this._completed=0,this._inlineStyles=[],i.forEach((t,e)=>{const i=t[c],a=rr.some(t=>s.hasOwnProperty(t)),h=t.style,l=this._inlineStyles[e]={},d=(o?o.settlingDuration:At(Lt(s.duration,F.defaults.duration),t,e,r))*y,u=At(Lt(s.delay,F.defaults.delay),t,e,r)*y,f=Lt(s.composition,"replace");for(let o in s){if(!K(o))continue;const c={},_={iterations:p,direction:m,fill:"both",easing:g,duration:d,delay:u,composite:f},T=s[o],S=!!a&&(b.includes(o)?o:v.get(o)),w=S?"transform":o;let x;if(l[w]||(l[w]=h[w]),X(T)){const s=T,a=Lt(s.ease,n),l=a.ease&&a,p=s.to,m=s.from;if(_.duration=(l?l.settlingDuration:At(Lt(s.duration,d),t,e,r))*y,_.delay=At(Lt(s.delay,u),t,e,r)*y,_.composite=Lt(s.composition,f),_.easing=er(a),x=ar(t,o,m,p,e,r),S?(c[`--${S}`]=x,i[S]=x):c[o]=ar(t,o,m,p,e,r),Ge(this,t,o,c,_),!H(m))if(S){const t=`--${S}`;h.setProperty(t,c[t][0])}else h[o]=c[o][0]}else x=U(T)?T.map(s=>or(o,s,t,e,r)):or(o,T,t,e,r),S?(c[`--${S}`]=x,i[S]=x):c[o]=x,Ge(this,t,o,c,_)}if(a){let t=_;for(let e in i)t+=`${T[e]}var(--${e})) `;h.transform=t}}),h&&this.autoplay.link(this)}forEach(t){const e=V(t)?e=>e[t]():t;return this.animations.forEach(e),this}get speed(){return this._speed}set speed(t){this._speed=+t,this.forEach(e=>e.playbackRate=t)}get currentTime(){const t=this.controlAnimation,e=F.timeScale;return this.completed?this.duration:t?+t.currentTime*(1===e?1:e):0}set currentTime(t){const e=t*(1===F.timeScale?1:f);this.forEach(t=>{!this.persist&&e>=this.duration&&t.play(),t.currentTime=e})}get progress(){return this.currentTime/this.duration}set progress(t){this.forEach(e=>e.currentTime=t*this.duration||0)}resume(){return this.paused?(this.paused=!1,this.forEach("play")):this}pause(){return this.paused?this:(this.paused=!0,this.forEach("pause"))}alternate(){return this.reversed=!this.reversed,this.forEach("reverse"),this.paused&&this.forEach("pause"),this}play(){return this.reversed&&this.alternate(),this.resume()}reverse(){return this.reversed||this.alternate(),this.resume()}seek(t,e=!1){return e&&(this.muteCallbacks=!0),t{const s=t.style,i=this._inlineStyles[e];for(let t in i){const e=i[t];H(e)||e===_?s.removeProperty(P(t)):s[t]=e}t.getAttribute("style")===_&&t.removeAttribute("style")}),this}then(t=S){const e=this.then,s=()=>{this.then=null,t(this),this.then=e,this._resolve=S};return new Promise(t=>(this._resolve=()=>t(s()),this.completed&&this._resolve(),this))}}const lr={animate:(t,e)=>new hr(t,e),convertEase:Ki};export{ve as $,ns as Animatable,Cs as Draggable,We as JSAnimation,Bs as Scope,Us as ScrollObserver,_s as Spring,Gi as TextSplitter,is as Timeline,fe as Timer,hr as WAAPIAnimation,He as animate,li as clamp,jt as cleanInlineStyles,os as createAnimatable,Es as createDraggable,Ei as createDrawable,wi as createMotionPath,Os as createScope,fi as createSeededRandom,vs as createSpring,rs as createTimeline,ge as createTimer,Ws as cubicBezier,ui as damp,oi as degToRad,Be as eases,Gs as easings,ee as engine,Qe as get,js as irregular,$s as keepTime,di as lerp,qs as linear,ni as mapRange,ki as morphTo,Xs as onScroll,ii as padEnd,si as padStart,ai as radToDeg,pi as random,gi as randomPick,Ze as remove,ci as round,ei as roundPad,Ds as scrollContainers,Je as set,_i as shuffle,hi as snap,Ji as split,Qi as splitText,ys as spring,yi as stagger,Hs as steps,$i as svg,ks as sync,Zi as text,vi as utils,lr as waapi,ri as wrap}; +const t="undefined"!=typeof window,e=t?window:null,s=t?document:null,i={OBJECT:0,ATTRIBUTE:1,CSS:2,TRANSFORM:3,CSS_VAR:4},r={NUMBER:0,UNIT:1,COLOR:2,COMPLEX:3},n={NONE:0,AUTO:1,FORCE:2},o={replace:0,none:1,blend:2},a=Symbol(),l=Symbol(),h=Symbol(),d=Symbol(),c=Symbol(),u=1e-11,p=1e12,m=1e3,f=240,g="",y="var(",v=(()=>{const t=new Map;return t.set("x","translateX"),t.set("y","translateY"),t.set("z","translateZ"),t})(),_=["perspective","translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY"],b=_.reduce((t,e)=>({...t,[e]:e+"("}),{}),T=()=>{},x=/\)\s*[-.\d]/,S=/(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i,w=/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i,C=/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,$=/hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i,E=/hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,N=/[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi,k=/^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i,R=/([a-z])([A-Z])/g,A=/(\*=|\+=|-=)/,D=/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/,O={id:null,keyframes:null,playbackEase:null,playbackRate:1,frameRate:f,loop:0,reversed:!1,alternate:!1,autoplay:!0,persist:!1,duration:m,delay:0,loopDelay:0,ease:"out(2)",composition:o.replace,modifier:t=>t,onBegin:T,onBeforeUpdate:T,onUpdate:T,onLoop:T,onPause:T,onComplete:T,onRender:T},I={current:null,root:s},L={defaults:O,precision:4,timeScale:1,tickThreshold:200,editor:null},B={version:"4.4.1",engine:null};t&&(e.AnimeJS||(e.AnimeJS=[]),e.AnimeJS.push(B));const M=t=>t.replace(R,"$1-$2").toLowerCase(),P=(t,e)=>0===t.indexOf(e),F=Date.now,V=Array.isArray,X=t=>t&&t.constructor===Object,z=t=>"number"==typeof t&&!isNaN(t),H=t=>"string"==typeof t,Y=t=>"function"==typeof t,U=t=>void 0===t,W=t=>U(t)||null===t,q=e=>t&&e instanceof SVGElement,j=t=>S.test(t),G=t=>P(t,"rgb"),Z=t=>P(t,"hsl"),Q=t=>j(t)||(G(t)||Z(t))&&(")"===t[t.length-1]||!x.test(t)),J=t=>!L.defaults.hasOwnProperty(t),K=["opacity","rotate","overflow","color"],tt=(t,e)=>{if(K.includes(e))return!1;if(t.getAttribute(e)||e in t){if("scale"===e){const e=t.parentNode;return e&&"filter"===e.tagName}return!0}},et=t=>H(t)?parseFloat(t):t,st=Math.pow,it=Math.sqrt,rt=Math.sin,nt=Math.cos,ot=Math.abs,at=Math.exp,lt=Math.ceil,ht=Math.floor,dt=Math.asin,ct=Math.max,ut=Math.atan2,pt=Math.PI,mt=Math.round,ft=(t,e,s)=>ts?s:t,gt=(t,e)=>{if(e<0)return t;if(!e)return mt(t);const s=10**e;return mt(t*s)/s},yt=(t,e)=>V(e)?e.reduce((e,s)=>ot(s-t)t+(e-t)*s,_t=t=>t===1/0?p:t===-1/0?-p:t,bt=t=>t<=u?u:_t(gt(t,11)),Tt=t=>V(t)?[...t]:t,xt=(t,e)=>{const s={...t};for(let i in e){const r=t[i];s[i]=U(r)?e[i]:r}return s},St=(t,e,s,i="_prev",r="_next")=>{let n=t._head,o=r;for(s&&(n=t._tail,o=i);n;){const t=n[o];e(n),n=t}},wt=(t,e,s="_prev",i="_next")=>{const r=e[s],n=e[i];r?r[i]=n:t._head=n,n?n[s]=r:t._tail=r,e[s]=null,e[i]=null},Ct=(t,e,s,i="_prev",r="_next")=>{let n=t._tail;for(;n&&s&&s(n,e);)n=n[i];const o=n?n[r]:t._head;n?n[r]=e:t._head=e,o?o[i]=e:t._tail=e,e[i]=n,e[r]=o},$t=(t,e,s)=>{const i=t.style.transform;if(i){const r=t[d];let n=0;const o=i.length;let a;for(;n=o)break;const t=n;for(;n=o)break;const e=i.substring(t,n);let s=1;const l=n+1;let h=-1,d=-1;for(n++;n0;){const t=i.charCodeAt(n);40===t?s++:41===t?s--:44===t&&1===s&&(-1===h?h=n:-1===d&&(d=n)),n++}const c=n-1;"translate"===e||"translate3d"===e?(-1===h?r.translateX=i.substring(l,c).trim():(r.translateX=i.substring(l,h).trim(),-1===d?r.translateY=i.substring(h+1,c).trim():(r.translateY=i.substring(h+1,d).trim(),r.translateZ=i.substring(d+1,c).trim())),a=i.substring(l,c)):"scale"===e||"scale3d"===e?-1===h?r.scale=i.substring(l,c).trim():(r.scaleX=i.substring(l,h).trim(),-1===d?r.scaleY=i.substring(h+1,c).trim():(r.scaleY=i.substring(h+1,d).trim(),r.scaleZ=i.substring(d+1,c).trim())):r[e]=i.substring(l,c)}if("translate3d"===e&&a)return s&&(s[e]=a),a;const l=r[e];if(!U(l))return s&&(s[e]=l),l}return"translate3d"===e?"0px, 0px, 0px":"rotate3d"===e?"0, 0, 0, 0deg":P(e,"scale")?"1":P(e,"rotate")||P(e,"skew")?"0deg":"0px"},Et=t=>{let e=g;for(let s=0,i=_.length;s{const e=w.exec(t)||C.exec(t),s=U(e[4])?1:+e[4];return[+e[1],+e[2],+e[3],s]},kt=t=>{const e=t.length,s=4===e||5===e;return[+("0x"+t[1]+t[s?1:2]),+("0x"+t[s?2:3]+t[s?2:4]),+("0x"+t[s?3:5]+t[s?3:6]),5===e||9===e?+(+("0x"+t[s?4:7]+t[s?4:8])/255).toFixed(3):1]},Rt=(t,e,s)=>(s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+(e-t)*(2/3-s)*6:t),At=t=>{const e=$.exec(t)||E.exec(t),s=+e[1]/360,i=+e[2]/100,r=+e[3]/100,n=U(e[4])?1:+e[4];let o,a,l;if(0===i)o=a=l=r;else{const t=r<.5?r*(1+i):r+i-r*i,e=2*r-t;o=gt(255*Rt(e,t,s+1/3),0),a=gt(255*Rt(e,t,s),0),l=gt(255*Rt(e,t,s-1/3),0)}return[o,a,l,n]},Dt=t=>G(t)?Nt(t):j(t)?kt(t):Z(t)?At(t):[0,0,0,1],Ot=(t,e)=>U(t)?e:t,It=(t,e,s,i,r,n)=>{let o;if(Y(t))o=()=>{const r=t(e,s,i,n);return isNaN(+r)?r||0:+r};else{if(!H(t)||!P(t,y))return t;o=()=>{const s=t.match(D),i=s[1],r=s[2];let n=getComputedStyle(e)?.getPropertyValue(i);return n&&n.trim()!==g||!r||(n=r.trim()),n||0}}return r&&(r.func=o),o()},Lt=(t,e)=>t[l]?t[h]&&tt(t,e)?i.ATTRIBUTE:_.includes(e)||v.get(e)?i.TRANSFORM:P(e,"--")?i.CSS_VAR:e in t.style?i.CSS:e in t?i.OBJECT:i.ATTRIBUTE:i.OBJECT,Bt=(t,e,s)=>{const i=t.style[e];i&&s&&(s[e]=i);const r=i||getComputedStyle(t[c]||t).getPropertyValue(e);return"auto"===r?"0":r},Mt=(t,e,s,r)=>{const n=U(s)?Lt(t,e):s;if(n===i.OBJECT){const s=t[e];return s&&r&&(r[e]=s),s||0}if(n===i.ATTRIBUTE){const s=t.getAttribute(e);return s&&r&&(r[e]=s),s}return n===i.TRANSFORM?$t(t,e,r):n===i.CSS_VAR?Bt(t,e,r).trimStart():Bt(t,e,r)},Pt=(t,e,s)=>"-"===s?t-e:"+"===s?t+e:t*e,Ft=()=>({t:r.NUMBER,n:0,u:null,o:null,d:null,s:null}),Vt=(t,e)=>{if(e.t=r.NUMBER,e.n=0,e.u=null,e.o=null,e.d=null,e.s=null,!t)return e;const s=+t;if(isNaN(s)){let s=t;"="===s[1]&&(e.o=s[0],s=s.slice(2));const i=!s.includes(" ")&&k.exec(s);if(i)return e.t=r.UNIT,e.n=+i[1],e.u=i[2],e;if(e.o)return e.n=+s,e;if(Q(s))return e.t=r.COLOR,e.d=Dt(s),e;{const t=s.match(N);return e.t=r.COMPLEX,e.d=t?t.map(Number):[],e.s=s.split(N)||[],e}}return e.n=s,e},Xt=(t,e)=>(e.t=t._valueType,e.n=t._toNumber,e.u=t._unit,e.o=null,e.d=Tt(t._toNumbers),e.s=Tt(t._strings),e),zt=Ft(),Ht=(t,e,s)=>{const i=t._modifier,r=t._fromNumbers,n=t._toNumbers,a=gt(ft(i(vt(r[0],n[0],e)),0,255),0),l=gt(ft(i(vt(r[1],n[1],e)),0,255),0),h=gt(ft(i(vt(r[2],n[2],e)),0,255),0),d=ft(i(gt(vt(r[3],n[3],e),s)),0,1);if(t._composition!==o.none){const e=t._numbers;e[0]=a,e[1]=l,e[2]=h,e[3]=d}return`rgba(${a},${l},${h},${d})`},Yt=(t,e,s)=>{const i=t._modifier,r=t._fromNumbers,n=t._toNumbers,a=t._strings,l=t._composition!==o.none;let h=a[0];for(let o=0,d=n.length;o{const h=t.parent,c=t.duration,p=t.completed,m=t.iterationDuration,f=t.iterationCount,g=t._currentIteration,y=t._loopDelay,v=t._reversed,_=t._alternate,b=t._hasChildren,T=t._delay,x=t._currentTime,S=T+m,w=e-T,C=ft(x,-T,c),$=ft(w,-T,c),E=w-x,N=$>0,k=$>=c,R=c<=u,A=l===n.FORCE;let D=0,O=w,I=0;if(f>1){const e=~~($/(m+(k?0:y)));t._currentIteration=ft(e,0,f),k&&t._currentIteration--,D=t._currentIteration%2,O=$%(m+y)||0}const B=v^(_&&D),M=t._ease;let P=k?B?0:c:B?m-O:O;M&&(P=m*M(P/m)||0);const F=(h?h.backwards:w=T&&e<=S||e<=T&&C>T||e>=S&&C!==c)||P>=S&&C!==c||P<=T&&C>0||e<=C&&C===c&&p||k&&!p&&R){if(N&&(t.computeDeltaTime(C),s||t.onBeforeUpdate(t)),!b){const e=A||(F?-1*E:E)>=L.tickThreshold,n=t._offset+(h?h._offset:0)+T+P;let l,c,u,p,m=t._head,f=0;for(;m;){const t=m._composition,s=m._currentTime,h=m._changeDuration,g=m._absoluteStartTime+m._changeDuration,y=m._nextRep,v=m._prevRep,_=t!==o.none;if((e||(s!==h||n<=g+(y?y._delay:0))&&(0!==s||n>=m._absoluteStartTime))&&(!_||!m._isOverridden&&(!m._isOverlapped||n<=g)&&(!y||y._isOverridden||n<=y._absoluteStartTime)&&(!v||v._isOverridden||n>=v._absoluteStartTime+v._changeDuration+m._delay))){const e=m._currentTime=ft(P-m._startTime,0,h),s=m._ease(e/m._updateDuration),n=m._modifier,g=m._valueType,y=m._tweenType,v=y===i.OBJECT,b=g===r.NUMBER,T=b&&v||0===s||1===s?-1:L.precision;let x,S;if(b?x=S=n(gt(vt(m._fromNumber,m._toNumber,s),T)):g===r.UNIT?(S=n(gt(vt(m._fromNumber,m._toNumber,s),T)),x=`${S}${m._unit}`):g===r.COLOR?x=Ht(m,s,T):g===r.COMPLEX&&(x=Yt(m,s,T)),_&&(m._number=S),a||t===o.blend)m._value=x;else{const t=m.property;l=m.target,v?l[t]=x:y===i.ATTRIBUTE?l.setAttribute(t,x):(c=l.style,y===i.TRANSFORM?(l!==u&&(u=l,p=l[d]),p[t]=x,f=1):y===i.CSS?c[t]=x:y===i.CSS_VAR&&c.setProperty(t,x)),N&&(I=1)}}f&&m._renderTransforms&&(c.transform=Et(p),f=0),m=m._next}!s&&I&&t.onRender(t)}!s&&N&&t.onUpdate(t)}return h&&R?!s&&(h.began&&!F&&w>0&&!p||F&&w<=u&&p)&&(t.onComplete(t),t.completed=!F):N&&k?f===1/0?t._startTime+=t.duration:t._currentIteration>=f-1&&(t.paused=!0,p||b||(t.completed=!0,s||h&&(F||!h.began)||(t.onComplete(t),t._resolve(t)))):t.completed=!1,I},Wt=(t,e,s,i,r)=>{const o=t._currentIteration;if(Ut(t,e,s,i,r),t._hasChildren){const a=t,l=a.backwards,h=i?e:a._iterationTime,d=F();let c=0,p=!0;if(!i&&a._currentIteration!==o){const t=a.iterationDuration;St(a,e=>{if(l){const i=e.duration,r=e._offset+e._delay;s||!(i<=u)||r&&r+i!==t||e.onComplete(e)}else!e.completed&&!e.backwards&&e._currentTime{const e=gt((h-t._offset)*t._speed,12),n=t._fps=a.duration&&(a.paused=!0,a.completed||(a.completed=!0,s||(a.onComplete(a),a._resolve(a))))}},qt={},jt=(t,e,s)=>{if(s===i.TRANSFORM)return v.get(t)||t;if(s===i.CSS||s===i.ATTRIBUTE&&q(e)&&t in e.style){const e=qt[t];if(e)return e;{const e=t?M(t):t;return qt[t]=e,e}}return t},Gt=(t,e=!1)=>{if(t._hasChildren)St(t,t=>Gt(t,e),!0);else{const s=t;s.pause(),St(s,t=>{const r=t.property,n=t.target,o=t._tweenType,a=t._inlineValue,h=W(a)||a===g;if(o===i.OBJECT)e||h||(n[r]=a);else if(n[l])if(o===i.ATTRIBUTE)e||(h?n.removeAttribute(r):n.setAttribute(r,a));else{const e=n.style;if(o===i.TRANSFORM){const s=n[d];h?delete s[r]:s[r]=a,t._renderTransforms&&(Object.keys(s).length?e.transform=Et(s):e.removeProperty("transform"))}else h?e.removeProperty(M(r)):e[r]=a}n[l]&&s._tail===t&&s.targets.forEach(t=>{t.getAttribute&&t.getAttribute("style")===g&&t.removeAttribute("style")})})}return t},Zt=t=>Gt(t,!0);class Qt{constructor(t=0){this.deltaTime=0,this._currentTime=t,this._lastTickTime=t,this._startTime=t,this._lastTime=t,this._scheduledTime=0,this._frameDuration=m/f,this._fps=f,this._speed=1,this._hasChildren=!1,this._head=null,this._tail=null}get fps(){return this._fps}set fps(t){const e=this._frameDuration,s=+t,i=sO.frameRate&&(O.frameRate=i),this._fps=i,this._frameDuration=r,this._scheduledTime+=r-e}get speed(){return this._speed}set speed(t){const e=+t;this._speed=e{let e=Jt.animation;return e||(e={duration:u,computeDeltaTime:T,_offset:0,_delay:0,_head:null,_tail:null},Jt.animation=e,Jt.update=()=>{t.forEach(t=>{for(let e in t){const s=t[e],i=s._head;if(i){const t=i._valueType,e=t===r.COMPLEX||t===r.COLOR?Tt(i._fromNumbers):null;let n=i._fromNumber,o=s._tail;for(;o&&o!==i;){if(e)for(let t=0,s=o._numbers.length;tt?requestAnimationFrame:setImmediate)(),ee=(()=>t?cancelAnimationFrame:clearImmediate)();class se extends Qt{constructor(t){super(t),this.useDefaultMainLoop=!0,this.pauseOnDocumentHidden=!0,this.defaults=O,this.paused=!0,this.reqId=0}update(){const t=this._currentTime=F();if(this.requestTick(t)){this.computeDeltaTime(t);const e=this._speed,s=this._fps;let i=this._head;for(;i;){const r=i._next;i.paused?(wt(this,i),this._hasChildren=!!this._tail,i._running=!1,i.completed&&!i._cancelled&&i.cancel()):Wt(i,(t-i._startTime)*i._speed*e,0,0,i._fpst.resetTime()),this.wake()}get speed(){return this._speed*(1===L.timeScale?1:m)}set speed(t){this._speed=t*L.timeScale,St(this,t=>t.speed=t._speed)}get timeUnit(){return 1===L.timeScale?"ms":"s"}set timeUnit(t){const e="s"===t,s=e?.001:1;if(L.timeScale!==s){L.timeScale=s,L.tickThreshold=200*s;const t=e?.001:m;this.defaults.duration*=t,this._speed*=t}}get precision(){return L.precision}set precision(t){L.precision=t}}const ie=(()=>{const e=new se(F());return t&&(B.engine=e,s.addEventListener("visibilitychange",()=>{e.pauseOnDocumentHidden&&(s.hidden?e.pause():e.resume())})),e})(),re=()=>{ie._head?(ie.reqId=te(re),ie.update()):ie.reqId=0},ne=()=>(ee(ie.reqId),ie.reqId=0,ie),oe={_rep:new WeakMap,_add:new Map},ae=(t,e,s="_rep")=>{const i=oe[s];let r=i.get(t);return r||(r={},i.set(t,r)),r[e]?r[e]:r[e]={_head:null,_tail:null}},le=(t,e)=>t._isOverridden||t._absoluteStartTime>e._absoluteStartTime,he=t=>{t._isOverlapped=1,t._isOverridden=1,t._changeDuration=u,t._currentTime=u},de=(t,e)=>{const s=t._composition;if(s===o.replace){const s=t._absoluteStartTime;Ct(e,t,le,"_prevRep","_nextRep");const i=t._prevRep;if(i){const e=i.parent,r=i._absoluteStartTime+i._changeDuration;if(t.parent.id!==e.id&&e.iterationCount>1&&r+(e.duration-e.iterationDuration)>s){he(i);let t=i._prevRep;for(;t&&t.parent.id===e.id;)he(t),t=t._prevRep}const n=s-t._delay;if(r>n){const t=i._startTime,e=r-(t+i._updateDuration),s=gt(n-e-t,12);i._changeDuration=s,i._currentTime=s,i._isOverlapped=1,s{t._isOverlapped||(o=!1)}),o){const t=e.parent;if(t){let s=!0;St(t,t=>{t!==e&&St(t,t=>{t._isOverlapped||(s=!1)})}),s&&t.cancel()}else e.cancel()}}}else if(s===o.blend){const e=ae(t.target,t.property,"_add"),s=Kt(oe._add);let i=e._head;i||(i={...t},i._composition=o.replace,i._updateDuration=u,i._startTime=0,i._numbers=Tt(t._fromNumbers),i._number=0,i._next=null,i._prev=null,Ct(e,i),Ct(s,i));const r=t._toNumber;if(t._fromNumber=i._fromNumber-r,t._toNumber=0,t._numbers=Tt(t._fromNumbers),t._number=0,i._fromNumber=r,t._toNumbers){const e=Tt(t._toNumbers);e&&e.forEach((e,s)=>{t._fromNumbers[s]=i._fromNumbers[s]-e,t._toNumbers[s]=0}),i._fromNumbers=e}Ct(e,t,null,"_prevAdd","_nextAdd")}return t},ce=t=>{const e=t._composition;if(e!==o.none){const s=t.target,i=t.property,r=oe._rep.get(s)[i];if(wt(r,t,"_prevRep","_nextRep"),e===o.blend){const e=oe._add,r=e.get(s);if(!r)return;const n=r[i],o=Jt.animation;wt(n,t,"_prevAdd","_nextAdd");const a=n._head;if(a&&a===n._tail){wt(n,a,"_prevAdd","_nextAdd"),wt(o,a);let t=!0;for(let e in r)if(r[e]._head){t=!1;break}t&&e.delete(s)}}}return t},ue=(t,e,s)=>{let r=!1;return St(e,n=>{const o=n.target;if(t.includes(o)){const t=n.property,a=n._tweenType,l=jt(s,o,a);(!l||l&&l===t)&&(n.parent._tail===n&&n._tweenType===i.TRANSFORM&&n._prev&&n._prev._tweenType===i.TRANSFORM&&(n._prev._renderTransforms=1),wt(e,n),ce(n),r=!0)}},!0),r},pe=(t,e,s)=>{const i=e||ie;let r;if(i._hasChildren){let e=0;St(i,n=>{if(!n._hasChildren)if(r=ue(t,n,s),r&&!n._head)n.cancel(),wt(i,n);else{const t=n._offset+n._delay+n.duration;t>e&&(e=t)}n._head?pe(t,n,s):n._hasChildren=!1},!0),U(i.iterationDuration)||(i.iterationDuration=e)}else r=ue(t,i,s);r&&!i._head&&(i._hasChildren=!1,i.cancel&&i.cancel())},me=t=>(t.paused=!0,t.began=!1,t.completed=!1,t),fe=t=>t._cancelled?(t._hasChildren?St(t,fe):St(t,t=>{t._composition!==o.none&&de(t,ae(t.target,t.property))}),t._cancelled=0,t):t;let ge=0;const ye=(t,e)=>t._priority>e._priority;class ve extends Qt{constructor(t={},e=null,s=0){super(0),++ge;const{id:i,delay:r,duration:n,reversed:o,alternate:a,loop:l,loopDelay:h,autoplay:d,frameRate:c,playbackRate:p,priority:m,onComplete:f,onLoop:g,onPause:y,onBegin:v,onBeforeUpdate:_,onUpdate:b}=t;I.current&&I.current.register(this);const x=e?0:ie._lastTickTime,S=e?e.defaults:L.defaults,w=Y(r)||U(r)?S.delay:+r,C=Y(n)||U(n)?1/0:+n,$=Ot(l,S.loop),E=Ot(h,S.loopDelay);let N=!0===$||$===1/0||$<0?1/0:$+1,k=0;e?k=s:(ie.reqId||ie.requestTick(F()),k=(ie._lastTickTime-ie._startTime)*L.timeScale),this.id=U(i)?ge:i,this.parent=e,this.duration=_t((C+E)*N-E)||u,this.backwards=!1,this.paused=!0,this.began=!1,this.completed=!1,this.onBegin=v||S.onBegin,this.onBeforeUpdate=_||S.onBeforeUpdate,this.onUpdate=b||S.onUpdate,this.onLoop=g||S.onLoop,this.onPause=y||S.onPause,this.onComplete=f||S.onComplete,this.iterationDuration=C,this.iterationCount=N,this._autoplay=!e&&Ot(d,S.autoplay),this._offset=k,this._delay=w,this._loopDelay=E,this._iterationTime=0,this._currentIteration=0,this._resolve=T,this._running=!1,this._reversed=+Ot(o,S.reversed),this._reverse=this._reversed,this._cancelled=0,this._alternate=Ot(a,S.alternate),this._prev=null,this._next=null,this._lastTickTime=x,this._startTime=x,this._lastTime=x,this._fps=Ot(c,S.frameRate),this._speed=Ot(p,S.playbackRate),this._priority=+Ot(m,1)}get cancelled(){return!!this._cancelled}set cancelled(t){t?this.cancel():this.reset(!0).play()}get currentTime(){return ft(gt(this._currentTime,L.precision),-this._delay,this.duration)}set currentTime(t){const e=this.paused;this.pause().seek(+t),e||this.resume()}get iterationCurrentTime(){return ft(gt(this._iterationTime,L.precision),0,this.iterationDuration)}set iterationCurrentTime(t){this.currentTime=this.iterationDuration*this._currentIteration+t}get progress(){return ft(gt(this._currentTime/this.duration,10),0,1)}set progress(t){this.currentTime=this.duration*t}get iterationProgress(){return ft(gt(this._iterationTime/this.iterationDuration,10),0,1)}set iterationProgress(t){const e=this.iterationDuration;this.currentTime=e*this._currentIteration+e*t}get currentIteration(){return this._currentIteration}set currentIteration(t){this.currentTime=this.iterationDuration*ft(+t,0,this.iterationCount-1)}get reversed(){return!!this._reversed}set reversed(t){t?this.reverse():this.play()}get speed(){return super.speed}set speed(t){super.speed=t,this.resetTime()}reset(t=!1){return fe(this),this._reversed&&!this._reverse&&(this.reversed=!1),this._iterationTime=this.iterationDuration,Wt(this,0,1,~~t,n.FORCE),me(this),this._hasChildren&&St(this,me),this}init(t=!1){this.fps=this._fps,this.speed=this._speed,!t&&this._hasChildren&&Wt(this,this.duration,1,~~t,n.FORCE),this.reset(t);const e=this._autoplay;return!0===e?this.resume():e&&!U(e.linked)&&e.link(this),this}resetTime(){const t=1/(this._speed*ie._speed);return this._startTime=F()-(this._currentTime+this._delay)*t,this}pause(){return this.paused||(this.paused=!0,this.onPause(this)),this}resume(){return this.paused?(this.paused=!1,this.duration<=u&&!this._hasChildren?Wt(this,u,0,0,n.FORCE):(this._running||(Ct(ie,this,ye),ie._hasChildren=!0,this._running=!0),this.resetTime(),this._startTime-=12,ie.wake()),this):this}restart(){return this.reset().resume()}seek(t,e=0,s=0){fe(this),this.completed=!1;const i=this.paused;return this.paused=!0,Wt(this,t+this._delay,~~e,~~s,n.AUTO),i?this:this.resume()}alternate(){const t=this._reversed,e=this.iterationCount,s=this.iterationDuration,i=e===1/0?ht(p/s):e;return this._reversed=+(!this._alternate||i%2?!t:t),e===1/0?this.iterationProgress=this._reversed?1-this.iterationProgress:this.iterationProgress:this.seek(s*i-this._currentTime),this.resetTime(),this}play(){return this._reversed&&this.alternate(),this.resume()}reverse(){return this._reversed||this.alternate(),this.resume()}cancel(){return this._hasChildren?St(this,t=>t.cancel(),!0):St(this,ce),this._cancelled=1,this.pause()}stretch(t){const e=this.duration,s=bt(t);if(e===s)return this;const i=t/e,r=t<=u;return this.duration=r?u:s,this.iterationDuration=r?u:bt(this.iterationDuration*i),this._offset*=i,this._delay*=i,this._loopDelay*=i,this}revert(){Wt(this,0,1,0,n.AUTO);const t=this._autoplay;return t&&t.linked&&t.linked===this&&t.revert(),this.cancel()}complete(t=0){return this.seek(this.duration,t).cancel()}then(t=T){const e=this.then,s=()=>{this.then=null,t(this),this.then=e,this._resolve=T};return new Promise(t=>(this._resolve=()=>t(s()),this.completed&&this._resolve(),this))}}const _e=t=>new ve(t,null,0).init();function be(t){const e=H(t)?I.root.querySelectorAll(t):t;if(e instanceof NodeList||e instanceof HTMLCollection)return e}function Te(e){if(W(e))return[];if(!t)return V(e)&&e.flat(1/0)||[e];if(V(e)){const t=e.flat(1/0),s=[];for(let e=0,i=t.length;e{const o=e.u,a=e.n;if(e.t===r.UNIT&&o===i)return e;const l=a+o+i,h=we[l];if(U(h)||n){let r;if(o in Se)r=a*Se[o]/Se[i];else{const e=100,n=t.cloneNode(),l=t.parentNode,h=l&&l!==s?l:s.body;h.appendChild(n);const d=n.style;d.width=e+o;const c=n.offsetWidth||e;d.width=e+i;const u=c/(n.offsetWidth||e);h.removeChild(n),r=u*a}e.n=r,we[l]=r}else e.n=h;return e.t,e.u=i,e},$e=t=>t,Ee=(t=1.68)=>e=>st(e,+t),Ne={in:t=>e=>t(e),out:t=>e=>1-t(1-e),inOut:t=>e=>e<.5?t(2*e)/2:1-t(-2*e+2)/2,outIn:t=>e=>e<.5?(1-t(1-2*e))/2:(t(2*e-1)+1)/2},ke=pt/2,Re=2*pt,Ae={[g]:Ee,Quad:Ee(2),Cubic:Ee(3),Quart:Ee(4),Quint:Ee(5),Sine:t=>1-nt(t*ke),Circ:t=>1-it(1-t*t),Expo:t=>t?st(2,10*t-10):0,Bounce:t=>{let e,s=4;for(;t<((e=st(2,--s))-1)/11;);return 1/st(4,3-s)-7.5625*st((3*e-2)/22-t,2)},Back:(t=1.7)=>e=>(+t+1)*e*e*e-+t*e*e,Elastic:(t=1,e=.3)=>{const s=ft(+t,1,10),i=ft(+e,u,2),r=i/Re*dt(1/s),n=Re/i;return t=>0===t||1===t?t:-s*st(2,-10*(1-t))*rt((1-t-r)*n)}},De=(()=>{const t={linear:$e,none:$e};for(let e in Ne)for(let s in Ae){const i=Ae[s],r=Ne[e];t[e+s]=s===g||"Back"===s||"Elastic"===s?(t,e)=>r(i(t,e)):r(i)}return t})(),Oe={linear:$e,none:$e},Ie=t=>{if(Oe[t])return Oe[t];if(t.indexOf("(")<=-1){const e=Ne[t]||t.includes("Back")||t.includes("Elastic")?De[t]():De[t];return e?Oe[t]=e:$e}{const e=t.slice(0,-1).split("("),s=De[e[0]];return s?Oe[t]=s(...e[1].split(",")):$e}},Le=["steps(","irregular(","linear(","cubicBezier("],Be=t=>{if(H(t))for(let e=0,s=Le.length;e{const s={};if(V(t)){const e=[].concat(...t.map(t=>Object.keys(t))).filter(J);for(let i=0,r=e.length;i{const e={};for(let s in t){const i=t[s];J(s)?s===r&&(e.to=i):e[s]=i}return e});s[r]=n}}else{const i=Ot(e.duration,L.defaults.duration),r=Object.keys(t).map(e=>({o:parseFloat(e)/100,p:t[e]})).sort((t,e)=>t.o-e.o);r.forEach(t=>{const e=t.o,r=t.p;for(let t in r)if(J(t)){let n=s[t];n||(n=s[t]=[]);const o=e*i;let a=n.length,l=n[a-1];const h={to:r[t]};let d=0;for(let t=0;t=m?o.none:U(x)?w.composition:x,I=this._offset+(s?s._offset:0);N&&(C.parent=this);let B=NaN,M=NaN,P=0,F=0;for(let t=0;t2&&e?(Ue=[],d.forEach((t,e)=>{e?1===e?(He[1]=t,Ue.push(He)):Ue.push(t):He[0]=t})):Ue=d}else ze[0]=d,Ue=ze;let y=null,v=null,_=NaN,b=0,T=0;for(let t=Ue.length;T1?It(R,e,n,c,null,m)/t:R),e,n,c,null,m),L=It(Ot(We.delay,T?0:A),e,n,c,null,m),B=We.modifier||D,M=!U(x),q=!U(g),j=V(g),G=j||M&&q,Z=v?b+L:L,Q=gt(I+Z,12);F||!M&&!j||(F=1);let J=v;if(d!==o.none){let t=y._head;for(;t&&!t._isOverridden&&t._absoluteStartTime<=Q;)if(J=t,t=t._nextRep,t&&t._absoluteStartTime>=Q)for(;t;)he(t),t=t._nextRep}if(G){Vt(j?It(g[0],e,n,c,Xe,m):x,Me),Vt(j?It(g[1],e,n,c,Ve,m):g,Pe);const t=Mt(e,h,l,Fe);Me.t===r.NUMBER&&(J?J._valueType===r.UNIT&&(Me.t=r.UNIT,Me.u=J._unit):(Vt(t,zt),zt.t===r.UNIT&&(Me.t=r.UNIT,Me.u=zt.u)))}else q?Vt(g,Pe):v?Xt(v,Pe):Vt(s&&J&&J.parent.parent===s?J._value:Mt(e,h,l,Fe),Pe),M?Vt(x,Me):v?Xt(v,Me):Vt(s&&J&&J.parent.parent===s?J._value:Mt(e,h,l,Fe),Me);if(Me.o&&(Me.n=Pt(J?J._toNumber:Vt(Mt(e,h,l,Fe),zt).n,Me.n,Me.o)),Pe.o&&(Pe.n=Pt(Me.n,Pe.n,Pe.o)),Me.t!==Pe.t)if(Me.t===r.COMPLEX||Pe.t===r.COMPLEX){const t=Me.t===r.COMPLEX?Me:Pe,e=Me.t===r.COMPLEX?Pe:Me;e.t=r.COMPLEX,e.s=Tt(t.s),e.d=t.d.map(()=>e.n)}else if(Me.t===r.UNIT||Pe.t===r.UNIT){const t=Me.t===r.UNIT?Me:Pe,e=Me.t===r.UNIT?Pe:Me;e.t=r.UNIT,e.u=t.u}else if(Me.t===r.COLOR||Pe.t===r.COLOR){const t=Me.t===r.COLOR?Me:Pe,e=Me.t===r.COLOR?Pe:Me;e.t=r.COLOR,e.s=t.s,e.d=[0,0,0,1]}if(Me.u!==Pe.u){let t=Pe.u?Me:Pe;t=Ce(e,t,Pe.u?Pe.u:Me.u,!1)}if(Pe.d&&Me.d&&Pe.d.length!==Me.d.length){const t=Me.d.length>Pe.d.length?Me:Pe,e=t===Me?Pe:Me;e.d=t.d.map((t,s)=>U(e.d[s])?0:e.d[s]),e.s=Tt(t.s)}const K=gt(+N||u,12);let tt=Fe[h];W(tt)||(Fe[h]=null);const et={parent:this,id:qe++,property:h,target:e,_value:null,_toFunc:Ve.func,_fromFunc:Xe.func,_ease:Be(E),_fromNumbers:Tt(Me.d),_toNumbers:Tt(Pe.d),_strings:Tt(Pe.s),_fromNumber:Me.n,_toNumber:Pe.n,_numbers:Tt(Me.d),_number:Me.n,_unit:Pe.u,_modifier:B,_currentTime:0,_startTime:Z,_delay:+L,_updateDuration:K,_changeDuration:K,_absoluteStartTime:Q,_tweenType:l,_valueType:Pe.t,_composition:d,_isOverlapped:0,_isOverridden:0,_renderTransforms:0,_inlineValue:tt,_prevRep:null,_nextRep:null,_prevAdd:null,_nextAdd:null,_prev:null,_next:null};d!==o.none&&de(et,y);const st=et._valueType;et._value=st===r.COMPLEX?Yt(et,1,-1):st===r.COLOR?Ht(et,1,-1):st===r.UNIT?`${B(et._toNumber)}${et._unit}`:B(et._toNumber),isNaN(_)&&(_=et._startTime),b=gt(Z+K,12),v=et,P++,Ct(this,et)}(isNaN(M)||_B)&&(B=b),l===i.TRANSFORM&&(p=P-T,m=P)}if(!isNaN(p)){let t=0;St(this,e=>{t>=p&&t{t.id===e.id&&(t._renderTransforms=1)})),t++})}}c||console.warn("No target found. Make sure the element you're trying to animate is accessible before creating your animation."),M?(St(this,t=>{t._startTime-t._delay||(t._delay-=M),t._startTime-=M}),B-=M):M=0,B||(B=u,this.iterationCount=0),this.targets=d,this.id=U(g)?je:g,this.duration=B===u?u:_t((B+this._loopDelay)*this.iterationCount-this._loopDelay)||u,this.onRender=S||w.onRender,this._ease=E,this._delay=M,this.iterationDuration=B,!this._autoplay&&F&&this.onRender(this)}stretch(t){const e=this.duration;if(e===bt(t))return this;const s=t/e;return St(this,t=>{t._updateDuration=bt(t._updateDuration*s),t._changeDuration=bt(t._changeDuration*s),t._currentTime*=s,t._startTime*=s,t._absoluteStartTime*=s}),super.stretch(t)}refresh(){return St(this,t=>{const e=t._toFunc,s=t._fromFunc;(e||s)&&(s?(Vt(s(),Me),Me.u!==t._unit&&t.target[l]&&Ce(t.target,Me,t._unit,!0),t._fromNumbers=Tt(Me.d),t._fromNumber=Me.n):e&&(Vt(Mt(t.target,t.property,t._tweenType),zt),t._fromNumbers=Tt(zt.d),t._fromNumber=zt.n),e&&(Vt(e(),Pe),t._toNumbers=Tt(Pe.d),t._strings=Tt(Pe.s),t._toNumber=Pe.o?Pt(t._fromNumber,Pe.n,Pe.o):Pe.n))}),this.duration===u&&this.restart(),this}revert(){return super.revert(),Gt(this)}then(t){return super.then(t)}}const Qe=(t,e)=>L.editor?L.editor.addAnimation(t,e):new Ze(t,e,null,0,!1).init(),Je=(t,e)=>{if(P(e,"<")){const s="<"===e[1],i=t._tail,r=i?i._offset+i._delay:0;return s?r:r+i.duration}},Ke=(t,e)=>{let s=t.iterationDuration;if(s===u&&(s=0),U(e))return s;if(z(+e))return+e;const i=e,r=t?t.labels:null,n=!W(r),o=Je(t,i),a=!U(o),l=A.exec(i);if(l){const t=l[0],e=i.split(t),h=n&&e[0]?r[e[0]]:s,d=a?o:n?h:s,c=+e[1];return Pt(d,c,t[0])}return a?o:n?U(r[i])?s:r[i]:s};function ts(t){return _t((t.iterationDuration+t._loopDelay)*t.iterationCount-t._loopDelay)||u}function es(t,e,s,i,r,o){const a=z(t.duration)&&t.duration<=u?s-u:s;e.composition&&Wt(e,a,1,1,n.AUTO);const l=i?new Ze(i,t,e,a,!1,r,o):new ve(t,e,a);return e.composition&&l.init(!0),Ct(e,l),St(e,t=>{const s=t._offset+t._delay+t.duration;s>e.iterationDuration&&(e.iterationDuration=s)}),e.duration=ts(e),e}let ss=0;class is extends ve{constructor(t={}){super(t,null,0),++ss,this.id=U(t.id)?ss:t.id,this.duration=0,this.labels={};const e=t.defaults,s=L.defaults;this.defaults=e?xt(e,s):s,this.composition=Ot(t.composition,!0),this.onRender=t.onRender||s.onRender;const i=Ot(t.playbackEase,s.playbackEase);this._ease=i?Be(i):null,this.iterationDuration=0}add(t,e,s){const i=X(e),r=X(t);if(i||r){if(this._hasChildren=!0,i){const i=e,r=L.editor&&L.editor.addTimelineChild,n=s&&"Stagger"===s.type&&L.editor,o=Y(s)?s:null;if(o||n){const e=Te(t),n=this.duration,a=this.iterationDuration,l=i.id;let h=0;const d=e.length,c=r?r(t,i,this.id,s,d):null,u=o||L.editor.resolveStagger(s.defaultValue);e.forEach(t=>{const s={...c||i};this.duration=n,this.iterationDuration=a,U(l)||(s.id=l+"-"+h),es(s,this,Ke(this,u(t,h,e,null,this)),t,h,e),h++})}else{const e=r?r(t,i,this.id,s):i,n=s&&s.type?s.defaultValue:s;es(e,this,Ke(this,n),t)}}else es(t,this,Ke(this,e));return this.composition&&this.init(!0),this}}sync(t,e){if(U(t)||t&&U(t.pause))return this;t.pause();const s=+(t.effect?t.effect.getTiming().duration:t.duration);return U(t)||U(t.persist)||(t.persist=!0),this.add(t,{currentTime:[0,s],duration:s,delay:0,ease:"linear",playbackEase:"linear"},e)}set(t,e,s){return U(e)?this:(e.duration=u,e.composition=o.replace,this.add(t,e,s))}call(t,e){return U(t)||t&&!Y(t)?this:this.add({duration:0,delay:0,onComplete:()=>t(this)},e)}label(t,e){return U(t)||t&&!H(t)||(this.labels[t]=Ke(this,e)),this}remove(t,e){return pe(Te(t),this,e),this}stretch(t){const e=this.duration;if(e===bt(t))return this;const s=t/e,i=this.labels;St(this,t=>t.stretch(t.duration*s));for(let t in i)i[t]*=s;return super.stretch(t)}refresh(){return St(this,t=>{t.refresh&&t.refresh()}),this}revert(){return super.revert(),St(this,t=>t.revert,!0),Gt(this)}then(t){return super.then(t)}}const rs=t=>L.editor?L.editor.addTimeline(t):new is(t).init();class ns{constructor(t,e){I.current&&I.current.register(this);const s=()=>{if(this.callbacks.completed)return;let t=!0;for(let e in this.animations)if(!this.animations[e].paused&&t){t=!1;break}t&&this.callbacks.complete()},i={onBegin:()=>{this.callbacks.completed&&this.callbacks.reset(),this.callbacks.play()},onComplete:s,onPause:s},r={v:1,autoplay:!1},n={};if(this.targets=[],this.animations={},this.callbacks=null,!U(t)&&!U(e)){for(let t in e){const s=e[t];J(t)?n[t]=s:P(t,"on")?r[t]=s:i[t]=s}this.callbacks=new Ze({v:0},r);for(let e in n){const s=n[e],r=X(s);let a={},l="+=0";if(r){const t=s.unit;H(t)&&(l+=t)}else a.duration=s;a[e]=r?xt({to:l},s):l;const h=xt(i,a);h.composition=o.replace,h.autoplay=!1;const d=this.animations[e]=new Ze(t,h,null,0,!1).init();this.targets.length||this.targets.push(...d.targets),this[e]=(t,e,s)=>{const i=d._head;if(U(t)&&i){const t=i._numbers;return t&&t.length?t:i._modifier(i._number)}return St(d,e=>{if(V(t))for(let s=0,i=t.length;snew ns(t,e),as=(t,e)=>(+t).toFixed(e),ls=(t,e,s)=>`${t}`.padStart(e,s),hs=(t,e,s)=>`${t}`.padEnd(e,s),ds=(t,e,s)=>((t-e)%(s-e)+(s-e))%(s-e)+e,cs=(t,e,s,i,r)=>i+(t-e)/(s-e)*(r-i),us=t=>t*Math.PI/180,ps=t=>180*t/Math.PI,ms=(t,e,s,i)=>i?1===i?e:vt(t,e,1-Math.exp(-i*s*.1)):t;var fs=Object.freeze({__proto__:null,clamp:ft,damp:ms,degToRad:us,lerp:vt,mapRange:cs,padEnd:hs,padStart:ls,radToDeg:ps,round:gt,roundPad:as,snap:yt,wrap:ds});const gs=10*m;class ys{constructor(t={}){const e=!U(t.bounce)||!U(t.duration);this.timeStep=.02,this.restThreshold=5e-4,this.restDuration=200,this.maxDuration=6e4,this.maxRestSteps=this.restDuration/this.timeStep/m,this.maxIterations=this.maxDuration/this.timeStep/m,this.bn=ft(Ot(t.bounce,.5),-1,1),this.pd=ft(Ot(t.duration,628),10*L.timeScale,gs*L.timeScale),this.m=ft(Ot(t.mass,1),1,gs),this.s=ft(Ot(t.stiffness,100),u,gs),this.d=ft(Ot(t.damping,10),u,gs),this.v=ft(Ot(t.velocity,0),-1e4,gs),this.w0=0,this.zeta=0,this.wd=0,this.b=0,this.completed=!1,this.solverDuration=0,this.settlingDuration=0,this.parent=null,this.onComplete=t.onComplete||T,e&&this.calculateSDFromBD(),this.compute(),this.ease=t=>{const e=t*this.settlingDuration,s=this.completed,i=this.pd;return e>=i&&!s&&(this.completed=!0,this.onComplete(this.parent)),e=0?this.d=4*(1-this.bn)*pt/t:this.d=4*pt/(t*(1+this.bn)),this.s=gt(ft(this.s,u,gs),3),this.d=gt(ft(this.d,u,300),3)}calculateBDFromSD(){const t=2*pt/it(this.s);this.pd=t*(1===L.timeScale?m:1);const e=this.d/(2*it(this.s));this.bn=e<=1?1-this.d*t/(4*pt):4*pt/(this.d*t)-1,this.bn=gt(ft(this.bn,-1,1),3),this.pd=gt(ft(this.pd,10*L.timeScale,gs*L.timeScale),3)}compute(){const{maxRestSteps:t,maxIterations:e,restThreshold:s,timeStep:i,m:r,d:n,s:o,v:a}=this,l=this.w0=ft(it(o/r),u,m),h=this.zeta=n/(2*it(o*r));h<1?(this.wd=l*it(1-h*h),this.b=(h*l-a)/this.wd):1===h?(this.wd=0,this.b=-a+l):(this.wd=l*it(h*h-1),this.b=(h*l-a)/this.wd);let d=0,c=0,p=0;for(;c<=t&&p<=e;)ot(1-this.solve(d))new ys(t),_s=t=>(console.warn("createSpring() is deprecated use spring() instead"),new ys(t)),bs={_head:null,_tail:null},Ts=(t,e,s)=>{let i,r=bs._head;for(;r;){const n=r._next,o=r.$el===t,a=!e||r.property===e,l=!s||r.parent===s;if(o&&a&&l){i=r.animation;try{i.commitStyles()}catch{}i.cancel(),wt(bs,r);const t=r.parent;t&&(t._completed++,t.animations.length===t._completed&&(t.completed=!0,t.paused=!0,t.muteCallbacks||(t.onComplete(t),t._resolve(t))))}r=n}return i},xs=(t,e,s,i,r)=>{const n=e.animate(i,r),o=r.delay+ +r.duration*r.iterations;n.playbackRate=t._speed,t.paused&&n.pause(),t.durationTs(e,s,t);return n.oncancel=a,n.onremove=a,t.persist||(n.onfinish=a),n};function Ss(t,e,s){const i=xe(t);if(!i.length)return;const[n]=i,o=Lt(n,e),a=jt(e,n,o);let l=Mt(n,a);if(U(s))return l;if(Vt(l,zt),zt.t===r.NUMBER||zt.t===r.UNIT){if(!1===s)return zt.n;{const t=Ce(n,zt,s,!1);return`${gt(t.n,L.precision)}${t.u}`}}}const ws=(t,e)=>{if(!U(e))return e.duration=u,e.composition=Ot(e.composition,o.none),new Ze(t,e,null,0,!0).resume()},Cs=(t,e,s)=>{const i=Te(t);for(let t=0,r=i.length;t{t.cancelable&&t.preventDefault()};class Es{constructor(t){this.el=t,this.zIndex=0,this.parentElement=null,this.classList={add:T,remove:T}}get x(){return this.el.x||0}set x(t){this.el.x=t}get y(){return this.el.y||0}set y(t){this.el.y=t}get width(){return this.el.width||0}set width(t){this.el.width=t}get height(){return this.el.height||0}set height(t){this.el.height=t}getBoundingClientRect(){return{top:this.y,right:this.x,bottom:this.y+this.height,left:this.x+this.width}}}class Ns{constructor(t){this.$el=t,this.inlineTransforms=[],this.point=new DOMPoint,this.inversedMatrix=this.getMatrix().inverse()}normalizePoint(t,e){return this.point.x=t,this.point.y=e,this.point.matrixTransform(this.inversedMatrix)}traverseUp(t){let e=this.$el.parentElement,i=0;for(;e&&e!==s;)t(e,i),e=e.parentElement,i++}getMatrix(){const t=new DOMMatrix;return this.traverseUp(e=>{const s=getComputedStyle(e).transform;if(s){const e=new DOMMatrix(s);t.preMultiplySelf(e)}}),t}remove(){this.traverseUp((t,e)=>{this.inlineTransforms[e]=t.style.transform,t.style.transform="none"})}revert(){this.traverseUp((t,e)=>{const s=this.inlineTransforms[e];""===s?t.style.removeProperty("transform"):t.style.transform=s})}}const ks=(t,e)=>t&&Y(t)?t(e):t;let Rs=0;class As{constructor(t,i={}){if(!t)return;I.current&&I.current.register(this);const r=i.x,n=i.y,o=i.trigger,a=i.modifier,l=i.releaseEase,h=l&&Be(l),d=!U(l)&&!U(l.ease),c=X(r)&&!U(r.mapTo)?r.mapTo:"translateX",u=X(n)&&!U(n.mapTo)?n.mapTo:"translateY",m=ks(i.container,this);this.containerArray=V(m)?m:null,this.$container=m&&!this.containerArray?Te(m)[0]:s.body,this.useWin=this.$container===s.body,this.$scrollContainer=this.useWin?e:this.$container,this.$target=X(t)?new Es(t):Te(t)[0],this.$trigger=Te(o||t)[0],this.fixed="fixed"===Ss(this.$target,"position"),this.isFinePointer=!0,this.containerPadding=[0,0,0,0],this.containerFriction=0,this.releaseContainerFriction=0,this.snapX=0,this.snapY=0,this.scrollSpeed=0,this.scrollThreshold=0,this.dragSpeed=0,this.dragThreshold=3,this.maxVelocity=0,this.minVelocity=0,this.velocityMultiplier=0,this.cursor=!1,this.releaseXSpring=d?l:vs({mass:Ot(i.releaseMass,1),stiffness:Ot(i.releaseStiffness,80),damping:Ot(i.releaseDamping,20)}),this.releaseYSpring=d?l:vs({mass:Ot(i.releaseMass,1),stiffness:Ot(i.releaseStiffness,80),damping:Ot(i.releaseDamping,20)}),this.releaseEase=h||De.outQuint,this.hasReleaseSpring=d,this.onGrab=i.onGrab||T,this.onDrag=i.onDrag||T,this.onRelease=i.onRelease||T,this.onUpdate=i.onUpdate||T,this.onSettle=i.onSettle||T,this.onSnap=i.onSnap||T,this.onResize=i.onResize||T,this.onAfterResize=i.onAfterResize||T,this.disabled=[0,0];const f={};if(a&&(f.modifier=a),U(r)||!0===r)f[c]=0;else if(X(r)){const t=r,e={};t.modifier&&(e.modifier=t.modifier),t.composition&&(e.composition=t.composition),f[c]=e}else!1===r&&(f[c]=0,this.disabled[0]=1);if(U(n)||!0===n)f[u]=0;else if(X(n)){const t=n,e={};t.modifier&&(e.modifier=t.modifier),t.composition&&(e.composition=t.composition),f[u]=e}else!1===n&&(f[u]=0,this.disabled[1]=1);this.animate=new ns(this.$target,f),this.xProp=c,this.yProp=u,this.destX=0,this.destY=0,this.deltaX=0,this.deltaY=0,this.scroll={x:0,y:0},this.coords=[this.x,this.y,0,0],this.snapped=[0,0],this.pointer=[0,0,0,0,0,0,0,0],this.scrollView=[0,0],this.dragArea=[0,0,0,0],this.containerBounds=[-p,p,p,-p],this.scrollBounds=[0,0,0,0],this.targetBounds=[0,0,0,0],this.window=[0,0],this.velocityStack=[0,0,0],this.velocityStackIndex=0,this.velocityTime=F(),this.velocity=0,this.angle=0,this.cursorStyles=null,this.triggerStyles=null,this.bodyStyles=null,this.targetStyles=null,this.touchActionStyles=null,this.transforms=new Ns(this.$target),this.overshootCoords={x:0,y:0},this.overshootTicker=new ve({autoplay:!1,onUpdate:()=>{this.updated=!0,this.manual=!0,this.disabled[0]||this.animate[this.xProp](this.overshootCoords.x,1),this.disabled[1]||this.animate[this.yProp](this.overshootCoords.y,1)},onComplete:()=>{this.manual=!1,this.disabled[0]||this.animate[this.xProp](this.overshootCoords.x,0),this.disabled[1]||this.animate[this.yProp](this.overshootCoords.y,0)}},null,0).init(),this.updateTicker=new ve({autoplay:!1,onUpdate:()=>this.update()},null,0).init(),this.contained=!U(m),this.manual=!1,this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.enabled=!1,this.initialized=!1,this.activeProp=this.disabled[1]?c:u,this.animate.callbacks.onRender=()=>{const t=this.updated,e=!(this.grabbed&&t)&&this.released,s=this.x,i=this.y,r=s-this.coords[2],n=i-this.coords[3];this.deltaX=r,this.deltaY=n,this.coords[2]=s,this.coords[3]=i,t&&(r||n)&&this.onUpdate(this),e?(this.computeVelocity(r,n),this.angle=ut(n,r)):this.updated=!1},this.animate.callbacks.onComplete=()=>{!this.grabbed&&this.released&&(this.released=!1),this.manual||(this.deltaX=0,this.deltaY=0,this.velocity=0,this.velocityStack[0]=0,this.velocityStack[1]=0,this.velocityStack[2]=0,this.velocityStackIndex=0,this.onSettle(this))},this.resizeTicker=new ve({autoplay:!1,duration:150*L.timeScale,onComplete:()=>{this.onResize(this),this.refresh(),this.onAfterResize(this)}}).init(),this.parameters=i,this.resizeObserver=new ResizeObserver(()=>{this.initialized?this.resizeTicker.restart():this.initialized=!0}),this.enable(),this.refresh(),this.resizeObserver.observe(this.$container),X(t)||this.resizeObserver.observe(this.$target)}computeVelocity(t,e){const s=this.velocityTime,i=F(),r=i-s;if(r<17)return this.velocity;this.velocityTime=i;const n=this.velocityStack,o=this.velocityMultiplier,a=this.minVelocity,l=this.maxVelocity,h=this.velocityStackIndex;n[h]=gt(ft(it(t*t+e*e)/r*o,a,l),5);const d=ct(n[0],n[1],n[2]);return this.velocity=d,this.velocityStackIndex=(h+1)%3,d}setX(t,e=!1){if(this.disabled[0])return;const s=gt(t,5);return this.overshootTicker.pause(),this.manual=!0,this.updated=!e,this.destX=s,this.snapped[0]=yt(s,this.snapX),this.animate[this.xProp](s,0),this.manual=!1,this}setY(t,e=!1){if(this.disabled[1])return;const s=gt(t,5);return this.overshootTicker.pause(),this.manual=!0,this.updated=!e,this.destY=s,this.snapped[1]=yt(s,this.snapY),this.animate[this.yProp](s,0),this.manual=!1,this}get x(){return gt(this.animate[this.xProp](),L.precision)}set x(t){this.setX(t,!1)}get y(){return gt(this.animate[this.yProp](),L.precision)}set y(t){this.setY(t,!1)}get progressX(){return cs(this.x,this.containerBounds[3],this.containerBounds[1],0,1)}set progressX(t){this.setX(cs(t,0,1,this.containerBounds[3],this.containerBounds[1]),!1)}get progressY(){return cs(this.y,this.containerBounds[0],this.containerBounds[2],0,1)}set progressY(t){this.setY(cs(t,0,1,this.containerBounds[0],this.containerBounds[2]),!1)}updateScrollCoords(){const t=gt(this.useWin?e.scrollX:this.$container.scrollLeft,0),s=gt(this.useWin?e.scrollY:this.$container.scrollTop,0),[i,r,n,o]=this.containerPadding,a=this.scrollThreshold;this.scroll.x=t,this.scroll.y=s,this.scrollBounds[0]=s-this.targetBounds[0]+i-a,this.scrollBounds[1]=t-this.targetBounds[1]-r+a,this.scrollBounds[2]=s-this.targetBounds[2]-n+a,this.scrollBounds[3]=t-this.targetBounds[3]+o-a}updateBoundingValues(){const t=this.$container;if(!t)return;const i=this.x,r=this.y,n=this.coords[2],o=this.coords[3];this.coords[2]=0,this.coords[3]=0,this.setX(0,!0),this.setY(0,!0),this.transforms.remove();const a=this.window[0]=e.innerWidth,l=this.window[1]=e.innerHeight,h=this.useWin,d=t.scrollWidth,c=t.scrollHeight,u=this.fixed,p=t.getBoundingClientRect(),[m,f,g,y]=this.containerPadding;this.dragArea[0]=h?0:p.left,this.dragArea[1]=h?0:p.top,this.scrollView[0]=h?ft(d,a,d):d,this.scrollView[1]=h?ft(c,l,c):c,this.updateScrollCoords();const{width:v,height:_,left:b,top:T,right:x,bottom:S}=t.getBoundingClientRect();this.dragArea[2]=gt(h?ft(v,a,a):v,0),this.dragArea[3]=gt(h?ft(_,l,l):_,0);const w=Ss(t,"overflow"),C="visible"===w,$="hidden"===w;if(this.canScroll=!u&&this.contained&&(t===s.body&&C||!$&&!C)&&(d>this.dragArea[2]+y-f||c>this.dragArea[3]+m-g)&&(!this.containerArray||this.containerArray&&!V(this.containerArray)),this.contained){const e=this.scroll.x,s=this.scroll.y,i=this.canScroll,r=this.$target.getBoundingClientRect(),n=i?h?0:t.scrollLeft:0,o=i?h?0:t.scrollTop:0,d=i?this.scrollView[0]-n-v:0,c=i?this.scrollView[1]-o-_:0;this.targetBounds[0]=gt(r.top+s-(h?0:T),0),this.targetBounds[1]=gt(r.right+e-(h?a:x),0),this.targetBounds[2]=gt(r.bottom+s-(h?l:S),0),this.targetBounds[3]=gt(r.left+e-(h?0:b),0),this.containerArray?(this.containerBounds[0]=this.containerArray[0]+m,this.containerBounds[1]=this.containerArray[1]-f,this.containerBounds[2]=this.containerArray[2]-g,this.containerBounds[3]=this.containerArray[3]+y):(this.containerBounds[0]=-gt(r.top-(u?ft(T,0,l):T)+o-m,0),this.containerBounds[1]=-gt(r.right-(u?ft(x,0,a):x)-d+f,0),this.containerBounds[2]=-gt(r.bottom-(u?ft(S,0,l):S)-c+g,0),this.containerBounds[3]=-gt(r.left-(u?ft(b,0,a):b)+n-y,0))}this.transforms.revert(),this.coords[2]=n,this.coords[3]=o,this.setX(i,!0),this.setY(r,!0)}isOutOfBounds(t,e,s){if(!this.contained)return 0;const[i,r,n,o]=t,[a,l]=this.disabled,h=!a&&er,d=!l&&sn;return h&&!d?1:!h&&d?2:h&&d?3:0}refresh(){const t=this.parameters,i=t.x,r=t.y,n=ks(t.container,this),o=ks(t.containerPadding,this)||0,a=V(o)?o:[o,o,o,o],l=this.x,h=this.y,d=ks(t.cursor,this),c={onHover:"grab",onGrab:"grabbing"};if(d){const{onHover:t,onGrab:e}=d;t&&(c.onHover=t),e&&(c.onGrab=e)}const u=ks(t.dragThreshold,this),p={mouse:3,touch:7};if(z(u))p.mouse=u,p.touch=u;else if(u){const{mouse:t,touch:e}=u;U(t)||(p.mouse=t),U(e)||(p.touch=e)}this.containerArray=V(n)?n:null,this.$container=n&&!this.containerArray?Te(n)[0]:s.body,this.useWin=this.$container===s.body,this.$scrollContainer=this.useWin?e:this.$container,this.isFinePointer=matchMedia("(pointer:fine)").matches,this.containerPadding=Ot(a,[0,0,0,0]),this.containerFriction=ft(Ot(ks(t.containerFriction,this),.8),0,1),this.releaseContainerFriction=ft(Ot(ks(t.releaseContainerFriction,this),this.containerFriction),0,1),this.snapX=ks(X(i)&&!U(i.snap)?i.snap:t.snap,this),this.snapY=ks(X(r)&&!U(r.snap)?r.snap:t.snap,this),this.scrollSpeed=Ot(ks(t.scrollSpeed,this),1.5),this.scrollThreshold=Ot(ks(t.scrollThreshold,this),20),this.dragSpeed=Ot(ks(t.dragSpeed,this),1),this.dragThreshold=this.isFinePointer?p.mouse:p.touch,this.minVelocity=Ot(ks(t.minVelocity,this),0),this.maxVelocity=Ot(ks(t.maxVelocity,this),50),this.velocityMultiplier=Ot(ks(t.velocityMultiplier,this),1),this.cursor=!1!==d&&c,this.updateBoundingValues();const[m,f,g,y]=this.containerBounds;this.setX(ft(l,y,f),!0),this.setY(ft(h,m,g),!0)}update(){if(this.updateScrollCoords(),this.canScroll){const[t,e,s,i]=this.containerPadding,[r,n]=this.scrollView,o=this.dragArea[2],a=this.dragArea[3],l=this.scroll.x,h=this.scroll.y,d=this.$container.scrollWidth,c=this.$container.scrollHeight,u=this.useWin?ft(d,this.window[0],d):d,p=this.useWin?ft(c,this.window[1],c):c,m=r-u,f=n-p;this.dragged&&m>0&&(this.coords[0]-=m,this.scrollView[0]=u),this.dragged&&f>0&&(this.coords[1]-=f,this.scrollView[1]=p);const g=10*this.scrollSpeed,y=this.scrollThreshold,[v,_]=this.coords,[b,T,x,S]=this.scrollBounds,w=gt(ft((_-b+t)/y,-1,0)*g,0),C=gt(ft((v-T-e)/y,0,1)*g,0),$=gt(ft((_-x-s)/y,0,1)*g,0),E=gt(ft((v-S+i)/y,-1,0)*g,0);if(w||$||E||C){const[t,e]=this.disabled;let s=l,i=h;t||(s=gt(ft(l+(E||C),0,r-o),0),this.coords[0]-=l-s),e||(i=gt(ft(h+(w||$),0,n-a),0),this.coords[1]-=h-i),this.useWin?this.$scrollContainer.scrollBy(-(l-s),-(h-i)):this.$scrollContainer.scrollTo(s,i)}}const[t,e,s,i]=this.containerBounds,[r,n,o,a,l,h]=this.pointer;this.coords[0]+=(r-l)*this.dragSpeed,this.coords[1]+=(n-h)*this.dragSpeed,this.pointer[4]=r,this.pointer[5]=n;const[d,c]=this.coords,[u,p]=this.snapped,m=(1-this.containerFriction)*this.dragSpeed;this.setX(d>e?e+(d-e)*m:ds?s+(c-s)*m:c{this.canScroll=!1,this.$scrollContainer.scrollTo(n.x,n.y)}}).init().then(()=>{this.canScroll=a})}return this}handleHover(){this.isFinePointer&&this.cursor&&!this.cursorStyles&&(this.cursorStyles=ws(this.$trigger,{cursor:this.cursor.onHover}))}animateInView(t,e=0,s=De.inOutQuad){this.stop(),this.updateBoundingValues();const i=this.x,r=this.y,[n,o,a,l]=this.containerPadding,h=this.scroll.y-this.targetBounds[0]+n+e,d=this.scroll.x-this.targetBounds[1]-o-e,c=this.scroll.y-this.targetBounds[2]-a-e,u=this.scroll.x-this.targetBounds[3]+l+e,p=this.isOutOfBounds([h,d,c,u],i,r);if(p){const[e,n]=this.disabled,o=ft(yt(i,this.snapX),u,d),a=ft(yt(r,this.snapY),h,c),l=U(t)?350*L.timeScale:t;e||1!==p&&3!==p||this.animate[this.xProp](o,l,s),n||2!==p&&3!==p||this.animate[this.yProp](a,l,s)}return this}handleDown(t){const e=t.target;if(this.grabbed||"range"===e.type)return;t.stopPropagation(),this.grabbed=!0,this.released=!1,this.stop(),this.updateBoundingValues();const i=t.changedTouches,r=i?i[0].clientX:t.clientX,n=i?i[0].clientY:t.clientY,{x:o,y:a}=this.transforms.normalizePoint(r,n),[l,h,d,c]=this.containerBounds,u=(1-this.containerFriction)*this.dragSpeed,p=this.x,m=this.y;this.coords[0]=this.coords[2]=u?p>h?h+(p-h)/u:pd?d+(m-d)/u:mRs?f:Rs)+1,this.targetStyles=ws(this.$target,{zIndex:Rs}),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.cursorStyles&&(this.cursorStyles.revert(),this.cursorStyles=null),this.isFinePointer&&this.cursor&&(this.bodyStyles=ws(s.body,{cursor:this.cursor.onGrab})),this.scrollInView(100,0,De.out(3)),this.onGrab(this),s.addEventListener("touchmove",this),s.addEventListener("touchend",this),s.addEventListener("touchcancel",this),s.addEventListener("mousemove",this),s.addEventListener("mouseup",this),s.addEventListener("selectstart",this)}handleMove(t){if(!this.grabbed)return;const e=t.changedTouches,s=e?e[0].clientX:t.clientX,i=e?e[0].clientY:t.clientY,{x:r,y:n}=this.transforms.normalizePoint(s,i),o=r-this.pointer[6],a=n-this.pointer[7];let l=t.target,h=!1,d=!1,c=!1;for(;e&&l&&l!==this.$trigger;){const t=Ss(l,"overflow-y");if("hidden"!==t&&"visible"!==t){const{scrollTop:t,scrollHeight:e,clientHeight:s}=l;if(e>s){c=!0,h=t<=3,d=t>=e-s-3;break}}l=l.parentElement}c&&(!h&&!d||h&&a<0||d&&a>0)?(this.pointer[0]=r,this.pointer[1]=n,this.pointer[2]=r,this.pointer[3]=n,this.pointer[4]=r,this.pointer[5]=n,this.pointer[6]=r,this.pointer[7]=n):($s(t),this.triggerStyles||(this.triggerStyles=ws(this.$trigger,{pointerEvents:"none"})),this.$trigger.addEventListener("touchstart",$s,{passive:!1}),this.$trigger.addEventListener("touchmove",$s,{passive:!1}),this.$trigger.addEventListener("touchend",$s),(this.dragged||!this.disabled[0]&&ot(o)>this.dragThreshold||!this.disabled[1]&&ot(a)>this.dragThreshold)&&(this.updateTicker.resume(),this.pointer[2]=this.pointer[0],this.pointer[3]=this.pointer[1],this.pointer[0]=r,this.pointer[1]=n,this.dragged=!0,this.released=!1,this.onDrag(this)))}handleUp(){if(!this.grabbed)return;this.updateTicker.pause(),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.bodyStyles&&(this.bodyStyles.revert(),this.bodyStyles=null);const[t,e]=this.disabled,[i,r,n,a,l,h]=this.pointer,[d,c,u,p]=this.containerBounds,[m,f]=this.snapped,g=this.releaseXSpring,y=this.releaseYSpring,v=this.releaseEase,_=this.hasReleaseSpring,b=this.overshootCoords,T=this.x,x=this.y,S=this.computeVelocity(i-l,r-h),w=this.angle=ut(r-a,i-n),C=150*S,$=(1-this.releaseContainerFriction)*this.dragSpeed,E=T+nt(w)*C,N=x+rt(w)*C,k=E>c?c+(E-c)*$:Eu?u+(N-u)*$:Nc?-1:1:TF&&(F=I)}if(!e){const e=D===u?x>u?-1:1:xF&&(F=B)}if(!_&&O&&$&&(I||B)){const t=o.blend;new Ze(b,{x:{to:k,duration:.65*I},y:{to:R,duration:.65*B},ease:v,composition:t}).init(),new Ze(b,{x:{to:A,duration:I},y:{to:D,duration:B},ease:v,composition:t}).init(),this.overshootTicker.stretch(ct(I,B)).restart()}else t||this.animate[this.xProp](A,I,M),e||this.animate[this.yProp](D,B,P);this.scrollInView(F,this.scrollThreshold,v);let V=!1;A!==m&&(this.snapped[0]=A,this.snapX&&(V=!0)),D!==f&&this.snapY&&(this.snapped[1]=D,this.snapY&&(V=!0)),V&&this.onSnap(this),this.grabbed=!1,this.dragged=!1,this.updated=!0,this.released=!0,this.onRelease(this),this.$trigger.removeEventListener("touchstart",$s),this.$trigger.removeEventListener("touchmove",$s),this.$trigger.removeEventListener("touchend",$s),s.removeEventListener("touchmove",this),s.removeEventListener("touchend",this),s.removeEventListener("touchcancel",this),s.removeEventListener("mousemove",this),s.removeEventListener("mouseup",this),s.removeEventListener("selectstart",this)}reset(){return this.stop(),this.resizeTicker.pause(),this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.setX(0,!0),this.setY(0,!0),this.coords[0]=0,this.coords[1]=0,this.pointer[0]=0,this.pointer[1]=0,this.pointer[2]=0,this.pointer[3]=0,this.pointer[4]=0,this.pointer[5]=0,this.pointer[6]=0,this.pointer[7]=0,this.velocity=0,this.velocityStack[0]=0,this.velocityStack[1]=0,this.velocityStack[2]=0,this.velocityStackIndex=0,this.angle=0,this}enable(){return this.enabled||(this.enabled=!0,this.$target.classList.remove("is-disabled"),this.touchActionStyles=ws(this.$trigger,{touchAction:this.disabled[0]?"pan-x":this.disabled[1]?"pan-y":"none"}),this.$trigger.addEventListener("touchstart",this,{passive:!0}),this.$trigger.addEventListener("mousedown",this,{passive:!0}),this.$trigger.addEventListener("mouseenter",this)),this}disable(){return this.enabled=!1,this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.touchActionStyles.revert(),this.cursorStyles&&(this.cursorStyles.revert(),this.cursorStyles=null),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.bodyStyles&&(this.bodyStyles.revert(),this.bodyStyles=null),this.targetStyles&&(this.targetStyles.revert(),this.targetStyles=null),this.$target.classList.add("is-disabled"),this.$trigger.removeEventListener("touchstart",this),this.$trigger.removeEventListener("mousedown",this),this.$trigger.removeEventListener("mouseenter",this),s.removeEventListener("touchmove",this),s.removeEventListener("touchend",this),s.removeEventListener("touchcancel",this),s.removeEventListener("mousemove",this),s.removeEventListener("mouseup",this),s.removeEventListener("selectstart",this),this}revert(){return this.reset(),this.disable(),this.$target.classList.remove("is-disabled"),this.updateTicker.revert(),this.overshootTicker.revert(),this.resizeTicker.revert(),this.animate.revert(),this.resizeObserver.disconnect(),this}handleEvent(t){switch(t.type){case"mousedown":case"touchstart":this.handleDown(t);break;case"mousemove":case"touchmove":this.handleMove(t);break;case"mouseup":case"touchend":case"touchcancel":this.handleUp();break;case"mouseenter":this.handleHover();break;case"selectstart":$s(t)}}}const Ds=(t,e)=>new As(t,e),Os=(t=T)=>new ve({duration:1*L.timeScale,onComplete:t},null,0).resume(),Is=t=>{let e;return(...s)=>{let i,r,n,o,a;e&&(i=e.currentIteration,r=e.iterationProgress,n=e.reversed,o=e._alternate,a=e._startTime,e.revert());const l=t(...s);return l&&!Y(l)&&l.revert&&(e=l),U(r)||(e.currentIteration=i,e.iterationProgress=(o&&i%2?!n:n)?1-r:r,e._startTime=a),l||T}};class Ls{constructor(t={}){I.current&&I.current.register(this);const i=t.root;let r=s;i&&(r=i.current||i.nativeElement||Te(i)[0]||s);const n=t.defaults,o=L.defaults,a=t.mediaQueries;if(this.defaults=n?xt(n,o):o,this.root=r,this.constructors=[],this.revertConstructors=[],this.revertibles=[],this.constructorsOnce=[],this.revertConstructorsOnce=[],this.revertiblesOnce=[],this.once=!1,this.onceIndex=0,this.methods={},this.matches={},this.mediaQueryLists={},this.data={},a)for(let t in a){const s=e.matchMedia(a[t]);this.mediaQueryLists[t]=s,s.addEventListener("change",this)}}register(t){(this.once?this.revertiblesOnce:this.revertibles).push(t)}execute(t){let e=I.current,s=I.root,i=L.defaults;I.current=this,I.root=this.root,L.defaults=this.defaults;const r=this.mediaQueryLists;for(let t in r)this.matches[t]=r[t].matches;const n=t(this);return I.current=e,I.root=s,L.defaults=i,n}refresh(){return this.onceIndex=0,this.execute(()=>{let t=this.revertibles.length,e=this.revertConstructors.length;for(;t--;)this.revertibles[t].revert();for(;e--;)this.revertConstructors[e](this);this.revertibles.length=0,this.revertConstructors.length=0,this.constructors.forEach(t=>{const e=t(this);Y(e)&&this.revertConstructors.push(e)})}),this}add(t,e){if(this.once=!1,Y(t)){const e=t;this.constructors.push(e),this.execute(()=>{const t=e(this);Y(t)&&this.revertConstructors.push(t)})}else this.methods[t]=(...t)=>this.execute(()=>e(...t));return this}addOnce(t){if(this.once=!0,Y(t)){const e=this.onceIndex++;if(this.constructorsOnce[e])return this;const s=t;this.constructorsOnce[e]=s,this.execute(()=>{const t=s(this);Y(t)&&this.revertConstructorsOnce.push(t)})}return this}keepTime(t){this.once=!0;const e=this.onceIndex++,s=this.constructorsOnce[e];if(Y(s))return s(this);const i=Is(t);let r;return this.constructorsOnce[e]=i,this.execute(()=>{r=i(this)}),r}handleEvent(t){"change"===t.type&&this.refresh()}revert(){const t=this.revertibles,e=this.revertConstructors,s=this.revertiblesOnce,i=this.revertConstructorsOnce,r=this.mediaQueryLists;let n=t.length,o=e.length,a=s.length,l=i.length;for(;n--;)t[n].revert();for(;o--;)e[o](this);for(;a--;)s[a].revert();for(;l--;)i[l](this);for(let t in r)r[t].removeEventListener("change",this);t.length=0,e.length=0,this.constructors.length=0,s.length=0,i.length=0,this.constructorsOnce.length=0,this.onceIndex=0,this.matches={},this.methods={},this.mediaQueryLists={},this.data={}}}const Bs=t=>new Ls(t),Ms=()=>{const t=s.createElement("div");s.body.appendChild(t),t.style.height="100lvh";const e=t.offsetHeight;return s.body.removeChild(t),e},Ps=(t,e)=>t&&Y(t)?t(e):t,Fs=new Map;class Vs{constructor(t){this.element=t,this.useWin=this.element===s.body,this.winWidth=0,this.winHeight=0,this.width=0,this.height=0,this.left=0,this.top=0,this.scale=1,this.zIndex=0,this.scrollX=0,this.scrollY=0,this.prevScrollX=0,this.prevScrollY=0,this.scrollWidth=0,this.scrollHeight=0,this.velocity=0,this.backwardX=!1,this.backwardY=!1,this.scrollTicker=new ve({autoplay:!1,onBegin:()=>this.dataTimer.resume(),onUpdate:()=>{const t=this.backwardX||this.backwardY;St(this,t=>t.handleScroll(),t)},onComplete:()=>this.dataTimer.pause()}).init(),this.dataTimer=new ve({autoplay:!1,frameRate:30,onUpdate:t=>{const e=t.deltaTime,s=this.prevScrollX,i=this.prevScrollY,r=this.scrollX,n=this.scrollY,o=s-r,a=i-n;this.prevScrollX=r,this.prevScrollY=n,o&&(this.backwardX=s>r),a&&(this.backwardY=i>n),this.velocity=gt(e>0?Math.sqrt(o*o+a*a)/e:0,5)}}).init(),this.resizeTicker=new ve({autoplay:!1,duration:250*L.timeScale,onComplete:()=>{this.updateWindowBounds(),this.refreshScrollObservers(),this.handleScroll()}}).init(),this.wakeTicker=new ve({autoplay:!1,duration:500*L.timeScale,onBegin:()=>{this.scrollTicker.resume()},onComplete:()=>{this.scrollTicker.pause()}}).init(),this._head=null,this._tail=null,this.updateScrollCoords(),this.updateWindowBounds(),this.updateBounds(),this.refreshScrollObservers(),this.handleScroll(),this.resizeObserver=new ResizeObserver(()=>this.resizeTicker.restart()),this.resizeObserver.observe(this.element),(this.useWin?e:this.element).addEventListener("scroll",this,!1)}updateScrollCoords(){const t=this.useWin,s=this.element;this.scrollX=gt(t?e.scrollX:s.scrollLeft,0),this.scrollY=gt(t?e.scrollY:s.scrollTop,0)}updateWindowBounds(){this.winWidth=e.innerWidth,this.winHeight=Ms()}updateBounds(){const t=getComputedStyle(this.element),e=this.element;let s,i;if(this.scrollWidth=e.scrollWidth+parseFloat(t.marginLeft)+parseFloat(t.marginRight),this.scrollHeight=e.scrollHeight+parseFloat(t.marginTop)+parseFloat(t.marginBottom),this.updateWindowBounds(),this.useWin)s=this.winWidth,i=this.winHeight;else{const t=e.getBoundingClientRect();s=e.clientWidth,i=e.clientHeight,this.top=t.top,this.left=t.left,this.scale=t.width?s/t.width:t.height?i/t.height:1}this.width=s,this.height=i}refreshScrollObservers(){St(this,t=>{t._debug&&t.removeDebug()}),this.updateBounds(),St(this,t=>{t.refresh(),t.onResize(t),t._debug&&t.debug()})}refresh(){this.updateWindowBounds(),this.updateBounds(),this.refreshScrollObservers(),this.handleScroll()}handleScroll(){this.updateScrollCoords(),this.wakeTicker.restart()}handleEvent(t){"scroll"===t.type&&this.handleScroll()}revert(){this.scrollTicker.cancel(),this.dataTimer.cancel(),this.resizeTicker.cancel(),this.wakeTicker.cancel(),this.resizeObserver.disconnect(),(this.useWin?e:this.element).removeEventListener("scroll",this),Fs.delete(this.element)}}const Xs=t=>{const e=t&&Te(t)[0]||s.body;let i=Fs.get(e);return i||(i=new Vs(e),Fs.set(e,i)),i},zs=(t,e,s,i,r)=>{const n="min"===e,o="max"===e,a="top"===e||"left"===e||"start"===e||n?0:"bottom"===e||"right"===e||"end"===e||o?"100%":"center"===e?"50%":e,{n:l,u:h}=Vt(a,zt);let d=l;return"%"===h?d=l/100*s:h&&(d=Ce(t,zt,"px",!0).n),o&&i<0&&(d+=i),n&&r>0&&(d+=r),d},Hs=(t,e,s,i,r)=>{let n;if(H(e)){const o=A.exec(e);if(o){const a=o[0],l=a[0],h=e.split(a),d="min"===h[0],c="max"===h[0],u=zs(t,h[0],s,i,r),p=zs(t,h[1],s,i,r);if(d){const e=Pt(zs(t,"min",s),p,l);n=eu?u:e}else n=Pt(u,p,l)}else n=zs(t,e,s,i,r)}else n=e;return gt(n,0)},Ys=t=>{let e;const s=t.targets;for(let t=0,i=s.length;t()=>{const e=this.linked;return e&&e[t]?e[t]():null}):null,h=a&&l.length>2;this.index=Us++,this.id=U(t.id)?this.index:t.id,this.container=Xs(t.container),this.target=null,this.linked=null,this.repeat=null,this.horizontal=null,this.enter=null,this.leave=null,this.sync=n||o||!!l,this.syncEase=n?i:null,this.syncSmooth=o?!0===e||r?1:e:null,this.onSyncEnter=l&&!h&&l[0]?l[0]:T,this.onSyncLeave=l&&!h&&l[1]?l[1]:T,this.onSyncEnterForward=l&&h&&l[0]?l[0]:T,this.onSyncLeaveForward=l&&h&&l[1]?l[1]:T,this.onSyncEnterBackward=l&&h&&l[2]?l[2]:T,this.onSyncLeaveBackward=l&&h&&l[3]?l[3]:T,this.onEnter=t.onEnter||T,this.onLeave=t.onLeave||T,this.onEnterForward=t.onEnterForward||T,this.onLeaveForward=t.onLeaveForward||T,this.onEnterBackward=t.onEnterBackward||T,this.onLeaveBackward=t.onLeaveBackward||T,this.onUpdate=t.onUpdate||T,this.onResize=t.onResize||T,this.onSyncComplete=t.onSyncComplete||T,this.reverted=!1,this.ready=!1,this.completed=!1,this.began=!1,this.isInView=!1,this.forceEnter=!1,this.hasEntered=!1,this.offset=0,this.offsetStart=0,this.offsetEnd=0,this.distance=0,this.prevProgress=0,this.thresholds=["start","end","end","start"],this.coords=[0,0,0,0],this.debugStyles=null,this.$debug=null,this._params=t,this._debug=Ot(t.debug,!1),this._next=null,this._prev=null,Ct(this.container,this),Os(()=>{if(!this.reverted){if(!this.target){const e=Te(t.target)[0];this.target=e||s.body,this.refresh()}this._debug&&this.debug()}})}link(t){if(t&&(t.pause(),this.linked=t,U(t)||U(t.persist)||(t.persist=!0),!this._params.target)){let e;U(t.targets)?St(t,t=>{t.targets&&!e&&(e=Ys(t))}):e=Ys(t),this.target=e||s.body,this.refresh()}return this}get velocity(){return this.container.velocity}get backward(){return this.horizontal?this.container.backwardX:this.container.backwardY}get scroll(){return this.horizontal?this.container.scrollX:this.container.scrollY}get progress(){const t=(this.scroll-this.offsetStart)/this.distance;return t===1/0||isNaN(t)?0:gt(ft(t,0,1),6)}refresh(){this.ready=!0,this.reverted=!1;const t=this._params;return this.repeat=Ot(Ps(t.repeat,this),!0),this.horizontal="x"===Ot(Ps(t.axis,this),"y"),this.enter=Ot(Ps(t.enter,this),"end start"),this.leave=Ot(Ps(t.leave,this),"start end"),this.updateBounds(),this.handleScroll(),this}removeDebug(){return this.$debug&&(this.$debug.parentNode.removeChild(this.$debug),this.$debug=null),this.debugStyles&&(this.debugStyles.revert(),this.$debug=null),this}debug(){this.removeDebug();const t=this.container,e=this.horizontal,i=t.element.querySelector(":scope > .animejs-onscroll-debug"),r=s.createElement("div"),n=s.createElement("div"),o=s.createElement("div"),a=Ws[this.index%Ws.length],l=t.useWin,h=l?t.winWidth:t.width,d=l?t.winHeight:t.height,c=t.scrollWidth,u=t.scrollHeight,p=this.container.width>360?320:260,m=e?0:10,f=e?10:0,g=e?24:p/2,y=e?g:15,v=e?60:g,_=e?v:y,b=e?"repeat-x":"repeat-y",T=t=>e?"0px "+t+"px":t+"px 2px",x=t=>`linear-gradient(${e?90:0}deg, ${t} 2px, transparent 1px)`,S=(t,e,s,i,r)=>`position:${t};left:${e}px;top:${s}px;width:${i}px;height:${r}px;`;r.style.cssText=`${S("absolute",m,f,e?c:p,e?p:u)}\n pointer-events: none;\n z-index: ${this.container.zIndex++};\n display: flex;\n flex-direction: ${e?"column":"row"};\n filter: drop-shadow(0px 1px 0px rgba(0,0,0,.75));\n `,n.style.cssText=`${S("sticky",0,0,e?h:g,e?g:d)}`,i||(n.style.cssText+=`background:\n ${x("#FFFF")}${T(g-10)} / 100px 100px ${b},\n ${x("#FFF8")}${T(g-10)} / 10px 10px ${b};\n `),o.style.cssText=`${S("relative",0,0,e?c:g,e?g:u)}`,i||(o.style.cssText+=`background:\n ${x("#FFFF")}${T(0)} / ${e?"100px 10px":"10px 100px"} ${b},\n ${x("#FFF8")}${T(0)} / ${e?"10px 0px":"0px 10px"} ${b};\n `);const w=[" enter: "," leave: "];this.coords.forEach((t,i)=>{const r=i>1,l=(r?0:this.offset)+t,m=i%2,f=l<_,g=l>(r?e?h:d:e?c:u)-_,b=(r?m&&!f:!m&&!f)||g,T=s.createElement("div"),x=s.createElement("div"),C=e?b?"right":"left":b?"bottom":"top",$=b?(e?v:y)+(r?e?-1:g?0:-2:e?-1:-2):e?1:0;x.innerHTML=`${this.id}${w[m]}${this.thresholds[i]}`,T.style.cssText=`${S("absolute",0,0,v,y)}\n display: flex;\n flex-direction: ${e?"column":"row"};\n justify-content: flex-${r?"start":"end"};\n align-items: flex-${b?"end":"start"};\n border-${C}: 2px solid ${a};\n `,x.style.cssText=`\n overflow: hidden;\n max-width: ${p/2-10}px;\n height: ${y};\n margin-${e?b?"right":"left":b?"bottom":"top"}: -2px;\n padding: 1px;\n font-family: ui-monospace, monospace;\n font-size: 10px;\n letter-spacing: -.025em;\n line-height: 9px;\n font-weight: 600;\n text-align: ${e&&b||!e&&!r?"right":"left"};\n white-space: pre;\n text-overflow: ellipsis;\n color: ${m?a:"rgba(0,0,0,.75)"};\n background-color: ${m?"rgba(0,0,0,.65)":a};\n border: 2px solid ${m?a:"transparent"};\n border-${e?b?"top-left":"top-right":b?"top-left":"bottom-left"}-radius: 5px;\n border-${e?b?"bottom-left":"bottom-right":b?"top-right":"bottom-right"}-radius: 5px;\n `,T.appendChild(x);let E=l-$+(e?1:0);T.style[e?"left":"top"]=`${E}px`,(r?n:o).appendChild(T)}),r.appendChild(n),r.appendChild(o),t.element.appendChild(r),i||r.classList.add("animejs-onscroll-debug"),this.$debug=r,"static"===Ss(t.element,"position")&&(this.debugStyles=ws(t.element,{position:"relative "}))}updateBounds(){let t;this._debug&&this.removeDebug();const e=this.target,i=this.container,r=this.horizontal,n=this.linked;let o,a=e;for(n&&(o=n.currentTime,n.seek(0,!0));a&&a!==i.element&&a!==s.body;){const e="sticky"===Ss(a,"position")&&ws(a,{position:"static"});a=a.parentElement,e&&(t||(t=[]),t.push(e))}const l=e.getBoundingClientRect(),h=i.scale,d=(r?l.left+i.scrollX-i.left:l.top+i.scrollY-i.top)*h,c=(r?l.width:l.height)*h,u=r?i.width:i.height,p=(r?i.scrollWidth:i.scrollHeight)-u,m=this.enter,f=this.leave;let g="start",y="end",v="end",_="start";if(H(m)){const t=m.split(" ");v=t[0],g=t.length>1?t[1]:g}else if(X(m)){const t=m;U(t.container)||(v=t.container),U(t.target)||(g=t.target)}else z(m)&&(v=m);if(H(f)){const t=f.split(" ");_=t[0],y=t.length>1?t[1]:y}else if(X(f)){const t=f;U(t.container)||(_=t.container),U(t.target)||(y=t.target)}else z(f)&&(_=f);const b=Hs(e,g,c),T=Hs(e,y,c),x=b+d-u,S=T+d-p,w=Hs(e,v,u,x,S),C=Hs(e,_,u,x,S),$=b+d-w,E=T+d-C,N=E-$;this.offset=d,this.offsetStart=$,this.offsetEnd=E,this.distance=N<=0?0:N,this.thresholds=[g,y,v,_],this.coords=[b,T,w,C],t&&t.forEach(t=>t.revert()),n&&n.seek(o,!0),this._debug&&this.debug()}handleScroll(){if(!this.ready)return;const t=this.linked,e=this.sync,s=this.syncEase,i=this.syncSmooth,r=t&&(s||i),n=this.horizontal,o=this.container,a=this.scroll,l=a<=this.offsetStart,h=a>=this.offsetEnd,d=!l&&!h,c=a===this.offsetStart||a===this.offsetEnd,u=!this.hasEntered&&c,p=this._debug&&this.$debug;let m=!1,f=!1,g=this.progress;if(l&&this.began&&(this.began=!1),g>0&&!this.began&&(this.began=!0),r){const e=t.progress;if(i&&z(i)){if(i<1){const t=1e-4,s=eg&&!g?-t:0;g=gt(vt(e,g,vt(.01,.2,i))+s,6)}}else s&&(g=s(g));m=g!==this.prevProgress,f=1===e,m&&!f&&i&&e&&o.wakeTicker.restart()}if(p){const t=n?o.scrollY:o.scrollX;p.style[n?"top":"left"]=t+10+"px"}(d&&!this.isInView||u&&!this.forceEnter&&!this.hasEntered)&&(d&&(this.isInView=!0),this.forceEnter&&this.hasEntered?d&&(this.forceEnter=!1):(p&&d&&(p.style.zIndex=""+this.container.zIndex++),this.onSyncEnter(this),this.onEnter(this),this.backward?(this.onSyncEnterBackward(this),this.onEnterBackward(this)):(this.onSyncEnterForward(this),this.onEnterForward(this)),this.hasEntered=!0,u&&(this.forceEnter=!0))),(d||!d&&this.isInView)&&(m=!0),m&&(r&&t.seek(t.duration*g),this.onUpdate(this)),!d&&this.isInView&&(this.isInView=!1,this.onSyncLeave(this),this.onLeave(this),this.backward?(this.onSyncLeaveBackward(this),this.onLeaveBackward(this)):(this.onSyncLeaveForward(this),this.onLeaveForward(this)),e&&!i&&(f=!0)),g>=1&&this.began&&!this.completed&&(e&&f||!e)&&(e&&this.onSyncComplete(this),this.completed=!0,(!this.repeat&&!t||!this.repeat&&t&&t.completed)&&this.revert()),g<1&&this.completed&&(this.completed=!1),this.prevProgress=g}revert(){if(this.reverted)return;const t=this.container;return wt(t,this),t._head||t.revert(),this._debug&&this.removeDebug(),this.reverted=!0,this.ready=!1,this}}const js=(t={})=>new qs(t),Gs=(t,e,s)=>(((1-3*s+3*e)*t+(3*s-6*e))*t+3*e)*t,Zs=(t,e,s)=>{let i,r,n=0,o=1,a=0;do{r=n+(o-n)/2,i=Gs(r,e,s)-t,i>0?o=r:n=r}while(ot(i)>1e-7&&++a<100);return r},Qs=(t=.5,e=0,s=.5,i=1)=>t===e&&s===i?$e:r=>0===r||1===r?r:Gs(Zs(r,t,s),e,i),Js=(t=10,e)=>{const s=e?lt:ht;return e=>s(ft(e,0,1)*t)*(1/t)},Ks=(...t)=>{const e=t.length;if(!e)return $e;const s=e-1,i=t[0],r=t[s],n=[0],o=[et(i)];for(let e=1;e{const s=[0],i=t-1;for(let t=1;t{const s=[];for(let i=0;i<=e;i++)s.push(gt(t(i/e),4));return`linear(${s.join(", ")})`},ii={},ri=t=>{let e=ii[t];if(e)return e;if(e="linear",H(t)){if(P(t,"linear")||P(t,"cubic-")||P(t,"steps")||P(t,"ease"))e=t;else if(P(t,"cubicB"))e=M(t);else{const s=Ie(t);Y(s)&&(e=s===$e?"linear":si(s))}ii[t]=e}else if(Y(t)){const s=si(t);s&&(e=s)}else t.ease&&(e=si(t.ease));return e},ni=["x","y","z"],oi=["perspective","width","height","margin","padding","top","right","bottom","left","borderWidth","fontSize","borderRadius",...ni],ai=(()=>[...ni,..._.filter(t=>["X","Y","Z"].some(e=>t.endsWith(e)))])();let li=null;const hi=(t,e,s,i,r)=>{let n=H(e)?e:It(e,s,i,r,null,null);return z(n)?oi.includes(t)||P(t,"translate")?`${n}px`:P(t,"rotate")||P(t,"skew")?`${n}deg`:`${n}`:n},di=(t,e,s,i,r,n)=>{let o="0";const a=U(i)?getComputedStyle(t)[e]:hi(e,i,t,r,n);return o=U(s)?V(i)?i.map(s=>hi(e,s,t,r,n)):a:[hi(e,s,t,r,n),a],o};class ci{constructor(e,s){I.current&&I.current.register(this),W(li)&&(!t||!U(CSS)&&Object.hasOwnProperty.call(CSS,"registerProperty")?(_.forEach(t=>{const e=P(t,"skew"),s=P(t,"scale"),i=P(t,"rotate"),r=P(t,"translate"),n=i||e,o=n?"":s?"":r?"":"*";try{CSS.registerProperty({name:"--"+t,syntax:o,inherits:!1,initialValue:r?"0px":n?"0deg":s?"1":"0"})}catch{}}),li=!0):li=!1);const i=xe(e);i.length||console.warn("No target found. Make sure the element you're trying to animate is accessible before creating your animation.");const r=Ot(s.autoplay,L.defaults.autoplay),n=!(!r||!r.link)&&r,o=s.alternate&&!0===s.alternate,a=s.reversed&&!0===s.reversed,l=Ot(s.loop,L.defaults.loop),h=!0===l||l===1/0?1/0:z(l)?l+1:1,c=o?a?"alternate-reverse":"alternate":a?"reverse":"normal",u=1===L.timeScale?1:m;this.targets=i,this.animations=[],this.controlAnimation=null,this.onComplete=s.onComplete||L.defaults.onComplete,this.duration=0,this.muteCallbacks=!1,this.completed=!1,this.paused=!r||!1!==n,this.reversed=a,this.persist=Ot(s.persist,L.defaults.persist),this.autoplay=r,this._speed=Ot(s.playbackRate,L.defaults.playbackRate),this._resolve=T,this._completed=0,this._inlineStyles=[],i.forEach((t,e)=>{const r=t[d],n=ai.some(t=>s.hasOwnProperty(t)),o=t.style,a=this._inlineStyles[e]={},l=Ot(s.ease,L.defaults.ease),p=It(l,t,e,i,null,null),m=Y(p)||H(p)?p:l,f=l.ease&&l,y=ri(m),T=(f?f.settlingDuration:It(Ot(s.duration,L.defaults.duration),t,e,i,null,null))*u,x=It(Ot(s.delay,L.defaults.delay),t,e,i,null,null)*u,S=Ot(s.composition,"replace");for(let l in s){if(!J(l))continue;const d={},p={iterations:h,direction:c,fill:"both",easing:y,duration:T,delay:x,composite:S},m=s[l],f=!!n&&(_.includes(l)?l:v.get(l)),g=f?"transform":l;let b;if(a[g]||(a[g]=o[g]),X(m)){const s=m,n=Ot(s.ease,y),a=n.ease&&n,h=s.to,c=s.from;if(p.duration=(a?a.settlingDuration:It(Ot(s.duration,T),t,e,i,null,null))*u,p.delay=It(Ot(s.delay,x),t,e,i,null,null)*u,p.composite=Ot(s.composition,S),p.easing=ri(n),b=di(t,l,c,h,e,i),f?(d[`--${f}`]=b,r[f]=b):d[l]=di(t,l,c,h,e,i),xs(this,t,l,d,p),!U(c))if(f){const t=`--${f}`;o.setProperty(t,d[t][0])}else o[l]=d[l][0]}else b=V(m)?m.map(s=>hi(l,s,t,e,i)):hi(l,m,t,e,i),f?(d[`--${f}`]=b,r[f]=b):d[l]=b,xs(this,t,l,d,p)}if(n){let t=g;for(let e in r)t+=`${b[e]}var(--${e})) `;o.transform=t}}),n&&this.autoplay.link(this)}forEach(t){try{const e=H(t)?e=>e[t]():t;this.animations.forEach(e)}catch{}return this}get speed(){return this._speed}set speed(t){this._speed=+t,this.forEach(e=>e.playbackRate=t)}get currentTime(){const t=this.controlAnimation,e=L.timeScale;return this.completed?this.duration:t?+t.currentTime*(1===e?1:e):0}set currentTime(t){const e=t*(1===L.timeScale?1:m);this.forEach(t=>{!this.persist&&e>=this.duration&&t.play(),t.currentTime=e})}get progress(){return this.currentTime/this.duration}set progress(t){this.forEach(e=>e.currentTime=t*this.duration||0)}resume(){return this.paused?(this.paused=!1,this.forEach("play")):this}pause(){return this.paused?this:(this.paused=!0,this.forEach("pause"))}alternate(){return this.reversed=!this.reversed,this.forEach("reverse"),this.paused&&this.forEach("pause"),this}play(){return this.reversed&&this.alternate(),this.resume()}reverse(){return this.reversed||this.alternate(),this.resume()}seek(t,e=!1){return e&&(this.muteCallbacks=!0),t{this.targets.forEach(t=>{"none"===t.style.transform&&t.style.removeProperty("transform")})}),this}revert(){return this.cancel().targets.forEach((t,e)=>{const s=t.style,i=this._inlineStyles[e];for(let e in i){const r=i[e];U(r)||r===g?s.removeProperty(M(e)):t.style[e]=r}t.getAttribute("style")===g&&t.removeAttribute("style")}),this}then(t=T){const e=this.then,s=()=>{this.then=null,t(this),this.then=e,this._resolve=T};return new Promise(t=>(this._resolve=()=>t(s()),this.completed&&this._resolve(),this))}}const ui={animate:(t,e)=>new ci(t,e),convertEase:si};let pi=0,mi=0;const fi=(t,e)=>!(!t||!e)&&(t===e||t.contains(e)),gi=t=>{if(!t)return null;const e=t.style,s=e.transition||"";return e.setProperty("transition","none","important"),s},yi=(t,e)=>{if(!t)return;const s=t.style;e?s.transition=e:s.removeProperty("transition")},vi=t=>{const e=t.layout.transitionMuteStore,s=t.$el,i=t.$measure;s&&!e.has(s)&&e.set(s,gi(s)),i&&!e.has(i)&&e.set(i,gi(i))},_i=t=>{t.forEach((t,e)=>yi(e,t)),t.clear()},bi={display:"none",visibility:"hidden",opacity:"0",transform:"none",position:"static"},Ti=t=>{if(!t)return;const e=t.parentNode;e&&(e._head===t&&(e._head=t._next),e._tail===t&&(e._tail=t._prev),t._prev&&(t._prev._next=t._next),t._next&&(t._next._prev=t._prev),t._prev=null,t._next=null,t.parentNode=null)},xi=(t,e,s,i)=>{let r=t.dataset.layoutId;r||(r=t.dataset.layoutId="node-"+mi++);const n=i||{};return n.$el=t,n.$measure=t,n.id=r,n.index=0,n.targets=null,n.delay=0,n.duration=0,n.ease=null,n.state=s,n.layout=s.layout,n.parentNode=e||null,n.isTarget=!1,n.isEntering=!1,n.isLeaving=!1,n.isInlined=!1,n.hasTransform=!1,n.inlineStyles=[],n.inlineTransforms=null,n.inlineTransition=null,n.branchAdded=!1,n.branchRemoved=!1,n.branchNotRendered=!1,n.sizeChanged=!1,n.hasVisibilitySwap=!1,n.hasDisplayNone=!1,n.hasVisibilityHidden=!1,n.measuredInlineTransform=null,n.measuredInlineTransition=null,n.measuredDisplay=null,n.measuredVisibility=null,n.measuredPosition=null,n.measuredHasDisplayNone=!1,n.measuredHasVisibilityHidden=!1,n.measuredIsVisible=!1,n.measuredIsRemoved=!1,n.measuredIsInsideRoot=!1,n.properties={transform:"none",x:0,y:0,left:0,top:0,clientLeft:0,clientTop:0,width:0,height:0},n.layout.properties.forEach(t=>n.properties[t]=0),n._head=null,n._tail=null,n._prev=null,n._next=null,n},Si=(t,e,s,i)=>{const r=t.$el,n=t.layout.root,o=n===r,a=t.properties,l=t.state.rootNode,h=t.parentNode,d=s.transform,c=r.style.transform,u=!!h&&h.measuredIsRemoved,p=s.position;o&&(t.layout.absoluteCoords="fixed"===p||"absolute"===p),t.$measure=e,t.inlineTransforms=c,t.hasTransform=d&&"none"!==d,t.measuredIsInsideRoot=fi(n,e),t.measuredInlineTransform=null,t.measuredDisplay=s.display,t.measuredVisibility=s.visibility,t.measuredPosition=p,t.measuredHasDisplayNone="none"===s.display,t.measuredHasVisibilityHidden="hidden"===s.visibility,t.measuredIsVisible=!(t.measuredHasDisplayNone||t.measuredHasVisibilityHidden),t.measuredIsRemoved=t.measuredHasDisplayNone||t.measuredHasVisibilityHidden||u;let m=!1,f=r.previousSibling;for(;f&&(f.nodeType===Node.COMMENT_NODE||f.nodeType===Node.TEXT_NODE&&!f.textContent.trim());)f=f.previousSibling;if(f&&f.nodeType===Node.TEXT_NODE)m=!0;else{for(f=r.nextSibling;f&&(f.nodeType===Node.COMMENT_NODE||f.nodeType===Node.TEXT_NODE&&!f.textContent.trim());)f=f.nextSibling;m=null!==f&&f.nodeType===Node.TEXT_NODE}if(t.isInlined=m,t.hasTransform&&!i){const s=t.layout.transitionMuteStore;s.get(r)||(t.inlineTransition=gi(r)),e===r?r.style.transform="none":(s.get(e)||(t.measuredInlineTransition=gi(e)),t.measuredInlineTransform=e.style.transform,e.style.transform="none")}let g,y,v=0,_=0,b=0,T=0;if(!i){const t=e.getBoundingClientRect();v=t.left,_=t.top,b=t.width,T=t.height}for(let t in a){const e="transform"===t?d:s[t]||s.getPropertyValue&&s.getPropertyValue(t);U(e)||(a[t]=e)}if(a.left=v,a.top=_,a.clientLeft=i?0:e.clientLeft,a.clientTop=i?0:e.clientTop,o)t.layout.absoluteCoords?(g=v,y=_):(g=0,y=0);else{const e=h||l,s=e.properties.left,i=e.properties.top,r=e.properties.clientLeft,n=e.properties.clientTop;if(t.layout.absoluteCoords)g=v-s-r,y=_-i-n;else if(e===l){const t=l.properties.left,e=l.properties.top;g=v-t-l.properties.clientLeft,y=_-e-l.properties.clientTop}else g=v-s-r,y=_-i-n}return a.x=g,a.y=y,a.width=b,a.height=T,t},wi=(t,e)=>{if(e)for(let s in e)t.properties[s]=e[s]},Ci=(t,e)=>{const s=It(e.ease,t.$el,t.index,t.targets,null,null),i=Y(s)?s:e.ease,r=!U(i)&&!U(i.ease);t.ease=r?i.ease:i,t.duration=r?i.settlingDuration:It(e.duration,t.$el,t.index,t.targets,null,null),t.delay=It(e.delay,t.$el,t.index,t.targets,null,null)},$i=t=>{const e=t.$el.style,s=t.inlineStyles;s.length=0,t.layout.recordedProperties.forEach(t=>{s.push(t,e[t]||"")})},Ei=t=>{const e=t.$el.style,s=t.inlineStyles;for(let t=0,i=s.length;t{const e=t.inlineTransforms,s=t.$el.style;!t.hasTransform||!e||t.hasTransform&&"none"===s.transform||e&&"none"===e?s.removeProperty("transform"):e&&(s.transform=e);const i=t.$measure;if(t.hasTransform&&i!==t.$el){const e=i.style,s=t.measuredInlineTransform;s&&""!==s?e.transform=s:e.removeProperty("transform")}t.measuredInlineTransform=null,null!==t.inlineTransition&&(yi(t.$el,t.inlineTransition),t.inlineTransition=null),i!==t.$el&&null!==t.measuredInlineTransition&&(yi(i,t.measuredInlineTransition),t.measuredInlineTransition=null)},ki=t=>{(t.measuredIsRemoved||t.hasVisibilitySwap)&&(t.$el.style.removeProperty("display"),t.$el.style.removeProperty("visibility"),t.hasVisibilitySwap&&(t.$measure.style.removeProperty("display"),t.$measure.style.removeProperty("visibility"))),t.layout.pendingRemoval.delete(t.$el)},Ri=(t,e,s)=>(e.properties={...t.properties},e.state=s,e.isTarget=t.isTarget,e.hasTransform=t.hasTransform,e.inlineTransforms=t.inlineTransforms,e.measuredIsVisible=t.measuredIsVisible,e.measuredDisplay=t.measuredDisplay,e.measuredIsRemoved=t.measuredIsRemoved,e.measuredHasDisplayNone=t.measuredHasDisplayNone,e.measuredHasVisibilityHidden=t.measuredHasVisibilityHidden,e.hasDisplayNone=t.hasDisplayNone,e.isInlined=t.isInlined,e.hasVisibilityHidden=t.hasVisibilityHidden,e);class Ai{constructor(t){this.layout=t,this.rootNode=null,this.rootNodes=new Set,this.nodes=new Map,this.scrollX=0,this.scrollY=0}revert(){return this.forEachNode(t=>{this.layout.pendingRemoval.delete(t.$el),t.$el.removeAttribute("data-layout-id"),t.$measure.removeAttribute("data-layout-id")}),this.rootNode=null,this.rootNodes.clear(),this.nodes.clear(),this}getNode(t){if(t&&t.dataset)return this.nodes.get(t.dataset.layoutId)}getComputedValue(t,e){const s=this.getNode(t);if(s)return s.properties[e]}forEach(t,e){let s=t,i=0;for(;s;)if(e(s,i++),s._head)s=s._head;else if(s._next)s=s._next;else{for(;s&&!s._next;)s=s.parentNode;s&&(s=s._next)}}forEachRootNode(t){this.forEach(this.rootNode,t)}forEachNode(t){for(const e of this.rootNodes)this.forEach(e,t)}registerElement(t,e){if(!t||1!==t.nodeType)return null;this.layout.transitionMuteStore.has(t)||this.layout.transitionMuteStore.set(t,gi(t));const s=[t,e],i=this.layout.root;let r=null;for(;s.length;){const t=s.pop(),e=s.pop();if(!e||1!==e.nodeType||q(e))continue;const n=!!t&&t.measuredIsRemoved,o=n?bi:getComputedStyle(e),a=!!n||"none"===o.display,l=!!n||"hidden"===o.visibility,h=!a&&!l,d=e.dataset.layoutId,c=fi(i,e);let u=d?this.nodes.get(d):null;if(u&&u.$el!==e){const a=fi(i,u.$el),l=u.measuredIsVisible;if(a||!c&&(c||l||!h)){if(a&&!l&&h){Si(u,e,o,n);let t=e.lastElementChild;for(;t;)s.push(t,u),t=t.previousElementSibling;r||(r=u);continue}{let i=e.lastElementChild;for(;i;)s.push(i,t),i=i.previousElementSibling;r||(r=u);continue}}Ti(u),u=xi(e,t,this,u)}else u=xi(e,t,this,u);u.branchAdded=!1,u.branchRemoved=!1,u.branchNotRendered=!1,u.isTarget=!1,u.sizeChanged=!1,u.hasVisibilityHidden=l,u.hasDisplayNone=a,u.hasVisibilitySwap=l&&!u.measuredHasVisibilityHidden||a&&!u.measuredHasDisplayNone,this.nodes.set(u.id,u),u.parentNode=t||null,u._prev=null,u._next=null,t?(this.rootNodes.delete(u),t._head?(t._tail._next=u,u._prev=t._tail,t._tail=u):(t._head=u,t._tail=u)):this.rootNodes.add(u),Si(u,u.$el,o,n);let p=e.lastElementChild;for(;p;)s.push(p,u),p=p.previousElementSibling;r||(r=u)}return r}ensureDetachedNode(t,e){if(!t||t===this.layout.root)return null;const s=t.dataset.layoutId,i=s?this.nodes.get(s):null;if(i&&i.$el===t)return i;let r=null,n=t.parentElement;for(;n&&n!==this.layout.root;){if(e.has(n)){r=this.ensureDetachedNode(n,e);break}n=n.parentElement}return this.registerElement(t,r)}record(){const t=this.layout,e=t.children,s=t.root,i=V(e)?e:[e],r=[],n="*"===e?s:I.root,o=[];let a=s.parentElement;for(;a&&1===a.nodeType;){const t=getComputedStyle(a);if(t.transform&&"none"!==t.transform){const t=a.style.transform||"",e=gi(a);o.push(a,t,e),a.style.transform="none"}a=a.parentElement}for(let t=0,e=i.length;t{u.push(t.$el)}),this.nodes.forEach((t,e)=>{t.index=c++,t.targets=u,t&&t.measuredIsInsideRoot&&d.add(e)});const p=new Set,m=[];for(let t=0,e=l.length;tthis.recordedProperties.add(t)),this.pendingRemoval=new WeakSet,this.transitionMuteStore=new Map,this.oldState=new Ai(this),this.newState=new Ai(this),this.timeline=null,this.transformAnimation=null,this.animating=[],this.swapping=[],this.leaving=[],this.entering=[],this.oldState.record(),_i(this.transitionMuteStore)}revert(){return this.root.classList.remove("is-animated"),this.timeline&&(this.timeline.complete(),this.timeline=null),this.transformAnimation&&(this.transformAnimation.complete(),this.transformAnimation=null),this.animating.length=this.swapping.length=this.leaving.length=this.entering.length=0,this.oldState.revert(),this.newState.revert(),requestAnimationFrame(()=>_i(this.transitionMuteStore)),this}record(){return this.transformAnimation&&(this.transformAnimation.cancel(),this.transformAnimation=null),this.oldState.record(),this.timeline&&(this.timeline.cancel(),this.timeline=null),this.newState.forEachRootNode(Ei),this}animate(t={}){const e={ease:Ot(t.ease,this.params.ease),delay:Ot(t.delay,this.params.delay),duration:Ot(t.duration,this.params.duration)},s={id:this.id},i=Ot(t.onComplete,this.params.onComplete),r=Ot(t.onPause,this.params.onPause);for(let e in O)"ease"!==e&&"duration"!==e&&"delay"!==e&&(U(t[e])?U(this.params[e])||(s[e]=this.params[e]):s[e]=t[e]);s.onComplete=()=>{const e=t.autoplay,s=L.editor;if(e&&e.linked||s&&s.showPanel)i&&i(this.timeline);else{this.transformAnimation&&this.transformAnimation.cancel(),f.forEachRootNode(t=>{ki(t),Ei(t)});for(let t=0,e=S.length;t{this.root.classList.contains("is-animated")||_i(this.transitionMuteStore)})}},s.onPause=()=>{const e=t.autoplay;if(e&&e.linked)return i&&i(this.timeline),void(r&&r(this.timeline));this.root.classList.contains("is-animated")&&(this.transformAnimation&&this.transformAnimation.cancel(),f.forEachRootNode(ki),this.root.classList.remove("is-animated"),i&&i(this.timeline),r&&r(this.timeline))},s.composition=!1;const n=xt(xt(t.swapAt||{},this.swapAtParams),e),o=xt(xt(t.enterFrom||{},this.enterFromParams),e),a=xt(xt(t.leaveTo||{},this.leaveToParams),e),[l,h]=Di(n),[d,c]=Di(o),[u,p]=Di(a),m=this.oldState,f=this.newState,g=this.animating,y=this.swapping,v=this.entering,_=this.leaving,b=this.pendingRemoval;g.length=y.length=v.length=_.length=0,m.forEachRootNode(vi),f.record(),f.forEachRootNode($i);const T=[],x=[],S=[],w=[],C=f.rootNode,$=C.$el;f.forEachRootNode(t=>{const e=t.$el,s=t.id,i=t.parentNode,r=!!i&&i.branchAdded,n=!!i&&i.branchRemoved,o=!!i&&i.branchNotRendered;let a=m.nodes.get(s);const l=!a;l?(a=Ri(t,{},m),m.nodes.set(s,a),a.measuredIsRemoved=!0):a.measuredIsRemoved&&!t.measuredIsRemoved&&(Ri(t,a,m),a.measuredIsRemoved=!0);const h=a.parentNode,c=(h?h.id:null)!==(i?i.id:null),p=a.$el!==t.$el,y=a.measuredIsRemoved,x=t.measuredIsRemoved;if(!a.measuredIsRemoved&&!x&&!l&&(c||p)){const t=a.properties.left,e=a.properties.top,s=i||f.rootNode,r=s.id?m.nodes.get(s.id):null,n=r?r.properties.left:s.properties.left,o=r?r.properties.top:s.properties.top,l=r?r.properties.clientLeft:s.properties.clientLeft,h=r?r.properties.clientTop:s.properties.clientTop;a.properties.x=t-n-l,a.properties.y=e-o-h}t.hasVisibilitySwap&&(t.hasVisibilityHidden&&(t.$el.style.visibility="visible",t.$measure.style.visibility="hidden"),t.hasDisplayNone&&(t.$el.style.display=a.measuredDisplay||t.measuredDisplay||"",t.$measure.style.visibility="hidden"));const S=b.has(e),w=a.measuredIsVisible,$=t.measuredIsVisible,E=!w&&$&&!o,N=!x&&(y||S)&&!r,k=x&&!y&&!n,R=k||x&&S&&!n;t.branchAdded=r||N,t.branchRemoved=n||R,t.branchNotRendered=o||x,x&&w&&(t.$el.style.display=a.measuredDisplay,t.$el.style.visibility="visible",Ri(a,t,f)),k?(t.isTarget&&(_.push(e),t.isLeaving=!0),b.add(e)):!x&&S&&b.delete(e),N&&!o||E?(wi(a,d),t.isTarget&&(v.push(e),t.isEntering=!0)):R&&!o&&wi(t,u),t===C||!t.isTarget||t.isEntering||t.isLeaving||g.push(e),T.push(e)});let E=0,N=0,k=0;f.forEachRootNode(t=>{const s=t.$el,i=t.parentNode,r=m.nodes.get(t.id),n=t.properties,o=r.properties;let a=i!==C&&i;for(;a&&!a.isTarget&&a!==C;)a=a.parentNode;t===C?(t.index=0,t.targets=g,Ci(t,e)):t.isEntering?(t.index=a?a.index:E,t.targets=a?g:v,Ci(t,c),E++):t.isLeaving?(t.index=a?a.index:N,t.targets=a?g:_,N++,Ci(t,p)):t.isTarget?(t.index=k++,t.targets=g,Ci(t,e)):(t.index=a?a.index:0,t.targets=g,Ci(t,h)),r.index=t.index,r.targets=t.targets;for(let e in n)n[e]=It(n[e],s,t.index,t.targets,null,null),o[e]=It(o[e],s,r.index,r.targets,null,null);const d=Math.abs(n.width-o.width)>1,u=Math.abs(n.height-o.height)>1;if(t.sizeChanged=d||u,t.isTarget&&(!t.measuredIsRemoved&&r.measuredIsVisible||t.measuredIsRemoved&&t.measuredIsVisible)){"none"===n.transform&&"none"===o.transform||(t.hasTransform=!0,S.push(s));for(let t in n)if("transform"!==t&&n[t]!==o[t]){x.push(s);break}}t.isTarget||(y.push(s),t.sizeChanged&&i&&i.isTarget&&i.sizeChanged&&(l.transform&&(t.hasTransform=!0,S.push(s)),w.push(s)))});const R={delay:t=>f.getNode(t).delay,duration:t=>f.getNode(t).duration,ease:t=>f.getNode(t).ease};if(s.defaults=R,this.timeline=rs(s),!x.length&&!S.length&&!y.length)return _i(this.transitionMuteStore),this.timeline.complete();if(T.length){this.root.classList.add("is-animated");for(let t=0,e=T.length;twindow.scrollTo(m.scrollX,m.scrollY));for(let t=0,e=x.length;t{const e=n[t],s=o[t];"transform"!==t&&e!==s&&(l[t]=[e,s],a=!0)}),a&&this.timeline.add(e,l,0)}}if(y.length){for(let t=0,e=y.length;t{"transform"!==t&&(e.style[t]=`${m.getComputedValue(e,t)}`)})}for(let t=0,e=y.length;t{e.style.width=`${i.width}px`,e.style.height=`${i.height}px`,e.style.minWidth="auto",e.style.minHeight="auto",e.style.maxWidth="none",e.style.maxHeight="none",s.isInlined||(e.style.translate=`${i.x}px ${i.y}px`),this.properties.forEach(t=>{"transform"!==t&&(e.style[t]=`${f.getComputedValue(e,t)}`)})},s.delay+s.duration/2)}if(w.length){const t=Be(f.nodes.get(w[0].dataset.layoutId).ease),e=e=>1-t(1-e),s={};if(l)for(let t in l)"transform"!==t&&(s[t]=[{from:e=>m.getComputedValue(e,t),to:l[t]},{from:l[t],to:e=>f.getComputedValue(e,t),ease:e}]);this.timeline.add(w,s,0)}}const A=S.length;if(A){for(let t=0;tf.getNode(t).isInlined?"0px 0px":`${f.getComputedValue(t,"x")}px ${f.getComputedValue(t,"y")}px`,transform:t=>{const e=f.getComputedValue(t,"transform");if(!w.includes(t))return e;const s=m.getComputedValue(t,"transform"),i=f.getNode(t);return[s,It(l.transform,t,i.index,i.targets,null,null),e]},autoplay:!1,...R}),this.timeline.sync(this.transformAnimation,0)}return this.timeline.init()}update(t,e={}){return this.record(),t(this),this.animate(e)}}const Ii=(t,e)=>new Oi(t,e),Li=fs,Bi={},Mi=(t,e=0)=>(...s)=>e?e=>t(...s,e):e=>t(e,...s),Pi=t=>(...e)=>{const s=t(...e);return new Proxy(T,{apply:(t,e,[i])=>s(i),get:(t,e)=>{if(Bi[e])return Pi((...t)=>{const i=Bi[e](...t);return t=>i(s(t))})}})},Fi=(t,e,s=0)=>{const i=(...t)=>(t.length{const i=10**s;return Math.floor((Math.random()*(e-t+1/i)+t)*i)/i};let Ki=0;const tr=(t,e=0,s=1,i=0)=>{let r=void 0===t?Ki++:t;return(t=e,n=s,o=i)=>{r+=1831565813,r=Math.imul(r^r>>>15,1|r),r^=r+Math.imul(r^r>>>7,61|r);const a=10**o;return Math.floor((((r^r>>>14)>>>0)/4294967296*(n-t+1/a)+t)*a)/a}},er=t=>t[Ji(0,t.length-1)],sr=t=>{let e,s,i=t.length;for(;i;)s=Ji(0,--i),e=t[i],t[i]=t[s],t[s]=e;return t},ir=(t,e={})=>{let s,i=[],r=0;const n=e.from,o=e.reversed,a=e.ease,l=!U(a),h=l&&!U(a.ease)?a.ease:l?Be(a):null,d=e.grid,c=!0===d,u=e.axis,p=e.total,m=U(n)||0===n||"first"===n,f="center"===n,y="last"===n,v="random"===n,_=V(n),b=V(t),T=e.use,x=et(b?t[0]:t),S=b?et(t[1]):0,w=k.exec((b?t[1]:t)+g),C=e.start||0+(b?x:0);let $=m?0:z(n)?n:0;return(t,a,l,m,g)=>{const[E]=xe(t),N=U(p)?l.length:p,k=!U(T)&&(Y(T)?T(E,a,N):Mt(E,T)),R=z(k)||H(k)&&z(+k)?+k:a;if(f&&($=(N-1)/2),y&&($=N-1),!i.length){if(c){let t=!0,e=1/0,s=1/0,r=-1/0,o=-1/0;const a=[],h=[];for(let i=0;ir&&(r=d),c>o&&(o=c)}if(t){let t=a[0],l=h[0];_?(t=e+n[0]*(r-e),l=s+n[1]*(o-s)):f?(t=(e+r)/2,l=(s+o)/2):y?(t=a[N-1],l=h[N-1]):z(n)&&(t=a[n],l=h[n]);for(let e=0;e0&&e0&&d<1/0)for(let t=0,e=i.length;th(t/r)*r)),o&&(i=i.map(t=>u?t<0?-1*t:-t:ot(r-t))),v&&(i=sr(i))}const A=b?(S-x)/r:x;U(s)&&(s=g?Ke(g,U(e.start)?g.iterationDuration:C):C);let D=s+(A*gt(i[R],2)||0);return e.modifier&&(D=e.modifier(D)),w&&(D=`${D}${w[2]}`),D}};var rr=Object.freeze({__proto__:null,$:xe,addChild:Ct,clamp:ji,cleanInlineStyles:Zt,createSeededRandom:tr,damp:Qi,degToRad:Ui,forEachChildren:St,get:Ss,keepTime:Is,lerp:Zi,mapRange:Yi,padEnd:zi,padStart:Xi,radToDeg:Wi,random:Ji,randomPick:er,remove:Cs,removeChild:wt,round:Gi,roundPad:Vi,set:ws,shuffle:sr,snap:qi,stagger:ir,sync:Os,wrap:Hi});const nr=t=>{const e=Te(t)[0];return e&&q(e)?e:console.warn(`${t} is not a valid SVGGeometryElement`)},or=(t,e,s,i,r)=>{const n=s+i,o=r?Math.max(0,Math.min(n,e)):(n%e+e)%e;return t.getPointAtLength(o)},ar=(t,e,s=0)=>i=>{const r=+t.getTotalLength(),n=i[h],o=t.getCTM(),a=0===s;return{from:0,to:r,modifier:i=>{const l=i+s*r;if("a"===e){const e=or(t,r,l,-1,a),s=or(t,r,l,1,a);return 180*ut(s.y-e.y,s.x-e.x)/pt}{const s=or(t,r,l,0,a);return"x"===e?n||!o?s.x:s.x*o.a+s.y*o.c+o.e:n||!o?s.y:s.x*o.b+s.y*o.d+o.f}}}},lr=(t,e=0)=>{const s=nr(t);if(s)return{translateX:ar(s,"x",e),translateY:ar(s,"y",e),rotate:ar(s,"a",e)}},hr=t=>{let e=1;if(t&&t.getCTM){const s=t.getCTM();s&&(e=(it(s.a*s.a+s.b*s.b)+it(s.c*s.c+s.d*s.d))/2)}return e},dr=(t,e,s)=>{const i=m,r=getComputedStyle(t),n=r.strokeLinecap,o="non-scaling-stroke"===r.vectorEffect?t:null;let a=n;const l=new Proxy(t,{get(t,e){const s=t[e];return e===c?t:"setAttribute"===e?(...e)=>{if("draw"===e[0]){const s=e[1].split(" "),r=+s[0],l=+s[1],h=hr(o),d=-1e3*r*h,c=l*i*h+d,u=i*h+(0===r&&1===l||1===r&&0===l?0:10*h)-c;if("butt"!==n){const e=r===l?"butt":n;a!==e&&(t.style.strokeLinecap=`${e}`,a=e)}t.setAttribute("stroke-dashoffset",`${d}`),t.setAttribute("stroke-dasharray",`${c} ${u}`)}return Reflect.apply(s,t,e)}:Y(s)?(...e)=>Reflect.apply(s,t,e):s}});return"1000"!==t.getAttribute("pathLength")&&(t.setAttribute("pathLength","1000"),l.setAttribute("draw",`${e} ${s}`)),l},cr=(t,e=0,s=0)=>Te(t).map(t=>dr(t,e,s)),ur=(t,e=.33)=>(s,i,r,n)=>{if(!(s.tagName||"").toLowerCase().match(/^(path|polygon|polyline)$/))throw new Error(`Can't morph a <${s.tagName}> SVG element. Use , or .`);const o=nr(t);if(!o)throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing , or SVG element.");if(!(o.tagName||"").toLowerCase().match(/^(path|polygon|polyline)$/))throw new Error(`Can't morph a <${o.tagName}> SVG element. Use , or .`);const a="path"===s.tagName,l=a?" ":",",h=n?n._value:null;h&&s.setAttribute(a?"d":"points",h);let d="",c="";if(e){const t=s.getTotalLength(),i=o.getTotalLength(),r=Math.max(Math.ceil(t*e),Math.ceil(i*e));for(let e=0;et.isWordLike||" "===t.segment||z(+t.segment),Er=t=>t.setAttribute("aria-hidden","true"),Nr=(t,e)=>[...t.querySelectorAll(`[data-${e}]:not([data-${e}] [data-${e}])`)],kr={line:"#00D672",word:"#FF4B4B",char:"#5A87FF"},Rr=t=>{if(!t.childElementCount&&!t.textContent.trim()){const e=t.parentElement;t.remove(),e&&Rr(e)}},Ar=(t,e,s)=>{const i=t.getAttribute(xr);if(null!==i&&+i!==e||"BR"===t.tagName){s.add(t);const e=t.previousSibling,i=t.nextSibling;e&&3===e.nodeType&&vr.test(e.textContent)&&s.add(e),i&&3===i.nodeType&&vr.test(i.textContent)&&s.add(i)}let r=t.childElementCount;for(;r--;)Ar(t.children[r],e,s);return s},Dr=(t,e={})=>{let s="";const i=H(e.class)?` class="${e.class}"`:"",r=Ot(e.clone,!1),n=Ot(e.wrap,!1),o=n?!0===n?"clip":n:!!r&&"clip";return n&&(s+=``),s+=``,r?(s+="{value}",s+=`{value}`):s+="{value}",s+="",n&&(s+=""),s},Or=(t,e,s,i,r,n,o,a,l)=>{const h=r===_r,d=r===Tr,c=`_${r}_`,u=Y(t)?t(s):t,p=h?"block":"inline-block";Cr.innerHTML=u.replace(fr,``).replace(gr,`${d?l:h?o:a}`);const m=Cr.content,f=m.firstElementChild,g=m.querySelector(`[data-${r}]`)||f,y=m.querySelectorAll(`i.${c}`),v=y.length;if(v){f.style.display=p,g.style.display=p,g.setAttribute(xr,`${o}`),h||(g.setAttribute("data-word",`${a}`),d&&g.setAttribute("data-char",`${l}`));let t=v;for(;t--;){const e=y[t],i=e.parentElement;i.style.display=p,h?i.innerHTML=s.innerHTML:i.replaceChild(s.cloneNode(!0),e)}e.push(g),i.appendChild(m)}else console.warn('The expression "{value}" is missing from the provided template.');return n&&(f.style.outline=`1px dotted ${kr[r]}`),f};class Ir{constructor(e,i={}){Sr||(Sr=mr?new mr([],{granularity:br}):{segment:t=>{const e=[],s=t.split(yr);for(let t=0,i=s.length;t[...t].map(t=>({segment:t}))}),!Cr&&t&&(Cr=s.createElement("template")),I.current&&I.current.register(this);const{words:r,chars:n,lines:o,accessible:a,includeSpaces:l,debug:h}=i,d=(e=V(e)?e[0]:e)&&e.nodeType?e:(be(e)||[])[0],c=!0===o?{}:o,u=!0===r||U(r)?{}:r,p=!0===n?{}:n;this.debug=Ot(h,!1),this.includeSpaces=Ot(l,!1),this.accessible=Ot(a,!0),this.linesOnly=c&&!u&&!p,this.lineTemplate=X(c)?Dr(_r,c):c,this.wordTemplate=X(u)||this.linesOnly?Dr(br,u):u,this.charTemplate=X(p)?Dr(Tr,p):p,this.$target=d,this.html=d&&d.innerHTML,this.lines=[],this.words=[],this.chars=[],this.effects=[],this.effectsCleanups=[],this.cache=null,this.ready=!1,this.width=0,this.resizeTimeout=null;const m=()=>this.html&&(c||u||p)&&this.split();this.resizeObserver=new ResizeObserver(()=>{clearTimeout(this.resizeTimeout),this.resizeTimeout=setTimeout(()=>{const t=d.offsetWidth;t!==this.width&&(this.width=t,m())},150)}),this.lineTemplate&&!this.ready?s.fonts.ready.then(m):m(),d?this.resizeObserver.observe(d):console.warn("No Text Splitter target found.")}addEffect(t){if(!Y(t))return console.warn("Effect must return a function."),this;const e=Is(t);return this.effects.push(e),this.ready&&(this.effectsCleanups[this.effects.length-1]=e(this)),this}revert(){return clearTimeout(this.resizeTimeout),this.lines.length=this.words.length=this.chars.length=0,this.resizeObserver.disconnect(),this.effectsCleanups.forEach(t=>Y(t)?t(this):t.revert&&t.revert()),this.$target.innerHTML=this.html,this}splitNode(t){const e=this.wordTemplate,i=this.charTemplate,r=this.includeSpaces,n=this.debug,o=t.nodeType;if(3===o){const o=t.nodeValue;if(o.trim()){const a=[],l=this.words,h=this.chars,d=Sr.segment(o),c=s.createDocumentFragment();let u=null;for(const t of d){const e=t.segment,s=$r(t);if(!u||s&&u&&$r(u))a.push(e);else{const t=a.length-1,s=a[t];yr.test(s)||yr.test(e)?a.push(e):a[t]+=e}u=t}for(let t=0,o=a.length;tY(t)&&t(this)),i||(t&&(e.innerHTML=this.html,this.words.length=this.chars.length=0),this.splitNode(e),this.cache=e.innerHTML),l&&(i&&(e.innerHTML=this.cache),this.lines.length=0,n&&(this.words=Nr(e,br))),o&&(l||n)&&(this.chars=Nr(e,Tr));const h=this.words.length?this.words:this.chars;let d,c=0;for(let t=0,e=h.length;t.5*i&&c++,e.setAttribute(xr,`${c}`);const r=e.querySelectorAll(`[${xr}]`);let n=r.length;for(;n--;)r[n].setAttribute(xr,`${c}`);d=s}if(l){const t=s.createDocumentFragment(),i=new Set,a=[];for(let t=0;t{const e=t.parentNode;e&&(1===t.nodeType&&i.add(e),e.removeChild(t))}),a.push(s)}i.forEach(Rr);for(let e=0,s=a.length;ethis.effectsCleanups[e]=t(this)),this}refresh(){this.split(!0)}}const Lr=(t,e)=>new Ir(t,e),Br=(t,e)=>(console.warn("text.split() is deprecated, import splitText() directly, or text.splitText()"),new Ir(t,e)),Mr=t=>{let e="";for(let s=0,i=t.length;s{t||(t={});const e=t.chars,s=Be(t.ease||"linear"),i=t.text,r=t.from,n=t.reversed||!1,o=t.perturbation||0,a=t.cursor,l=!0===a?"_":"number"==typeof a?String.fromCharCode(a):"string"==typeof a?a:"",h=l.length,d=t.seed||0,c=void 0===t.override||t.override,u=t.revealRate||60,p=1e3*L.timeScale/u,m=t.settleDuration||300*L.timeScale,f=t.settleRate||30,g=t.duration,y=t.revealDelay,v=t.delay,_=t.onChange||T;return(t,a,u,b)=>{const T="function"==typeof e?e(t,a,u):e||"a-zA-Z0-9!%#_",x=Mr(Pr[T]||T),S=x.length-1,w="function"==typeof g?g(t,a,u):g,C="function"==typeof y?y(t,a,u):y||0,$="function"==typeof v?v(t,a,u):v||0,E=d?tr(d):tr();Fr.has(t)||Fr.set(t,t.textContent);const N=b?b._value:t.textContent,k=void 0!==i?"function"==typeof i?i(t,a,u):i:b?b._value:Fr.get(t),R=" "===k||" "===k?" ":k,A=" "===N?0:N.length,D=R.length,O=!0===c?x:"string"==typeof c&&c.length>0?Mr(Pr[c]||c):null,I=O?O.length-1:0,B=" "===c?" ":null,M=""===c?D:Math.max(A,D),P=w>0?w:(M-1)*p+m,F=gt((P+C)/L.timeScale,0)*L.timeScale,V=C>0?gt(C/F,12):0,X=void 0===r||"auto"===r?D0;t--){const e=E(0,t),s=z[t];z[t]=z[e],z[e]=s}}else{const t="right"===X?(""!==c&&A?A:M)-1:"center"===X?((""!==c&&A?A:M)-1)/2:"number"==typeof X?X:0,e=Math.abs,s=new Array(M);for(let t=0;te(s-t)-e(i-t));for(let t=0;t0?o*H:0;for(let t=0;t0?(E(0,2e3)-1e3)/1e3*G:0,s=G>0?(E(0,2e3)-1e3)/1e3*G:0;q[t]=z[t]*Y+e,j[t]=Math.ceil((q[t]+H+s)/W)*W}if(Dt&&(t=j[e]);const e=new Array(D);for(let t=0;tz[t]-z[e]);const s=(1-t)/D;for(let i=0;ij[e[i]]&&(j[e[i]]=r)}}const Z=new Array(M);for(let t=0;t0;return{from:0,to:1,duration:F,delay:$,ease:"linear",modifier:t=>{if(t===K)return et;if(K=t,$>0&&t<=0)return et=N,N;if(t<=0)return et=J,J;if(t>=1)return et=R,R;et="";const e=t/W|0,i=e!==tt;i&&(tt=e);const r=V>0?(t-V)/(1-V):t,n=r>0?s(r):0;for(let t=0;t=j[t]?t any} [modifier] * @property {Callback} [onBegin] @@ -37,7 +37,7 @@ /** @typedef {JSAnimation|Timeline} Renderable */ /** @typedef {Timer|Renderable} Tickable */ /** @typedef {Timer&JSAnimation&Timeline} CallbackArgument */ -/** @typedef {Animatable|Tickable|WAAPIAnimation|Draggable|ScrollObserver|TextSplitter|Scope} Revertible */ +/** @typedef {Animatable|Tickable|WAAPIAnimation|Draggable|ScrollObserver|TextSplitter|Scope|AutoLayout} Revertible */ // Stagger types @@ -46,7 +46,8 @@ * @callback StaggerFunction * @param {Target} [target] * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [targets] + * @param {Tween|null} [prevTween] * @param {Timeline} [tl] * @return {T} */ @@ -54,9 +55,9 @@ /** * @typedef {Object} StaggerParams * @property {Number|String} [start] - * @property {Number|'first'|'center'|'last'|'random'} [from] + * @property {Number|'first'|'center'|'last'|'random'|Array.} [from] * @property {Boolean} [reversed] - * @property {Array.} [grid] + * @property {Array.|Boolean} [grid] * @property {('x'|'y')} [axis] * @property {String|((target: Target, i: Number, length: Number) => Number)} [use] * @property {Number} [total] @@ -117,8 +118,8 @@ // A hack to get both ease names suggestions AND allow any strings // https://github.com/microsoft/TypeScript/issues/29729#issuecomment-460346421 -/** @typedef {(String & {})|EaseStringParamNames|EasingFunction|Spring} EasingParam */ -/** @typedef {(String & {})|EaseStringParamNames|WAAPIEaseStringParamNames|EasingFunction|Spring} WAAPIEasingParam */ +/** @typedef {(String & {})|EaseStringParamNames|EasingFunction|Spring|TweakRegister} EasingParam */ +/** @typedef {(String & {})|EaseStringParamNames|WAAPIEaseStringParamNames|EasingFunction|Spring|TweakRegister} WAAPIEasingParam */ // Spring types @@ -174,6 +175,7 @@ * @property {Boolean|ScrollObserver} [autoplay] * @property {Number} [frameRate] * @property {Number} [playbackRate] + * @property {Number} [priority] */ /** @@ -184,10 +186,11 @@ /** * @callback FunctionValue - * @param {Target} target - The animated target - * @param {Number} index - The target index - * @param {Number} length - The total number of animated targets - * @return {Number|String|TweenObjectValue|Array.} + * @param {Target} [target] - The animated target + * @param {Number} [index] - The target index + * @param {TargetsArray} [targets] - The array of all animated targets + * @param {Tween|null} [prevTween] - The previous sibling tween for the same target and property + * @return {Number|String|TweenObjectValue|EasingParam|Array.} */ /** @@ -205,7 +208,8 @@ * @property {String} property * @property {Target} target * @property {String|Number} _value - * @property {Function|null} _func + * @property {Function|null} _toFunc + * @property {Function|null} _fromFunc * @property {EasingFunction} _ease * @property {Array.} _fromNumbers * @property {Array.} _toNumbers @@ -255,7 +259,7 @@ // JSAnimation types /** - * @typedef {Number|String|FunctionValue} TweenParamValue + * @typedef {Number|String|FunctionValue|EasingParam} TweenParamValue */ /** @@ -270,7 +274,7 @@ * @typedef {Object} TweenParamsOptions * @property {TweenParamValue} [duration] * @property {TweenParamValue} [delay] - * @property {EasingParam} [ease] + * @property {EasingParam|FunctionValue} [ease] * @property {TweenModifier} [modifier] * @property {TweenComposition} [composition] */ @@ -354,13 +358,14 @@ * - `'label'` - Label: Position animation at a named label position (e.g., `'My Label'`)
* - `stagger(String|Nummber)` - Stagger multi-elements animation positions (e.g., 10, 20, 30...) * - * @typedef {TimelinePosition | StaggerFunction} TimelineAnimationPosition + * @typedef {TimelinePosition | StaggerFunction | TweakRegister} TimelineAnimationPosition */ /** * @typedef {Object} TimelineOptions * @property {DefaultsParams} [defaults] * @property {EasingParam} [playbackEase] + * @property {Boolean} [composition] */ /** @@ -377,8 +382,8 @@ * @callback WAAPIFunctionValue * @param {DOMTarget} target - The animated target * @param {Number} index - The target index - * @param {Number} length - The total number of animated targets - * @return {WAAPITweenValue} + * @param {DOMTargetsArray} targets - The array of all animated targets + * @return {WAAPITweenValue|WAAPIEasingParam} */ /** @@ -404,7 +409,7 @@ * @property {Number} [playbackRate] * @property {Number|WAAPIFunctionValue} [duration] * @property {Number|WAAPIFunctionValue} [delay] - * @property {WAAPIEasingParam} [ease] + * @property {WAAPIEasingParam|WAAPIFunctionValue} [ease] * @property {CompositeOperation} [composition] * @property {Boolean} [persist] * @property {Callback} [onComplete] @@ -535,6 +540,7 @@ * @property {Callback} [onEnterBackward] * @property {Callback} [onLeaveBackward] * @property {Callback} [onUpdate] + * @property {Callback} [onResize] * @property {Callback} [onSyncComplete] */ @@ -623,6 +629,26 @@ * @property {Boolean} [debug] */ +/** + * @typedef {Object} ScrambleTextParams + * @property {String|function(Target, Number, TargetsArray): String} [text] - the text to transition to, otherwise uses the original text + * @property {String|function(Target, Number, TargetsArray): String} [chars] - the characters used for scramble; named sets: 'lowercase', 'uppercase', 'numbers', 'symbols', 'braille', 'blocks', 'shades'; range syntax: 'A-Z', 'a-z0-9'; defaults to 'a-zA-Z0-9!%#_' + * @property {EasingParam} [ease] - the easing applied to the scramble animation + * @property {Number|'left'|'center'|'right'|'random'|'auto'} [from] - where the reveal wave starts from, 'auto' (default) uses 'left' when text grows and 'right' when it shrinks + * @property {Boolean} [reversed] - reverses the reveal order, so 'center' reveals from edges inward instead of center outward + * @property {Boolean|Number|String} [cursor] - characters displayed at the leading edge of the reveal wave; true uses '_', a number is a char code, a string is used directly + * @property {Number} [perturbation] - adds random timing offsets to each character's start and end, creating a more organic reveal + * @property {Number} [seed] - a seed for the random number generator to produce reproducible scramble sequences + * @property {Boolean|String} [override] - controls the starting appearance: false shows original text, true scrambles it (default), '' starts from blank, ' ' replaces characters with spaces, a custom string (supports range syntax like 'A-Z') uses its characters as scramble set + * @property {Number} [revealRate] - characters per second entering the active zone; higher values make the reveal wave move faster (default: 60) + * @property {Number} [settleDuration] - time in ms each character spends scrambling before settling into its final glyph (default: 300) + * @property {Number} [settleRate] - how many times per second scramble characters cycle in the active zone (default: 30) + * @property {Number|function(Target, Number, TargetsArray): Number} [duration] - if set to a value greater than 0, overrides the computed duration from interval and settle; if unset or 0, duration is calculated automatically from text length and timing parameters + * @property {Number|function(Target, Number, TargetsArray): Number} [revealDelay] - delay in ms before the reveal wave starts within the scramble animation + * @property {Number|function(Target, Number, TargetsArray): Number} [delay] - delay in ms before the entire scramble animation starts + * @property {function(String, Number): void} [onChange] - callback fired each time a character changes during scramble; receives the current scrambled text and the eased progress (0-1) + */ + // SVG types /** @@ -644,8 +670,10 @@ // TODO: Do we need to check if we're running inside a worker ? const isBrowser = typeof window !== 'undefined'; - /** @type {Window & {AnimeJS: Array}|null} */ - const win = isBrowser ? /** @type {Window & {AnimeJS: Array}} */(/** @type {unknown} */(window)) : null; + /** @typedef {Window & {AnimeJS: Array}|null} AnimeJSWindow + + /** @type {AnimeJSWindow} */ + const win = isBrowser ? /** @type {AnimeJSWindow} */(/** @type {unknown} */(window)) : null; /** @type {Document|null} */ const doc = isBrowser ? document : null; @@ -689,7 +717,6 @@ const isDomSymbol = Symbol(); const isSvgSymbol = Symbol(); const transformsSymbol = Symbol(); - const morphPointsSymbol = Symbol(); const proxyTargetSymbol = Symbol(); // Numbers @@ -697,7 +724,7 @@ const minValue = 1e-11; const maxValue = 1e12; const K = 1e3; - const maxFps = 120; + const maxFps = 240; // Strings @@ -713,6 +740,7 @@ })(); const validTransforms = [ + 'perspective', 'translateX', 'translateY', 'translateZ', @@ -727,9 +755,6 @@ 'skew', 'skewX', 'skewY', - 'matrix', - 'matrix3d', - 'perspective', ]; const transformsFragmentStrings = /*#__PURE__*/ validTransforms.reduce((a, v) => ({...a, [v]: v + '('}), {}); @@ -741,6 +766,7 @@ // Regex + const validRgbHslRgx = /\)\s*[-.\d]/; const hexTestRgx = /(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i; const rgbExecRgx = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i; const rgbaExecRgx = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i; @@ -751,12 +777,23 @@ // export const unitsExecRgx = /^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)+([a-z]+|%)$/i; const unitsExecRgx = /^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i; const lowerCaseRgx = /([a-z])([A-Z])/g; - const transformsExecRgx = /(\w+)(\([^)]+\)+)/g; // Match inline transforms with cacl() values, returns the value wrapped in () const relativeValuesExecRgx = /(\*=|\+=|-=)/; const cssVariableMatchRgx = /var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/; + /** + * @typedef {Object} EditorGlobals + * @property {boolean} showPanel + * @property {boolean} synced + * @property {Function} addAnimation + * @property {Function} addTimeline + * @property {Function} addTimelineChild + * @property {Function} resolveStagger + * @property {Object|null} _head + * @property {Object|null} _tail + */ + /** @type {DefaultsParams} */ const defaults = { id: null, @@ -800,9 +837,11 @@ timeScale: 1, /** @type {Number} */ tickThreshold: 200, + /** @type {EditorGlobals|null} */ + editor: null, }; - const globalVersions = { version: '4.2.2', engine: null }; + const globalVersions = { version: '4.4.1', engine: null }; if (isBrowser) { if (!win.AnimeJS) win.AnimeJS = []; @@ -853,8 +892,8 @@ const isRgb = a => stringStartsWith(a, 'rgb'); /**@param {any} a @return {Boolean} */ const isHsl = a => stringStartsWith(a, 'hsl'); - /**@param {any} a @return {Boolean} */ - const isCol = a => isHex(a) || isRgb(a) || isHsl(a); + /**@param {any} a @return {Boolean} */ // Make sure boxShadow syntax like 'rgb(255, 0, 0) 0px 0px 6px 0px' is not a valid color type + const isCol = a => isHex(a) || ((isRgb(a) || isHsl(a)) && (a[a.length - 1] === ')' || !validRgbHslRgx.test(a))); /**@param {any} a @return {Boolean} */ const isKey = a => !globals.defaults.hasOwnProperty(a); @@ -918,8 +957,6 @@ */ const clamp$1 = (v, min, max) => v < min ? min : v > max ? max : v; - const powCache = {}; - /** * Rounds a number to specified decimal places * @@ -927,13 +964,12 @@ * @param {Number} decimalLength - Number of decimal places * @return {Number} */ - const round$1 = (v, decimalLength) => { - if (decimalLength < 0) return v; - if (!decimalLength) return _round(v); - let p = powCache[decimalLength]; - if (!p) p = powCache[decimalLength] = 10 ** decimalLength; - return _round(v * p) / p; - }; + const round$1 = (v, decimalLength) => { + if (decimalLength < 0) return v; + if (!decimalLength) return _round(v); + const p = 10 ** decimalLength; + return _round(v * p) / p; + }; /** * Snaps a value to nearest increment or array value @@ -1064,28 +1100,143 @@ */ const parseInlineTransforms = (target, propName, animationInlineStyles) => { const inlineTransforms = target.style.transform; - let inlinedStylesPropertyValue; if (inlineTransforms) { const cachedTransforms = target[transformsSymbol]; - let t; while (t = transformsExecRgx.exec(inlineTransforms)) { - const inlinePropertyName = t[1]; - // const inlinePropertyValue = t[2]; - const inlinePropertyValue = t[2].slice(1, -1); - cachedTransforms[inlinePropertyName] = inlinePropertyValue; - if (inlinePropertyName === propName) { - inlinedStylesPropertyValue = inlinePropertyValue; - // Store the new parsed inline styles if animationInlineStyles is provided - if (animationInlineStyles) { - animationInlineStyles[propName] = inlinePropertyValue; + let pos = 0; + const len = inlineTransforms.length; + let fullTranslateValue; + while (pos < len) { + // Skip whitespace + while (pos < len && inlineTransforms.charCodeAt(pos) === 32) pos++; + if (pos >= len) break; + // Read function name + const nameStart = pos; + while (pos < len && inlineTransforms.charCodeAt(pos) !== 40) pos++; + if (pos >= len) break; + const name = inlineTransforms.substring(nameStart, pos); + // Scan to closing paren, recording top-level comma positions + let depth = 1; + const valueStart = pos + 1; + let c1 = -1, c2 = -1; + pos++; + while (pos < len && depth > 0) { + const c = inlineTransforms.charCodeAt(pos); + if (c === 40) depth++; + else if (c === 41) depth--; + else if (c === 44 && depth === 1) { + if (c1 === -1) c1 = pos; + else if (c2 === -1) c2 = pos; } + pos++; } + const valueEnd = pos - 1; + // Decompose multi-arg functions into individual axis properties + if (name === 'translate' || name === 'translate3d') { + if (c1 === -1) { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.translateZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } + } + fullTranslateValue = inlineTransforms.substring(valueStart, valueEnd); + } else if (name === 'scale' || name === 'scale3d') { + if (c1 === -1) { + cachedTransforms.scale = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.scaleX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.scaleZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } + } + } else { + cachedTransforms[name] = inlineTransforms.substring(valueStart, valueEnd); + } + } + // Resolve the requested property from the cache + if (propName === 'translate3d' && fullTranslateValue) { + if (animationInlineStyles) animationInlineStyles[propName] = fullTranslateValue; + return fullTranslateValue; + } + const cached = cachedTransforms[propName]; + if (!isUnd(cached)) { + if (animationInlineStyles) animationInlineStyles[propName] = cached; + return cached; } } - return inlineTransforms && !isUnd(inlinedStylesPropertyValue) ? inlinedStylesPropertyValue : + return propName === 'translate3d' ? '0px, 0px, 0px' : + propName === 'rotate3d' ? '0, 0, 0, 0deg' : stringStartsWith(propName, 'scale') ? '1' : stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew') ? '0deg' : '0px'; }; + /** + * Builds a CSS transform string from the target's cached transform properties. + * Iterates validTransforms in order (perspective > translate > rotate > scale > skew > matrix). + * When adjacent axis properties are all present, emits a shorter shorthand (translateX + translateY -> translate(x, y)) + * The index is advanced past consumed properties so they are not emitted twice. + * Properties without a grouping partner (e.g. translateY alone, scaleZ alone) emit individually. + * + * @param {Record} props + * @return {String} + */ + const buildTransformString = (props) => { + let str = emptyString; + for (let i = 0, l = validTransforms.length; i < l; i++) { + const key = validTransforms[i]; + const val = props[key]; + if (val !== undefined) { + // Group translateX with adjacent translateY / translateZ + if (key === 'translateX') { + const next = props.translateY; + if (next !== undefined) { + const next2 = props.translateZ; + if (next2 !== undefined) { + str += `translate3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `translate(${val},${next}) `; + i += 1; + } + continue; + } + } + // Group scaleX with adjacent scaleY / scaleZ (only when standalone scale is absent) + if (key === 'scaleX' && props.scale === undefined) { + const next = props.scaleY; + if (next !== undefined) { + const next2 = props.scaleZ; + if (next2 !== undefined) { + str += `scale3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `scale(${val},${next}) `; + i += 1; + } + continue; + } + } + // All other properties: emit individually using pre-built fragment string + str += `${transformsFragmentStrings[key]}${val}) `; + } + // Preserve non-animatable rotate3d in correct position (after rotateZ, before scale) + if (key === 'rotateZ') { + if (props.rotate3d !== undefined) str += `rotate3d(${props.rotate3d}) `; + } + } + // Preserve non-animatable matrix/matrix3d from inline styles + if (props.matrix !== undefined) str += `matrix(${props.matrix}) `; + if (props.matrix3d !== undefined) str += `matrix3d(${props.matrix3d}) `; + return str; + }; + /** @@ -1187,15 +1338,16 @@ * @param {TweenPropValue} value * @param {Target} target * @param {Number} index - * @param {Number} total - * @param {Object} [store] + * @param {TargetsArray} targets + * @param {Object|null} store + * @param {Tween|null} prevTween * @return {any} */ - const getFunctionValue = (value, target, index, total, store) => { + const getFunctionValue = (value, target, index, targets, store, prevTween) => { let func; if (isFnc(value)) { func = () => { - const computed = /** @type {Function} */(value)(target, index, total); + const computed = /** @type {Function} */(value)(target, index, targets, prevTween); // Fallback to 0 if the function returns undefined / NaN / null / false / 0 return !isNaN(+computed) ? +computed : computed || 0; }; @@ -1262,9 +1414,17 @@ */ const getOriginalAnimatableValue = (target, propName, tweenType, animationInlineStyles) => { const type = !isUnd(tweenType) ? tweenType : getTweenType(target, propName); - return type === tweenTypes.OBJECT ? target[propName] || 0 : - type === tweenTypes.ATTRIBUTE ? /** @type {DOMTarget} */(target).getAttribute(propName) : - type === tweenTypes.TRANSFORM ? parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : + if (type === tweenTypes.OBJECT) { + const value = target[propName]; + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value || 0; + } + if (type === tweenTypes.ATTRIBUTE) { + const value = /** @type {DOMTarget} */(target).getAttribute(propName); + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value; + } + return type === tweenTypes.TRANSFORM ? parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : type === tweenTypes.CSS_VAR ? getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles).trimStart() : getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles); }; @@ -1366,6 +1526,54 @@ const decomposedOriginalValue = createDecomposedValueTargetObject(); + /** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ + const composeColorValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const r = round$1(clamp$1(/** @type {Number} */(mod(lerp$1(fn[0], tn[0], progress))), 0, 255), 0); + const g = round$1(clamp$1(/** @type {Number} */(mod(lerp$1(fn[1], tn[1], progress))), 0, 255), 0); + const b = round$1(clamp$1(/** @type {Number} */(mod(lerp$1(fn[2], tn[2], progress))), 0, 255), 0); + const a = clamp$1(/** @type {Number} */(mod(round$1(lerp$1(fn[3], tn[3], progress), precision))), 0, 1); + if (tween._composition !== compositionTypes.none) { + const ns = tween._numbers; + ns[0] = r; + ns[1] = g; + ns[2] = b; + ns[3] = a; + } + return `rgba(${r},${g},${b},${a})`; + }; + + /** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ + const composeComplexValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const ts = tween._strings; + const hasComposition = tween._composition !== compositionTypes.none; + let v = ts[0]; + for (let j = 0, l = tn.length; j < l; j++) { + const n = /** @type {Number} */(mod(round$1(lerp$1(fn[j], tn[j], progress), precision))); + const s = ts[j + 1]; + v += `${s ? n + s : n}`; + if (hasComposition) { + tween._numbers[j] = n; + } + } + return v; + }; + @@ -1394,7 +1602,6 @@ const _hasChildren = tickable._hasChildren; const tickableDelay = tickable._delay; const tickablePrevAbsoluteTime = tickable._currentTime; // TODO: rename ._currentTime to ._absoluteCurrentTime - const tickableEndTime = tickableDelay + iterationDuration; const tickableAbsoluteTime = time - tickableDelay; const tickablePrevTime = clamp$1(tickablePrevAbsoluteTime, -tickableDelay, duration); @@ -1526,30 +1733,9 @@ number = /** @type {Number} */(tweenModifier(round$1(lerp$1(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision))); value = `${number}${tween._unit}`; } else if (tweenValueType === valueTypes.COLOR) { - const fn = tween._fromNumbers; - const tn = tween._toNumbers; - const r = round$1(clamp$1(/** @type {Number} */(tweenModifier(lerp$1(fn[0], tn[0], tweenProgress))), 0, 255), 0); - const g = round$1(clamp$1(/** @type {Number} */(tweenModifier(lerp$1(fn[1], tn[1], tweenProgress))), 0, 255), 0); - const b = round$1(clamp$1(/** @type {Number} */(tweenModifier(lerp$1(fn[2], tn[2], tweenProgress))), 0, 255), 0); - const a = clamp$1(/** @type {Number} */(tweenModifier(round$1(lerp$1(fn[3], tn[3], tweenProgress), tweenPrecision))), 0, 1); - value = `rgba(${r},${g},${b},${a})`; - if (tweenHasComposition) { - const ns = tween._numbers; - ns[0] = r; - ns[1] = g; - ns[2] = b; - ns[3] = a; - } + value = composeColorValue(tween, tweenProgress, tweenPrecision); } else if (tweenValueType === valueTypes.COMPLEX) { - value = tween._strings[0]; - for (let j = 0, l = tween._toNumbers.length; j < l; j++) { - const n = /** @type {Number} */(tweenModifier(round$1(lerp$1(tween._fromNumbers[j], tween._toNumbers[j], tweenProgress), tweenPrecision))); - const s = tween._strings[j + 1]; - value += `${s ? n + s : n}`; - if (tweenHasComposition) { - tween._numbers[j] = n; - } - } + value = composeComplexValue(tween, tweenProgress, tweenPrecision); } // For additive tweens and Animatables @@ -1592,14 +1778,8 @@ } - // NOTE: Possible improvement: Use translate(x,y) / translate3d(x,y,z) syntax - // to reduce memory usage on string composition if (tweenTransformsNeedUpdate && tween._renderTransforms) { - let str = emptyString; - for (let key in tweenTargetTransformsProperties) { - str += `${transformsFragmentStrings[key]}${tweenTargetTransformsProperties[key]}) `; - } - tweenStyle.transform = str; + tweenStyle.transform = buildTransformString(tweenTargetTransformsProperties); tweenTransformsNeedUpdate = 0; } @@ -1710,7 +1890,6 @@ // Renders on timeline are triggered by its children so it needs to be set after rendering the children if (!muteCallbacks && tlChildrenHasRendered) tl.onRender(/** @type {CallbackArgument} */(tl)); - // Triggers the timeline onComplete() once all chindren all completed and the current time has reached the end if ((tlChildrenHaveCompleted || tlIsRunningBackwards) && tl._currentTime >= tl.duration) { // Make sure the paused flag is false in case it has been skipped in the render function @@ -1764,59 +1943,78 @@ /** * @template {Renderable} T * @param {T} renderable + * @param {Boolean} [inlineStylesOnly] * @return {T} */ - const cleanInlineStyles = renderable => { - // Allow cleanInlineStyles() to be called on timelines + const revertValues = (renderable, inlineStylesOnly = false) => { + // Allow revertValues() to be called on timelines if (renderable._hasChildren) { - forEachChildren(renderable, cleanInlineStyles, true); + forEachChildren(renderable, (/** @type {Renderable} */child) => revertValues(child, inlineStylesOnly), true); } else { const animation = /** @type {JSAnimation} */(renderable); animation.pause(); forEachChildren(animation, (/** @type {Tween} */tween) => { const tweenProperty = tween.property; const tweenTarget = tween.target; - if (tweenTarget[isDomSymbol]) { - const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; - const originalInlinedValue = tween._inlineValue; - const tweenHadNoInlineValue = isNil(originalInlinedValue) || originalInlinedValue === emptyString; - if (tween._tweenType === tweenTypes.TRANSFORM) { - const cachedTransforms = tweenTarget[transformsSymbol]; - if (tweenHadNoInlineValue) { - delete cachedTransforms[tweenProperty]; - } else { - cachedTransforms[tweenProperty] = originalInlinedValue; - } - if (tween._renderTransforms) { - if (!Object.keys(cachedTransforms).length) { - targetStyle.removeProperty('transform'); + const tweenType = tween._tweenType; + const originalInlinedValue = tween._inlineValue; + const tweenHadNoInlineValue = isNil(originalInlinedValue) || originalInlinedValue === emptyString; + if (tweenType === tweenTypes.OBJECT) { + if (!inlineStylesOnly && !tweenHadNoInlineValue) { + tweenTarget[tweenProperty] = originalInlinedValue; + } + } else if (tweenTarget[isDomSymbol]) { + if (tweenType === tweenTypes.ATTRIBUTE) { + if (!inlineStylesOnly) { + if (tweenHadNoInlineValue) { + /** @type {DOMTarget} */(tweenTarget).removeAttribute(tweenProperty); } else { - let str = emptyString; - for (let key in cachedTransforms) { - str += transformsFragmentStrings[key] + cachedTransforms[key] + ') '; - } - targetStyle.transform = str; + /** @type {DOMTarget} */(tweenTarget).setAttribute(tweenProperty, /** @type {String} */(originalInlinedValue)); } } } else { - if (tweenHadNoInlineValue) { - targetStyle.removeProperty(toLowerCase(tweenProperty)); + const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; + if (tweenType === tweenTypes.TRANSFORM) { + const cachedTransforms = tweenTarget[transformsSymbol]; + if (tweenHadNoInlineValue) { + delete cachedTransforms[tweenProperty]; + } else { + cachedTransforms[tweenProperty] = originalInlinedValue; + } + if (tween._renderTransforms) { + if (!Object.keys(cachedTransforms).length) { + targetStyle.removeProperty('transform'); + } else { + targetStyle.transform = buildTransformString(cachedTransforms); + } + } } else { - targetStyle[tweenProperty] = originalInlinedValue; + if (tweenHadNoInlineValue) { + targetStyle.removeProperty(toLowerCase(tweenProperty)); + } else { + targetStyle[tweenProperty] = originalInlinedValue; + } } } - if (animation._tail === tween) { - animation.targets.forEach(t => { - if (t.getAttribute && t.getAttribute('style') === emptyString) { - t.removeAttribute('style'); - } }); - } + } + if (tweenTarget[isDomSymbol] && animation._tail === tween) { + animation.targets.forEach(t => { + if (t.getAttribute && t.getAttribute('style') === emptyString) { + t.removeAttribute('style'); + } }); } }); } return renderable; }; + /** + * @template {Renderable} T + * @param {T} renderable + * @return {T} + */ + const cleanInlineStyles = renderable => revertValues(renderable, true); + /* @@ -1832,7 +2030,7 @@ /** @type {Number} */ this._currentTime = initTime; /** @type {Number} */ - this._elapsedTime = initTime; + this._lastTickTime = initTime; /** @type {Number} */ this._startTime = initTime; /** @type {Number} */ @@ -1840,7 +2038,7 @@ /** @type {Number} */ this._scheduledTime = 0; /** @type {Number} */ - this._frameDuration = round$1(K / maxFps, 0); + this._frameDuration = K / maxFps; /** @type {Number} */ this._fps = maxFps; /** @type {Number} */ @@ -1861,7 +2059,8 @@ const previousFrameDuration = this._frameDuration; const fr = +frameRate; const fps = fr < minValue ? minValue : fr; - const frameDuration = round$1(K / fps, 0); + const frameDuration = K / fps; + if (fps > defaults.frameRate) defaults.frameRate = fps; this._fps = fps; this._frameDuration = frameDuration; this._scheduledTime += frameDuration - previousFrameDuration; @@ -1882,14 +2081,13 @@ */ requestTick(time) { const scheduledTime = this._scheduledTime; - const elapsedTime = this._elapsedTime; - this._elapsedTime += (time - elapsedTime); - // If the elapsed time is lower than the scheduled time + this._lastTickTime = time; + // If the current time is lower than the scheduled time // this means not enough time has passed to hit one frameDuration // so skip that frame - if (elapsedTime < scheduledTime) return tickModes.NONE; + if (time < scheduledTime) return tickModes.NONE; const frameDuration = this._frameDuration; - const frameDelta = elapsedTime - scheduledTime; + const frameDelta = time - scheduledTime; // Ensures that _scheduledTime progresses in steps of at least 1 frameDuration. // Skips ahead if the actual elapsed time is higher. this._scheduledTime += frameDelta < frameDuration ? frameDuration : frameDelta; @@ -2026,7 +2224,7 @@ wake() { if (this.useDefaultMainLoop && !this.reqId) { - // Imediatly request a tick to update engine._elapsedTime and get accurate offsetPosition calculation in timer.js + // Imediatly request a tick to update engine._lastTickTime and get accurate offsetPosition calculation in timer.js this.requestTick(now()); this.reqId = engineTickMethod(tickEngine); } @@ -2501,6 +2699,9 @@ let timerId = 0; + /** @param {Timer} prev @param {Timer} child */ + const sortByPriority = (prev, child) => prev._priority > child._priority; + /** * Base class used to create Timers, Animations and Timelines */ @@ -2514,6 +2715,8 @@ super(0); + ++timerId; + const { id, delay, @@ -2525,6 +2728,7 @@ autoplay, frameRate, playbackRate, + priority, onComplete, onLoop, onPause, @@ -2535,31 +2739,32 @@ if (scope.current) scope.current.register(this); - const timerInitTime = parent ? 0 : engine._elapsedTime; + const timerInitTime = parent ? 0 : engine._lastTickTime; const timerDefaults = parent ? parent.defaults : globals.defaults; const timerDelay = /** @type {Number} */(isFnc(delay) || isUnd(delay) ? timerDefaults.delay : +delay); const timerDuration = isFnc(duration) || isUnd(duration) ? Infinity : +duration; const timerLoop = setValue(loop, timerDefaults.loop); const timerLoopDelay = setValue(loopDelay, timerDefaults.loopDelay); - const timerIterationCount = timerLoop === true || - timerLoop === Infinity || - /** @type {Number} */(timerLoop) < 0 ? Infinity : - /** @type {Number} */(timerLoop) + 1; + let timerIterationCount = timerLoop === true || + timerLoop === Infinity || + /** @type {Number} */(timerLoop) < 0 ? Infinity : + /** @type {Number} */(timerLoop) + 1; let offsetPosition = 0; if (parent) { offsetPosition = parentPosition; } else { - // Make sure to tick the engine once if not currently running to get up to date engine._elapsedTime + // Make sure to tick the engine once if not currently running to get up to date engine._lastTickTime // to avoid big gaps with the following offsetPosition calculation if (!engine.reqId) engine.requestTick(now()); // Make sure to scale the offset position with globals.timeScale to properly handle seconds unit - offsetPosition = (engine._elapsedTime - engine._startTime) * globals.timeScale; + offsetPosition = (engine._lastTickTime - engine._startTime) * globals.timeScale; } // Timer's parameters - this.id = !isUnd(id) ? id : ++timerId; + /** @type {String|Number} */ + this.id = !isUnd(id) ? id : timerId; /** @type {Timeline} */ this.parent = parent; // Total duration of the timer @@ -2619,7 +2824,7 @@ // Clock's parameters /** @type {Number} */ - this._elapsedTime = timerInitTime; + this._lastTickTime = timerInitTime; /** @type {Number} */ this._startTime = timerInitTime; /** @type {Number} */ @@ -2628,6 +2833,8 @@ this._fps = setValue(frameRate, timerDefaults.frameRate); /** @type {Number} */ this._speed = setValue(playbackRate, timerDefaults.playbackRate); + /** @type {Number} */ + this._priority = +setValue(priority, 1); } get cancelled() { @@ -2650,7 +2857,7 @@ } get iterationCurrentTime() { - return round$1(this._iterationTime, globals.precision); + return clamp$1(round$1(this._iterationTime, globals.precision), 0, this.iterationDuration); } set iterationCurrentTime(time) { @@ -2748,9 +2955,9 @@ /** @return {this} */ resetTime() { const timeScale = 1 / (this._speed * engine._speed); - // TODO: See if we can safely use engine._elapsedTime here + // TODO: See if we can safely use engine._lastTickTime here // if (!engine.reqId) engine.requestTick(now()) - // this._startTime = engine._elapsedTime - (this._currentTime + this._delay) * timeScale; + // this._startTime = engine._lastTickTime - (this._currentTime + this._delay) * timeScale; this._startTime = now() - (this._currentTime + this._delay) * timeScale; return this; } @@ -2772,7 +2979,7 @@ tick(this, minValue, 0, 0, tickModes.FORCE); } else { if (!this._running) { - addChild(engine, this); + addChild(engine, this, sortByPriority); engine._hasChildren = true; this._running = true; } @@ -2882,10 +3089,11 @@ /** * Imediatly completes the timer, cancels it and triggers the onComplete callback + * @param {Boolean|Number} [muteCallbacks] * @return {this} */ - complete() { - return this.seek(this.duration).cancel(); + complete(muteCallbacks = 0) { + return this.seek(this.duration, muteCallbacks).cancel(); } /** @@ -3258,12 +3466,14 @@ const toTargetObject = createDecomposedValueTargetObject(); const inlineStylesStore = {}; const toFunctionStore = { func: null }; + const fromFunctionStore = { func: null }; const keyframesTargetArray = [null]; const fastSetValuesArray = [null, null]; /** @type {TweenKeyValue} */ const keyObjectTarget = { to: null }; let tweenId = 0; + let JSAnimationId = 0; let keyframes; /** @type {TweenParamsOptions & TweenValues} */ let key; @@ -3364,7 +3574,7 @@ * @param {Number} [parentPosition] * @param {Boolean} [fastSet=false] * @param {Number} [index=0] - * @param {Number} [length=0] + * @param {TargetsArray} [allTargets] */ constructor( targets, @@ -3373,11 +3583,13 @@ parentPosition, fastSet = false, index = 0, - length = 0 + allTargets ) { super(/** @type {TimerParams & AnimationParams} */(parameters), parent, parentPosition); + ++JSAnimationId; + const parsedTargets = registerTargets(targets); const targetsLength = parsedTargets.length; @@ -3387,6 +3599,7 @@ const params = /** @type {AnimationParams} */(kfParams ? mergeObjects(generateKeyframes(/** @type {DurationKeyframes} */(kfParams), parameters), parameters) : parameters); const { + id, delay, duration, ease, @@ -3397,11 +3610,12 @@ } = params; const animDefaults = parent ? parent.defaults : globals.defaults; - const animaPlaybackEase = setValue(playbackEase, animDefaults.playbackEase); - const animEase = animaPlaybackEase ? parseEase(animaPlaybackEase) : null; - const hasSpring = !isUnd(ease) && !isUnd(/** @type {Spring} */(ease).ease); - const tEasing = hasSpring ? /** @type {Spring} */(ease).ease : setValue(ease, animEase ? 'linear' : animDefaults.ease); - const tDuration = hasSpring ? /** @type {Spring} */(ease).settlingDuration : setValue(duration, animDefaults.duration); + const animEase = setValue(ease, animDefaults.ease); + const animPlaybackEase = setValue(playbackEase, animDefaults.playbackEase); + const parsedAnimPlaybackEase = animPlaybackEase ? parseEase(animPlaybackEase) : null; + const hasSpring = !isUnd(/** @type {Spring} */(animEase).ease); + const tEasing = hasSpring ? /** @type {Spring} */(animEase).ease : setValue(ease, parsedAnimPlaybackEase ? 'linear' : animDefaults.ease); + const tDuration = hasSpring ? /** @type {Spring} */(animEase).settlingDuration : setValue(duration, animDefaults.duration); const tDelay = setValue(delay, animDefaults.delay); const tModifier = modifier || animDefaults.modifier; // If no composition is defined and the targets length is high (>= 1000) set the composition to 'none' (0) for faster tween creation @@ -3409,7 +3623,7 @@ // const absoluteOffsetTime = this._offset; const absoluteOffsetTime = this._offset + (parent ? parent._offset : 0); // This allows targeting the current animation in the spring onComplete callback - if (hasSpring) /** @type {Spring} */(ease).parent = this; + if (hasSpring) /** @type {Spring} */(animEase).parent = this; let iterationDuration = NaN; let iterationDelay = NaN; @@ -3420,7 +3634,7 @@ const target = parsedTargets[targetIndex]; const ti = index || targetIndex; - const tl = length || targetsLength; + const tl = allTargets || parsedTargets; let lastTransformGroupIndex = NaN; let lastTransformGroupLength = NaN; @@ -3494,8 +3708,16 @@ } toFunctionStore.func = null; + fromFunctionStore.func = null; - const computedToValue = getFunctionValue(key.to, target, ti, tl, toFunctionStore); + const computedComposition = getFunctionValue(setValue(key.composition, tComposition), target, ti, tl, null, null); + const tweenComposition = isNum(computedComposition) ? computedComposition : compositionTypes[computedComposition]; + if (!siblings && tweenComposition !== compositionTypes.none) siblings = getTweenSiblings(target, propName); + // Timelines pass the last sibling tween if it belongs to the same timeline + // Standalone animations only pass prevTween when the property has multiple keyframes + const tailTween = siblings ? siblings._tail : null; + const prevSiblingTween = parent && tailTween && tailTween.parent.parent === parent ? tailTween : prevTween; + const computedToValue = getFunctionValue(key.to, target, ti, tl, toFunctionStore, prevSiblingTween); let tweenToValue; // Allows function based values to return an object syntax value ({to: v}) @@ -3505,17 +3727,18 @@ } else { tweenToValue = computedToValue; } - const tweenFromValue = getFunctionValue(key.from, target, ti, tl); - const keyEasing = key.ease; + const tweenFromValue = getFunctionValue(key.from, target, ti, tl, null, prevSiblingTween); + const easeToParse = key.ease || tEasing; + + const easeFunctionResult = getFunctionValue(easeToParse, target, ti, tl, null, prevSiblingTween); + const keyEasing = isFnc(easeFunctionResult) || isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; + const hasSpring = !isUnd(keyEasing) && !isUnd(/** @type {Spring} */(keyEasing).ease); - // Easing are treated differently and don't accept function based value to prevent having to pass a function wrapper that returns an other function all the time - const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing || tEasing; + const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; // Calculate default individual keyframe duration by dividing the tl of keyframes - const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(setValue(key.duration, (l > 1 ? getFunctionValue(tDuration, target, ti, tl) / l : tDuration)), target, ti, tl); + const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(setValue(key.duration, (l > 1 ? getFunctionValue(tDuration, target, ti, tl, null, prevSiblingTween) / l : tDuration)), target, ti, tl, null, prevSiblingTween); // Default delay value should only be applied to the first tween - const tweenDelay = getFunctionValue(setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl); - const computedComposition = getFunctionValue(setValue(key.composition, tComposition), target, ti, tl); - const tweenComposition = isNum(computedComposition) ? computedComposition : compositionTypes[computedComposition]; + const tweenDelay = getFunctionValue(setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl, null, prevSiblingTween); // Modifiers are treated differently and don't accept function based value to prevent having to pass a function wrapper const tweenModifier = key.modifier || tModifier; const hasFromvalue = !isUnd(tweenFromValue); @@ -3532,7 +3755,6 @@ let prevSibling = prevTween; if (tweenComposition !== compositionTypes.none) { - if (!siblings) siblings = getTweenSiblings(target, propName); let nextSibling = siblings._head; // Iterate trough all the next siblings until we find a sibling with an equal or inferior start time while (nextSibling && !nextSibling._isOverridden && nextSibling._absoluteStartTime <= absoluteStartTime) { @@ -3551,8 +3773,10 @@ // Decompose values if (isFromToValue) { - decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[0], target, ti, tl) : tweenFromValue, fromTargetObject); - decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore) : tweenToValue, toTargetObject); + decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[0], target, ti, tl, fromFunctionStore, prevSiblingTween) : tweenFromValue, fromTargetObject); + decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore, prevSiblingTween) : tweenToValue, toTargetObject); + // Needed to force an inline style registration + const originalValue = getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore); if (fromTargetObject.t === valueTypes.NUMBER) { if (prevSibling) { if (prevSibling._valueType === valueTypes.UNIT) { @@ -3561,7 +3785,7 @@ } } else { decomposeRawValue( - getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore), + originalValue, decomposedOriginalValue ); if (decomposedOriginalValue.t === valueTypes.UNIT) { @@ -3666,7 +3890,8 @@ property: propName, target: target, _value: null, - _func: toFunctionStore.func, + _toFunc: toFunctionStore.func, + _fromFunc: fromFunctionStore.func, _ease: parseEase(tweenEasing), _fromNumbers: cloneArray(fromTargetObject.d), _toNumbers: cloneArray(toTargetObject.d), @@ -3703,6 +3928,18 @@ composeTween(tween, siblings); } + // Pre-compute the tween end value for function-based value chaining (ie morphTo / scrambleText in keyframe arrays and timelines) + const vt = tween._valueType; + if (vt === valueTypes.COMPLEX) { + tween._value = composeComplexValue(tween, 1, -1); + } else if (vt === valueTypes.COLOR) { + tween._value = composeColorValue(tween, 1, -1); + } else if (vt === valueTypes.UNIT) { + tween._value = `${tweenModifier(tween._toNumber)}${tween._unit}`; + } else { + tween._value = tweenModifier(tween._toNumber); + } + if (isNaN(firstTweenChangeStartTime)) { firstTweenChangeStartTime = tween._startTime; } @@ -3780,12 +4017,14 @@ } /** @type {TargetsArray} */ this.targets = parsedTargets; + /** @type {String|Number} */ + this.id = !isUnd(id) ? id : JSAnimationId; /** @type {Number} */ this.duration = iterationDuration === minValue ? minValue : clampInfinity(((iterationDuration + this._loopDelay) * this.iterationCount) - this._loopDelay) || minValue; /** @type {Callback} */ this.onRender = onRender || animDefaults.onRender; /** @type {EasingFunction} */ - this._ease = animEase; + this._ease = parsedAnimPlaybackEase; /** @type {Number} */ this._delay = iterationDelay; // NOTE: I'm keeping delay values separated from offsets in timelines because delays can override previous tweens and it could be confusing to debug a timeline with overridden tweens and no associated visible delays. @@ -3822,18 +4061,29 @@ */ refresh() { forEachChildren(this, (/** @type {Tween} */tween) => { - const tweenFunc = tween._func; - if (tweenFunc) { - const ogValue = getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType); - decomposeRawValue(ogValue, decomposedOriginalValue); - // TODO: Check for from / to Array based values here, - decomposeRawValue(tweenFunc(), toTargetObject); - tween._fromNumbers = cloneArray(decomposedOriginalValue.d); - tween._fromNumber = decomposedOriginalValue.n; - tween._toNumbers = cloneArray(toTargetObject.d); - tween._strings = cloneArray(toTargetObject.s); - // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 - tween._toNumber = toTargetObject.o ? getRelativeValue(decomposedOriginalValue.n, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + const toFunc = tween._toFunc; + const fromFunc = tween._fromFunc; + if (toFunc || fromFunc) { + if (fromFunc) { + decomposeRawValue(fromFunc(), fromTargetObject); + if (fromTargetObject.u !== tween._unit && tween.target[isDomSymbol]) { + convertValueUnit(/** @type {DOMTarget} */(tween.target), fromTargetObject, tween._unit, true); + } + tween._fromNumbers = cloneArray(fromTargetObject.d); + tween._fromNumber = fromTargetObject.n; + } else if (toFunc) { + // When only toFunc exists, get from value from target + decomposeRawValue(getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType), decomposedOriginalValue); + tween._fromNumbers = cloneArray(decomposedOriginalValue.d); + tween._fromNumber = decomposedOriginalValue.n; + } + if (toFunc) { + decomposeRawValue(toFunc(), toTargetObject); + tween._toNumbers = cloneArray(toTargetObject.d); + tween._strings = cloneArray(toTargetObject.s); + // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 + tween._toNumber = toTargetObject.o ? getRelativeValue(tween._fromNumber, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + } } }); // This forces setter animations to render once @@ -3847,7 +4097,7 @@ */ revert() { super.revert(); - return cleanInlineStyles(this); + return revertValues(this); } /** @@ -3869,231 +4119,75 @@ * @param {AnimationParams} parameters * @return {JSAnimation} */ - const animate = (targets, parameters) => new JSAnimation(targets, parameters, null, 0, false).init(); + const animate = (targets, parameters) => { + if (globals.editor) { + return globals.editor.addAnimation(targets, parameters); + } else { + return new JSAnimation(targets, parameters, null, 0, false).init(); + } + }; - const WAAPIAnimationsLookups = { - _head: null, - _tail: null, - }; - /** - * @param {DOMTarget} $el - * @param {String} [property] - * @param {WAAPIAnimation} [parent] - * @return {globalThis.Animation} + * Timeline's children offsets positions parser + * @param {Timeline} timeline + * @param {String} timePosition + * @return {Number} */ - const removeWAAPIAnimation = ($el, property, parent) => { - let nextLookup = WAAPIAnimationsLookups._head; - let anim; - while (nextLookup) { - const next = nextLookup._next; - const matchTarget = nextLookup.$el === $el; - const matchProperty = !property || nextLookup.property === property; - const matchParent = !parent || nextLookup.parent === parent; - if (matchTarget && matchProperty && matchParent) { - anim = nextLookup.animation; - try { anim.commitStyles(); } catch {} anim.cancel(); - removeChild(WAAPIAnimationsLookups, nextLookup); - const lookupParent = nextLookup.parent; - if (lookupParent) { - lookupParent._completed++; - if (lookupParent.animations.length === lookupParent._completed) { - lookupParent.completed = true; - lookupParent.paused = true; - if (!lookupParent.muteCallbacks) { - lookupParent.onComplete(lookupParent); - lookupParent._resolve(lookupParent); - } - } - } - } - nextLookup = next; + const getPrevChildOffset = (timeline, timePosition) => { + if (stringStartsWith(timePosition, '<')) { + const goToPrevAnimationOffset = timePosition[1] === '<'; + const prevAnimation = /** @type {Tickable} */(timeline._tail); + const prevOffset = prevAnimation ? prevAnimation._offset + prevAnimation._delay : 0; + return goToPrevAnimationOffset ? prevOffset : prevOffset + prevAnimation.duration; } - return anim; }; /** - * @param {WAAPIAnimation} parent - * @param {DOMTarget} $el - * @param {String} property - * @param {PropertyIndexedKeyframes} keyframes - * @param {KeyframeAnimationOptions} params - * @retun {globalThis.Animation} + * @param {Timeline} timeline + * @param {TimelinePosition} [timePosition] + * @return {Number} */ - const addWAAPIAnimation = (parent, $el, property, keyframes, params) => { - const animation = $el.animate(keyframes, params); - const animTotalDuration = params.delay + (+params.duration * params.iterations); - animation.playbackRate = parent._speed; - if (parent.paused) animation.pause(); - if (parent.duration < animTotalDuration) { - parent.duration = animTotalDuration; - parent.controlAnimation = animation; - } - parent.animations.push(animation); - removeWAAPIAnimation($el, property); - addChild(WAAPIAnimationsLookups, { parent, animation, $el, property, _next: null, _prev: null }); - const handleRemove = () => { removeWAAPIAnimation($el, property, parent); }; - animation.oncancel = handleRemove; - animation.onremove = handleRemove; - if (!parent.persist) { - animation.onfinish = handleRemove; + const parseTimelinePosition = (timeline, timePosition) => { + let tlDuration = timeline.iterationDuration; + if (tlDuration === minValue) tlDuration = 0; + if (isUnd(timePosition)) return tlDuration; + if (isNum(+timePosition)) return +timePosition; + const timePosStr = /** @type {String} */(timePosition); + const tlLabels = timeline ? timeline.labels : null; + const hasLabels = !isNil(tlLabels); + const prevOffset = getPrevChildOffset(timeline, timePosStr); + const hasSibling = !isUnd(prevOffset); + const matchedRelativeOperator = relativeValuesExecRgx.exec(timePosStr); + if (matchedRelativeOperator) { + const fullOperator = matchedRelativeOperator[0]; + const split = timePosStr.split(fullOperator); + const labelOffset = hasLabels && split[0] ? tlLabels[split[0]] : tlDuration; + const parsedOffset = hasSibling ? prevOffset : hasLabels ? labelOffset : tlDuration; + const parsedNumericalOffset = +split[1]; + return getRelativeValue(parsedOffset, parsedNumericalOffset, fullOperator[0]); + } else { + return hasSibling ? prevOffset : + hasLabels ? !isUnd(tlLabels[timePosStr]) ? tlLabels[timePosStr] : + tlDuration : tlDuration; } - return animation; }; + + /** - * @overload - * @param {DOMTargetSelector} targetSelector - * @param {String} propName - * @return {String} - * - * @overload - * @param {JSTargetsParam} targetSelector - * @param {String} propName - * @return {Number|String} - * - * @overload - * @param {DOMTargetsParam} targetSelector - * @param {String} propName - * @param {String} unit - * @return {String} - * - * @overload - * @param {TargetsParam} targetSelector - * @param {String} propName - * @param {Boolean} unit + * @param {Timeline} tl * @return {Number} - * - * @param {TargetsParam} targetSelector - * @param {String} propName - * @param {String|Boolean} [unit] */ - function get(targetSelector, propName, unit) { - const targets = registerTargets(targetSelector); - if (!targets.length) return; - const [ target ] = targets; - const tweenType = getTweenType(target, propName); - const normalizePropName = sanitizePropertyName(propName, target, tweenType); - let originalValue = getOriginalAnimatableValue(target, normalizePropName); - if (isUnd(unit)) { - return originalValue; - } else { - decomposeRawValue(originalValue, decomposedOriginalValue); - if (decomposedOriginalValue.t === valueTypes.NUMBER || decomposedOriginalValue.t === valueTypes.UNIT) { - if (unit === false) { - return decomposedOriginalValue.n; - } else { - const convertedValue = convertValueUnit(/** @type {DOMTarget} */(target), decomposedOriginalValue, /** @type {String} */(unit), false); - return `${round$1(convertedValue.n, globals.precision)}${convertedValue.u}`; - } - } - } - } - - /** - * @param {TargetsParam} targets - * @param {AnimationParams} parameters - * @return {JSAnimation} - */ - const set = (targets, parameters) => { - if (isUnd(parameters)) return; - parameters.duration = minValue; - // Do not overrides currently active tweens by default - parameters.composition = setValue(parameters.composition, compositionTypes.none); - // Skip init() and force rendering by playing the animation - return new JSAnimation(targets, parameters, null, 0, true).resume(); - }; - - /** - * @param {TargetsParam} targets - * @param {Renderable|WAAPIAnimation} [renderable] - * @param {String} [propertyName] - * @return {TargetsArray} - */ - const remove = (targets, renderable, propertyName) => { - const targetsArray = parseTargets(targets); - for (let i = 0, l = targetsArray.length; i < l; i++) { - removeWAAPIAnimation( - /** @type {DOMTarget} */(targetsArray[i]), - propertyName, - renderable && /** @type {WAAPIAnimation} */(renderable).controlAnimation && /** @type {WAAPIAnimation} */(renderable), - ); - } - removeTargetsFromRenderable( - targetsArray, - /** @type {Renderable} */(renderable), - propertyName - ); - return targetsArray; - }; - - - - - - /** - * Timeline's children offsets positions parser - * @param {Timeline} timeline - * @param {String} timePosition - * @return {Number} - */ - const getPrevChildOffset = (timeline, timePosition) => { - if (stringStartsWith(timePosition, '<')) { - const goToPrevAnimationOffset = timePosition[1] === '<'; - const prevAnimation = /** @type {Tickable} */(timeline._tail); - const prevOffset = prevAnimation ? prevAnimation._offset + prevAnimation._delay : 0; - return goToPrevAnimationOffset ? prevOffset : prevOffset + prevAnimation.duration; - } - }; - - /** - * @param {Timeline} timeline - * @param {TimelinePosition} [timePosition] - * @return {Number} - */ - const parseTimelinePosition = (timeline, timePosition) => { - let tlDuration = timeline.iterationDuration; - if (tlDuration === minValue) tlDuration = 0; - if (isUnd(timePosition)) return tlDuration; - if (isNum(+timePosition)) return +timePosition; - const timePosStr = /** @type {String} */(timePosition); - const tlLabels = timeline ? timeline.labels : null; - const hasLabels = !isNil(tlLabels); - const prevOffset = getPrevChildOffset(timeline, timePosStr); - const hasSibling = !isUnd(prevOffset); - const matchedRelativeOperator = relativeValuesExecRgx.exec(timePosStr); - if (matchedRelativeOperator) { - const fullOperator = matchedRelativeOperator[0]; - const split = timePosStr.split(fullOperator); - const labelOffset = hasLabels && split[0] ? tlLabels[split[0]] : tlDuration; - const parsedOffset = hasSibling ? prevOffset : hasLabels ? labelOffset : tlDuration; - const parsedNumericalOffset = +split[1]; - return getRelativeValue(parsedOffset, parsedNumericalOffset, fullOperator[0]); - } else { - return hasSibling ? prevOffset : - hasLabels ? !isUnd(tlLabels[timePosStr]) ? tlLabels[timePosStr] : - tlDuration : tlDuration; - } - }; - - - - - - /** - * @param {Timeline} tl - * @return {Number} - */ - function getTimelineTotalDuration(tl) { - return clampInfinity(((tl.iterationDuration + tl._loopDelay) * tl.iterationCount) - tl._loopDelay) || minValue; + function getTimelineTotalDuration(tl) { + return clampInfinity(((tl.iterationDuration + tl._loopDelay) * tl.iterationCount) - tl._loopDelay) || minValue; } /** @@ -4109,7 +4203,7 @@ * @param {Number} timePosition * @param {TargetsParam} targets * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] * @return {Timeline} * * @param {TimerParams|AnimationParams} childParams @@ -4117,17 +4211,17 @@ * @param {Number} timePosition * @param {TargetsParam} [targets] * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] */ - function addTlChild(childParams, tl, timePosition, targets, index, length) { + function addTlChild(childParams, tl, timePosition, targets, index, allTargets) { const isSetter = isNum(childParams.duration) && /** @type {Number} */(childParams.duration) <= minValue; // Offset the tl position with -minValue for 0 duration animations or .set() calls in order to align their end value with the defined position const adjustedPosition = isSetter ? timePosition - minValue : timePosition; - tick(tl, adjustedPosition, 1, 1, tickModes.AUTO); + if (tl.composition) tick(tl, adjustedPosition, 1, 1, tickModes.AUTO); const tlChild = targets ? - new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, length) : + new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, allTargets) : new Timer(/** @type {TimerParams} */(childParams), tl, adjustedPosition); - tlChild.init(true); + if (tl.composition) tlChild.init(true); // TODO: Might be better to insert at a position relative to startTime? addChild(tl, tlChild); forEachChildren(tl, (/** @type {Renderable} */child) => { @@ -4139,6 +4233,8 @@ return tl; } + let TLId = 0; + class Timeline extends Timer { /** @@ -4146,6 +4242,9 @@ */ constructor(parameters = {}) { super(/** @type {TimerParams&TimelineParams} */(parameters), null, 0); + ++TLId; + /** @type {String|Number} */ + this.id = !isUnd(parameters.id) ? parameters.id : TLId; /** @type {Number} */ this.duration = 0; // TL duration starts at 0 and grows when adding children /** @type {Record} */ @@ -4154,6 +4253,8 @@ const globalDefaults = globals.defaults; /** @type {DefaultsParams} */ this.defaults = defaultsParams ? mergeObjects(defaultsParams, globalDefaults) : globalDefaults; + /** @type {Boolean} */ + this.composition = setValue(parameters.composition, true); /** @type {Callback} */ this.onRender = parameters.onRender || globalDefaults.onRender; const tlPlaybackEase = setValue(parameters.playbackEase, globalDefaults.playbackEase); @@ -4166,7 +4267,7 @@ * @overload * @param {TargetsParam} a1 * @param {AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] * @return {this} * * @overload @@ -4176,7 +4277,7 @@ * * @param {TargetsParam|TimerParams} a1 * @param {TimelinePosition|AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] */ add(a1, a2, a3) { const isAnim = isObj(a2); @@ -4185,9 +4286,11 @@ this._hasChildren = true; if (isAnim) { const childParams = /** @type {AnimationParams} */(a2); - // Check for function for children stagger positions - if (isFnc(a3)) { - const staggeredPosition = a3; + const editorHook = globals.editor && globals.editor.addTimelineChild; + const isStaggerType = a3 && /** @type {TweakRegister} */(a3).type === 'Stagger' && globals.editor; + // Check for function or Stagger type children positions + const staggeredPosition = isFnc(a3) ? a3 : null; + if (staggeredPosition || isStaggerType) { const parsedTargetsArray = parseTargets(/** @type {TargetsParam} */(a1)); // Store initial duration before adding new children that will change the duration const tlDuration = this.duration; @@ -4198,28 +4301,36 @@ let i = 0; /** @type {Number} */ const parsedLength = (parsedTargetsArray.length); + // Call editor hook once for the entire stagger group instead of per target + const resolvedParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3, parsedLength) : null; + // Resolve stagger AFTER editor hook so tweaked position value (a3.defaultValue) is used + const staggerFn = staggeredPosition || globals.editor.resolveStagger(/** @type {TweakRegister} */(a3).defaultValue); parsedTargetsArray.forEach((/** @type {Target} */target) => { // Create a new parameter object for each staggered children - const staggeredChildParams = { ...childParams }; + const staggeredChildParams = { ...(resolvedParams || childParams) }; // Reset the duration of the timeline iteration before each stagger to prevent wrong start value calculation this.duration = tlDuration; this.iterationDuration = tlIterationDuration; if (!isUnd(id)) staggeredChildParams.id = id + '-' + i; + const staggeredTimePosition = parseTimelinePosition(this, staggerFn(target, i, parsedTargetsArray, null, this)); addTlChild( staggeredChildParams, this, - parseTimelinePosition(this, staggeredPosition(target, i, parsedLength, this)), + staggeredTimePosition, target, i, - parsedLength + parsedTargetsArray, ); i++; }); } else { + // Call editor hook before resolving position so tweaked values are applied + const resolvedChildParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3) : childParams; + const resolvedPosition = a3 && /** @type {*} */(a3).type ? /** @type {*} */(a3).defaultValue : a3; addTlChild( - childParams, + resolvedChildParams, this, - parseTimelinePosition(this, a3), + parseTimelinePosition(this, resolvedPosition), /** @type {TargetsParam} */(a1), ); } @@ -4231,7 +4342,8 @@ parseTimelinePosition(this,a2), ); } - return this.init(true); + if (this.composition) this.init(true); + return this; } } @@ -4258,7 +4370,11 @@ if (isUnd(synced) || synced && isUnd(synced.pause)) return this; synced.pause(); const duration = +(/** @type {globalThis.Animation} */(synced).effect ? /** @type {globalThis.Animation} */(synced).effect.getTiming().duration : /** @type {Tickable} */(synced).duration); - return this.add(synced, { currentTime: [0, duration], duration, ease: 'linear' }, position); + // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. + if (!isUnd(synced) && !isUnd(/** @type {WAAPIAnimation} */(synced).persist)) { + /** @type {WAAPIAnimation} */(synced).persist = true; + } + return this.add(synced, { currentTime: [0, duration], duration, delay: 0, ease: 'linear', playbackEase: 'linear' }, position); } /** @@ -4281,7 +4397,7 @@ */ call(callback, position) { if (isUnd(callback) || callback && !isFnc(callback)) return this; - return this.add({ duration: 0, onComplete: () => callback(this) }, position); + return this.add({ duration: 0, delay: 0, onComplete: () => callback(this) }, position); } /** @@ -4324,8 +4440,8 @@ * @return {this} */ refresh() { - forEachChildren(this, (/** @type {JSAnimation} */child) => { - if (child.refresh) child.refresh(); + forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => { + if (/** @type {JSAnimation} */(child).refresh) /** @type {JSAnimation} */(child).refresh(); }); return this; } @@ -4335,8 +4451,8 @@ */ revert() { super.revert(); - forEachChildren(this, (/** @type {JSAnimation} */child) => child.revert, true); - return cleanInlineStyles(this); + forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => child.revert, true); + return revertValues(this); } /** @@ -4356,7 +4472,12 @@ * @param {TimelineParams} [parameters] * @return {Timeline} */ - const createTimeline = parameters => new Timeline(parameters).init(); + const createTimeline = parameters => { + if (globals.editor) { + return /** @type {Timeline} */(/** @type {unknown} */(globals.editor.addTimeline(parameters))); + } + return new Timeline(parameters).init(); + }; @@ -4803,63 +4924,227 @@ + const WAAPIAnimationsLookups = { + _head: null, + _tail: null, + }; + /** - * @param {Event} e + * @param {DOMTarget} $el + * @param {String} [property] + * @param {WAAPIAnimation} [parent] + * @return {globalThis.Animation} */ - const preventDefault = e => { - if (e.cancelable) e.preventDefault(); + const removeWAAPIAnimation = ($el, property, parent) => { + let nextLookup = WAAPIAnimationsLookups._head; + let anim; + while (nextLookup) { + const next = nextLookup._next; + const matchTarget = nextLookup.$el === $el; + const matchProperty = !property || nextLookup.property === property; + const matchParent = !parent || nextLookup.parent === parent; + if (matchTarget && matchProperty && matchParent) { + anim = nextLookup.animation; + try { anim.commitStyles(); } catch {} anim.cancel(); + removeChild(WAAPIAnimationsLookups, nextLookup); + const lookupParent = nextLookup.parent; + if (lookupParent) { + lookupParent._completed++; + if (lookupParent.animations.length === lookupParent._completed) { + lookupParent.completed = true; + lookupParent.paused = true; + if (!lookupParent.muteCallbacks) { + lookupParent.onComplete(lookupParent); + lookupParent._resolve(lookupParent); + } + } + } + } + nextLookup = next; + } + return anim; }; - class DOMProxy { - /** @param {Object} el */ - constructor(el) { - this.el = el; - this.zIndex = 0; - this.parentElement = null; - this.classList = { - add: noop, - remove: noop, - }; + /** + * @param {WAAPIAnimation} parent + * @param {DOMTarget} $el + * @param {String} property + * @param {PropertyIndexedKeyframes} keyframes + * @param {KeyframeAnimationOptions} params + * @retun {globalThis.Animation} + */ + const addWAAPIAnimation = (parent, $el, property, keyframes, params) => { + const animation = $el.animate(keyframes, params); + const animTotalDuration = params.delay + (+params.duration * params.iterations); + animation.playbackRate = parent._speed; + if (parent.paused) animation.pause(); + if (parent.duration < animTotalDuration) { + parent.duration = animTotalDuration; + parent.controlAnimation = animation; } + parent.animations.push(animation); + removeWAAPIAnimation($el, property); + addChild(WAAPIAnimationsLookups, { parent, animation, $el, property, _next: null, _prev: null }); + const handleRemove = () => removeWAAPIAnimation($el, property, parent); + animation.oncancel = handleRemove; + animation.onremove = handleRemove; + if (!parent.persist) { + animation.onfinish = handleRemove; + } + return animation; + }; - get x() { return this.el.x || 0 }; - set x(v) { this.el.x = v; }; - - get y() { return this.el.y || 0 }; - set y(v) { this.el.y = v; }; - - get width() { return this.el.width || 0 }; - set width(v) { this.el.width = v; }; + - get height() { return this.el.height || 0 }; - set height(v) { this.el.height = v; }; + - getBoundingClientRect() { - return { - top: this.y, - right: this.x, - bottom: this.y + this.height, - left: this.x + this.width, + /** + * @overload + * @param {DOMTargetSelector} targetSelector + * @param {String} propName + * @return {String} + * + * @overload + * @param {JSTargetsParam} targetSelector + * @param {String} propName + * @return {Number|String} + * + * @overload + * @param {DOMTargetsParam} targetSelector + * @param {String} propName + * @param {String} unit + * @return {String} + * + * @overload + * @param {TargetsParam} targetSelector + * @param {String} propName + * @param {Boolean} unit + * @return {Number} + * + * @param {TargetsParam} targetSelector + * @param {String} propName + * @param {String|Boolean} [unit] + */ + function get(targetSelector, propName, unit) { + const targets = registerTargets(targetSelector); + if (!targets.length) return; + const [ target ] = targets; + const tweenType = getTweenType(target, propName); + const normalizePropName = sanitizePropertyName(propName, target, tweenType); + let originalValue = getOriginalAnimatableValue(target, normalizePropName); + if (isUnd(unit)) { + return originalValue; + } else { + decomposeRawValue(originalValue, decomposedOriginalValue); + if (decomposedOriginalValue.t === valueTypes.NUMBER || decomposedOriginalValue.t === valueTypes.UNIT) { + if (unit === false) { + return decomposedOriginalValue.n; + } else { + const convertedValue = convertValueUnit(/** @type {DOMTarget} */(target), decomposedOriginalValue, /** @type {String} */(unit), false); + return `${round$1(convertedValue.n, globals.precision)}${convertedValue.u}`; + } } } } - class Transforms { - /** - * @param {DOMTarget|DOMProxy} $el - */ - constructor($el) { - this.$el = $el; - this.inlineTransforms = []; - this.point = new DOMPoint(); - this.inversedMatrix = this.getMatrix().inverse(); - } - - /** - * @param {Number} x - * @param {Number} y - * @return {DOMPoint} - */ + /** + * @param {TargetsParam} targets + * @param {AnimationParams} parameters + * @return {JSAnimation} + */ + const set = (targets, parameters) => { + if (isUnd(parameters)) return; + parameters.duration = minValue; + // Do not overrides currently active tweens by default + parameters.composition = setValue(parameters.composition, compositionTypes.none); + // Skip init() and force rendering by playing the animation + return new JSAnimation(targets, parameters, null, 0, true).resume(); + }; + + /** + * @param {TargetsParam} targets + * @param {Renderable|WAAPIAnimation} [renderable] + * @param {String} [propertyName] + * @return {TargetsArray} + */ + const remove = (targets, renderable, propertyName) => { + const targetsArray = parseTargets(targets); + for (let i = 0, l = targetsArray.length; i < l; i++) { + removeWAAPIAnimation( + /** @type {DOMTarget} */(targetsArray[i]), + propertyName, + renderable && /** @type {WAAPIAnimation} */(renderable).controlAnimation && /** @type {WAAPIAnimation} */(renderable), + ); + } + removeTargetsFromRenderable( + targetsArray, + /** @type {Renderable} */(renderable), + propertyName + ); + return targetsArray; + }; + + + + + + /** + * @param {Event} e + */ + const preventDefault = e => { + if (e.cancelable) e.preventDefault(); + }; + + class DOMProxy { + /** @param {Object} el */ + constructor(el) { + this.el = el; + this.zIndex = 0; + this.parentElement = null; + this.classList = { + add: noop, + remove: noop, + }; + } + + get x() { return this.el.x || 0 }; + set x(v) { this.el.x = v; }; + + get y() { return this.el.y || 0 }; + set y(v) { this.el.y = v; }; + + get width() { return this.el.width || 0 }; + set width(v) { this.el.width = v; }; + + get height() { return this.el.height || 0 }; + set height(v) { this.el.height = v; }; + + getBoundingClientRect() { + return { + top: this.y, + right: this.x, + bottom: this.y + this.height, + left: this.x + this.width, + } + } + } + + class Transforms { + /** + * @param {DOMTarget|DOMProxy} $el + */ + constructor($el) { + this.$el = $el; + this.inlineTransforms = []; + this.point = new DOMPoint(); + this.inversedMatrix = this.getMatrix().inverse(); + } + + /** + * @param {Number} x + * @param {Number} y + * @return {DOMPoint} + */ normalizePoint(x, y) { this.point.x = x; this.point.y = y; @@ -6005,19 +6290,20 @@ }; /** - * @param {(...args: any[]) => Tickable | ((...args: any[]) => void)} constructor + * @param {(...args: any[]) => Tickable | ((...args: any[]) => void) | void} constructor * @return {(...args: any[]) => Tickable | ((...args: any[]) => void)} */ const keepTime = constructor => { /** @type {Tickable} */ let tracked; return (...args) => { - let currentIteration, currentIterationProgress, reversed, alternate; + let currentIteration, currentIterationProgress, reversed, alternate, startTime; if (tracked) { currentIteration = tracked.currentIteration; currentIterationProgress = tracked.iterationProgress; reversed = tracked.reversed; alternate = tracked._alternate; + startTime = tracked._startTime; tracked.revert(); } const cleanup = constructor(...args); @@ -6025,6 +6311,7 @@ if (!isUnd(currentIterationProgress)) { /** @type {Tickable} */(tracked).currentIteration = currentIteration; /** @type {Tickable} */(tracked).iterationProgress = (alternate ? !(currentIteration % 2) ? reversed : !reversed : reversed) ? 1 - currentIterationProgress : currentIterationProgress; + /** @type {Tickable} */(tracked)._startTime = startTime; } return cleanup || noop; } @@ -6432,6 +6719,7 @@ this.updateBounds(); forEachChildren(this, (/** @type {ScrollObserver} */child) => { child.refresh(); + child.onResize(child); if (child._debug) { child.debug(); } @@ -6643,6 +6931,8 @@ /** @type {Callback} */ this.onUpdate = parameters.onUpdate || noop; /** @type {Callback} */ + this.onResize = parameters.onResize || noop; + /** @type {Callback} */ this.onSyncComplete = parameters.onSyncComplete || noop; /** @type {Boolean} */ this.reverted = false; @@ -6706,7 +6996,9 @@ linked.pause(); this.linked = linked; // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. - if (!isUnd(/** @type {WAAPIAnimation} */(linked))) /** @type {WAAPIAnimation} */(linked).persist = true; + if (!isUnd(linked) && !isUnd(/** @type {WAAPIAnimation} */(linked).persist)) { + /** @type {WAAPIAnimation} */(linked).persist = true; + } // Try to use a target of the linked object if no target parameters specified if (!this._params.target) { /** @type {HTMLElement} */ @@ -6908,12 +7200,11 @@ // let offsetX = 0; // let offsetY = 0; // let $offsetParent = $el; - /** @type {Element} */ if (linked) { linkedTime = linked.currentTime; linked.seek(0, true); } - /* Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 + // Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 // const isContainerStatic = get(container.element, 'position') === 'static' ? set(container.element, { position: 'relative '}) : false; // while ($el && $el !== container.element && $el !== doc.body) { // const isSticky = get($el, 'position') === 'sticky' ? @@ -7287,1521 +7578,3446 @@ steps: steps }); - // Chain-able utilities + - const numberUtils = numberImports; // Needed to keep the import when bundling + - const chainables = {}; + /** - * @callback UtilityFunction - * @param {...*} args - * @return {Number|String} - * - * @param {UtilityFunction} fn - * @param {Number} [last=0] - * @return {function(...(Number|String)): function(Number|String): (Number|String)} + * Converts an easing function into a valid CSS linear() timing function string + * @param {EasingFunction} fn + * @param {number} [samples=100] + * @returns {string} CSS linear() timing function */ - const curry = (fn, last = 0) => (...args) => last ? v => fn(...args, v) : v => fn(v, ...args); + const easingToLinear = (fn, samples = 100) => { + const points = []; + for (let i = 0; i <= samples; i++) points.push(round$1(fn(i / samples), 4)); + return `linear(${points.join(', ')})`; + }; + + const WAAPIEasesLookups = {}; /** - * @param {Function} fn - * @return {function(...(Number|String))} + * @param {EasingParam} ease + * @return {String} */ - const chain = fn => { - return (...args) => { - const result = fn(...args); - return new Proxy(noop, { - apply: (_, __, [v]) => result(v), - get: (_, prop) => chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { - const nextResult = chainables[prop](...nextArgs); - return (/**@type {Number|String} */v) => nextResult(result(v)); - }) - }); + const parseWAAPIEasing = (ease) => { + let parsedEase = WAAPIEasesLookups[ease]; + if (parsedEase) return parsedEase; + parsedEase = 'linear'; + if (isStr(ease)) { + if ( + stringStartsWith(ease, 'linear') || + stringStartsWith(ease, 'cubic-') || + stringStartsWith(ease, 'steps') || + stringStartsWith(ease, 'ease') + ) { + parsedEase = ease; + } else if (stringStartsWith(ease, 'cubicB')) { + parsedEase = toLowerCase(ease); + } else { + const parsed = parseEaseString(ease); + if (isFnc(parsed)) parsedEase = parsed === none ? 'linear' : easingToLinear(parsed); + } + // Only cache string based easing name, otherwise function arguments get lost + WAAPIEasesLookups[ease] = parsedEase; + } else if (isFnc(ease)) { + const easing = easingToLinear(ease); + if (easing) parsedEase = easing; + } else if (/** @type {Spring} */(ease).ease) { + parsedEase = easingToLinear(/** @type {Spring} */(ease).ease); } + return parsedEase; }; - /** - * @param {UtilityFunction} fn - * @param {String} name - * @param {Number} [right] - * @return {function(...(Number|String)): UtilityFunction} - */ - const makeChainable = (name, fn, right = 0) => { - const chained = (...args) => (args.length < fn.length ? chain(curry(fn, right)) : fn)(...args); - if (!chainables[name]) chainables[name] = chained; - return chained; - }; + const transformsShorthands = ['x', 'y', 'z']; + const commonDefaultPXProperties = [ + 'perspective', + 'width', + 'height', + 'margin', + 'padding', + 'top', + 'right', + 'bottom', + 'left', + 'borderWidth', + 'fontSize', + 'borderRadius', + ...transformsShorthands + ]; - /** - * @typedef {Object} ChainablesMap - * @property {ChainedClamp} clamp - * @property {ChainedRound} round - * @property {ChainedSnap} snap - * @property {ChainedWrap} wrap - * @property {ChainedLerp} lerp - * @property {ChainedDamp} damp - * @property {ChainedMapRange} mapRange - * @property {ChainedRoundPad} roundPad - * @property {ChainedPadStart} padStart - * @property {ChainedPadEnd} padEnd - * @property {ChainedDegToRad} degToRad - * @property {ChainedRadToDeg} radToDeg - */ + const validIndividualTransforms = /*#__PURE__*/ (() => [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))])(); - /** - * @callback ChainedUtilsResult - * @param {Number} value - The value to process through the chained operations - * @return {Number} The processed result - */ + let transformsPropertiesRegistered = null; /** - * @typedef {ChainablesMap & ChainedUtilsResult} ChainableUtil + * @param {String} propName + * @param {WAAPIKeyframeValue} value + * @param {DOMTarget} $el + * @param {Number} i + * @param {DOMTargetsArray} parsedTargets + * @return {String} */ - - // Chainable + const normalizeTweenValue = (propName, value, $el, i, parsedTargets) => { + // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables + let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, parsedTargets, null, null); + if (!isNum(v)) return v; + if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`; + if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`; + return `${v}`; + }; /** - * @callback ChainedRoundPad - * @param {Number} decimalLength - Number of decimal places - * @return {ChainableUtil} + * @param {DOMTarget} $el + * @param {String} propName + * @param {WAAPIKeyframeValue} from + * @param {WAAPIKeyframeValue} to + * @param {Number} i + * @param {DOMTargetsArray} parsedTargets + * @return {WAAPITweenValue} */ - const roundPad = /** @type {typeof numberUtils.roundPad & ChainedRoundPad} */(makeChainable('roundPad', numberUtils.roundPad)); + const parseIndividualTweenValue = ($el, propName, from, to, i, parsedTargets) => { + /** @type {WAAPITweenValue} */ + let tweenValue = '0'; + const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, parsedTargets) : getComputedStyle($el)[propName]; + if (!isUnd(from)) { + const computedFrom = normalizeTweenValue(propName, from, $el, i, parsedTargets); + tweenValue = [computedFrom, computedTo]; + } else { + tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, parsedTargets)) : computedTo; + } + return tweenValue; + }; + class WAAPIAnimation { /** - * @callback ChainedPadStart - * @param {Number} totalLength - Target length - * @param {String} padString - String to pad with - * @return {ChainableUtil} + * @param {DOMTargetsParam} targets + * @param {WAAPIAnimationParams} params */ - const padStart = /** @type {typeof numberUtils.padStart & ChainedPadStart} */(makeChainable('padStart', numberUtils.padStart)); + constructor(targets, params) { - /** - * @callback ChainedPadEnd - * @param {Number} totalLength - Target length - * @param {String} padString - String to pad with - * @return {ChainableUtil} - */ - const padEnd = /** @type {typeof numberUtils.padEnd & ChainedPadEnd} */(makeChainable('padEnd', numberUtils.padEnd)); + if (scope.current) scope.current.register(this); - /** - * @callback ChainedWrap - * @param {Number} min - Minimum boundary - * @param {Number} max - Maximum boundary - * @return {ChainableUtil} - */ - const wrap = /** @type {typeof numberUtils.wrap & ChainedWrap} */(makeChainable('wrap', numberUtils.wrap)); + // Skip the registration and fallback to no animation in case CSS.registerProperty is not supported + if (isNil(transformsPropertiesRegistered)) { + if (isBrowser && (isUnd(CSS) || !Object.hasOwnProperty.call(CSS, 'registerProperty'))) { + transformsPropertiesRegistered = false; + } else { + validTransforms.forEach(t => { + const isSkew = stringStartsWith(t, 'skew'); + const isScale = stringStartsWith(t, 'scale'); + const isRotate = stringStartsWith(t, 'rotate'); + const isTranslate = stringStartsWith(t, 'translate'); + const isAngle = isRotate || isSkew; + const syntax = isAngle ? '' : isScale ? "" : isTranslate ? "" : "*"; + try { + CSS.registerProperty({ + name: '--' + t, + syntax, + inherits: false, + initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0', + }); + } catch {} }); + transformsPropertiesRegistered = true; + } + } - /** - * @callback ChainedMapRange - * @param {Number} inLow - Input range minimum - * @param {Number} inHigh - Input range maximum - * @param {Number} outLow - Output range minimum - * @param {Number} outHigh - Output range maximum - * @return {ChainableUtil} - */ - const mapRange = /** @type {typeof numberUtils.mapRange & ChainedMapRange} */(makeChainable('mapRange', numberUtils.mapRange)); + const parsedTargets = registerTargets(targets); - /** - * @callback ChainedDegToRad - * @return {ChainableUtil} - */ - const degToRad = /** @type {typeof numberUtils.degToRad & ChainedDegToRad} */(makeChainable('degToRad', numberUtils.degToRad)); + if (!parsedTargets.length) { + console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`); + } - /** - * @callback ChainedRadToDeg - * @return {ChainableUtil} - */ - const radToDeg = /** @type {typeof numberUtils.radToDeg & ChainedRadToDeg} */(makeChainable('radToDeg', numberUtils.radToDeg)); + const autoplay = setValue(params.autoplay, globals.defaults.autoplay); + const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false; + const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true; + const reversed = params.reversed && /** @type {Boolean} */(params.reversed) === true; + const loop = setValue(params.loop, globals.defaults.loop); + const iterations = /** @type {Number} */((loop === true || loop === Infinity) ? Infinity : isNum(loop) ? loop + 1 : 1); + /** @type {PlaybackDirection} */ + const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal'; + /** @type {FillMode} */ + const fill = 'both'; // We use 'both' here because the animation can be reversed during playback + const timeScale = (globals.timeScale === 1 ? 1 : K); - /** - * @callback ChainedSnap - * @param {Number|Array} increment - Step size or array of snap points - * @return {ChainableUtil} - */ - const snap = /** @type {typeof numberUtils.snap & ChainedSnap} */(makeChainable('snap', numberUtils.snap)); + /** @type {DOMTargetsArray}] */ + this.targets = parsedTargets; + /** @type {Array}] */ + this.animations = []; + /** @type {globalThis.Animation}] */ + this.controlAnimation = null; + /** @type {Callback} */ + this.onComplete = params.onComplete || /** @type {Callback} */(/** @type {unknown} */(globals.defaults.onComplete)); + /** @type {Number} */ + this.duration = 0; + /** @type {Boolean} */ + this.muteCallbacks = false; + /** @type {Boolean} */ + this.completed = false; + /** @type {Boolean} */ + this.paused = !autoplay || scroll !== false; + /** @type {Boolean} */ + this.reversed = reversed; + /** @type {Boolean} */ + this.persist = setValue(params.persist, globals.defaults.persist); + /** @type {Boolean|ScrollObserver} */ + this.autoplay = autoplay; + /** @type {Number} */ + this._speed = setValue(params.playbackRate, globals.defaults.playbackRate); + /** @type {Function} */ + this._resolve = noop; // Used by .then() + /** @type {Number} */ + this._completed = 0; + /** @type {Array.} */ + this._inlineStyles = []; - /** - * @callback ChainedClamp - * @param {Number} min - Minimum boundary - * @param {Number} max - Maximum boundary - * @return {ChainableUtil} - */ - const clamp = /** @type {typeof numberUtils.clamp & ChainedClamp} */(makeChainable('clamp', numberUtils.clamp)); + parsedTargets.forEach(($el, i) => { - /** - * @callback ChainedRound - * @param {Number} decimalLength - Number of decimal places - * @return {ChainableUtil} - */ - const round = /** @type {typeof numberUtils.round & ChainedRound} */(makeChainable('round', numberUtils.round)); + const cachedTransforms = $el[transformsSymbol]; + const hasIndividualTransforms = validIndividualTransforms.some(t => params.hasOwnProperty(t)); + const elStyle = $el.style; + const inlineStyles = this._inlineStyles[i] = {}; - /** - * @callback ChainedLerp - * @param {Number} start - Starting value - * @param {Number} end - Ending value - * @return {ChainableUtil} - */ - const lerp = /** @type {typeof numberUtils.lerp & ChainedLerp} */(makeChainable('lerp', numberUtils.lerp, 1)); + const easeToParse = setValue(params.ease, globals.defaults.ease); - /** - * @callback ChainedDamp - * @param {Number} start - Starting value - * @param {Number} end - Target value - * @param {Number} deltaTime - Delta time in ms - * @return {ChainableUtil} - */ - const damp = /** @type {typeof numberUtils.damp & ChainedDamp} */(makeChainable('damp', numberUtils.damp, 1)); + const easeFunctionResult = getFunctionValue(easeToParse, $el, i, parsedTargets, null, null); + const keyEasing = isFnc(easeFunctionResult) || isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; - /** - * Generate a random number between optional min and max (inclusive) and decimal precision - * - * @callback RandomNumberGenerator - * @param {Number} [min=0] - The minimum value (inclusive) - * @param {Number} [max=1] - The maximum value (inclusive) - * @param {Number} [decimalLength=0] - Number of decimal places to round to - * @return {Number} A random number between min and max - */ + const spring = /** @type {Spring} */(easeToParse).ease && easeToParse; + /** @type {String} */ + const easing = parseWAAPIEasing(keyEasing); - /** - * Generates a random number between min and max (inclusive) with optional decimal precision - * - * @type {RandomNumberGenerator} - */ - const random = (min = 0, max = 1, decimalLength = 0) => { - const m = 10 ** decimalLength; - return Math.floor((Math.random() * (max - min + (1 / m)) + min) * m) / m; - }; + /** @type {Number} */ + const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, parsedTargets, null, null)) * timeScale; + /** @type {Number} */ + const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, parsedTargets, null, null) * timeScale; + /** @type {CompositeOperation} */ + const composite = /** @type {CompositeOperation} */(setValue(params.composition, 'replace')); - let _seed = 0; + for (let name in params) { + if (!isKey(name)) continue; + /** @type {PropertyIndexedKeyframes} */ + const keyframes = {}; + /** @type {KeyframeAnimationOptions} */ + const tweenParams = { iterations, direction, fill, easing, duration, delay, composite }; + const propertyValue = params[name]; + const individualTransformProperty = hasIndividualTransforms ? validTransforms.includes(name) ? name : shortTransforms.get(name) : false; - /** - * Creates a seeded pseudorandom number generator function - * - * @param {Number} [seed] - The seed value for the random number generator - * @param {Number} [seededMin=0] - The minimum default value (inclusive) of the returned function - * @param {Number} [seededMax=1] - The maximum default value (inclusive) of the returned function - * @param {Number} [seededDecimalLength=0] - Default number of decimal places to round to of the returned function - * @return {RandomNumberGenerator} A function to generate a random number between optional min and max (inclusive) and decimal precision - */ - const createSeededRandom = (seed, seededMin = 0, seededMax = 1, seededDecimalLength = 0) => { - let t = seed === undefined ? _seed++ : seed; - return (min = seededMin, max = seededMax, decimalLength = seededDecimalLength) => { - t += 0x6D2B79F5; - t = Math.imul(t ^ t >>> 15, t | 1); - t ^= t + Math.imul(t ^ t >>> 7, t | 61); - const m = 10 ** decimalLength; - return Math.floor(((((t ^ t >>> 14) >>> 0) / 4294967296) * (max - min + (1 / m)) + min) * m) / m; + const styleName = individualTransformProperty ? 'transform' : name; + if (!inlineStyles[styleName]) { + inlineStyles[styleName] = elStyle[styleName]; + } + + let parsedPropertyValue; + if (isObj(propertyValue)) { + const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue); + const tweenOptionsEase = setValue(tweenOptions.ease, easing); + const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase; + const to = /** @type {WAAPITweenOptions} */(tweenOptions).to; + const from = /** @type {WAAPITweenOptions} */(tweenOptions).from; + /** @type {Number} */ + tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, parsedTargets, null, null)) * timeScale; + /** @type {Number} */ + tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, parsedTargets, null, null) * timeScale; + /** @type {CompositeOperation} */ + tweenParams.composite = /** @type {CompositeOperation} */(setValue(tweenOptions.composition, composite)); + /** @type {String} */ + tweenParams.easing = parseWAAPIEasing(tweenOptionsEase); + parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); + if (individualTransformProperty) { + keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; + cachedTransforms[individualTransformProperty] = parsedPropertyValue; + } else { + keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); + } + addWAAPIAnimation(this, $el, name, keyframes, tweenParams); + if (!isUnd(from)) { + if (!individualTransformProperty) { + elStyle[name] = keyframes[name][0]; + } else { + const key = `--${individualTransformProperty}`; + elStyle.setProperty(key, keyframes[key][0]); + } + } + } else { + parsedPropertyValue = isArr(propertyValue) ? + propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, parsedTargets)) : + normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, parsedTargets); + if (individualTransformProperty) { + keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; + cachedTransforms[individualTransformProperty] = parsedPropertyValue; + } else { + keyframes[name] = parsedPropertyValue; + } + addWAAPIAnimation(this, $el, name, keyframes, tweenParams); + } + } + if (hasIndividualTransforms) { + let transforms = emptyString; + for (let t in cachedTransforms) { + transforms += `${transformsFragmentStrings[t]}var(--${t})) `; + } + elStyle.transform = transforms; + } + }); + + if (scroll) { + /** @type {ScrollObserver} */(this.autoplay).link(this); + } } - }; - /** - * Picks a random element from an array or a string - * - * @template T - * @param {String|Array} items - The array or string to pick from - * @return {String|T} A random element from the array or character from the string - */ - const randomPick = items => items[random(0, items.length - 1)]; + /** + * @callback forEachCallback + * @param {globalThis.Animation} animation + */ - /** - * Shuffles an array in-place using the Fisher-Yates algorithm - * Adapted from https://bost.ocks.org/mike/shuffle/ - * - * @param {Array} items - The array to shuffle (will be modified in-place) - * @return {Array} The same array reference, now shuffled + /** + * @param {forEachCallback|String} callback + * @return {this} + */ + forEach(callback) { + try { + const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; + this.animations.forEach(cb); + } catch {} return this; + } + + get speed() { + return this._speed; + } + + set speed(speed) { + this._speed = +speed; + this.forEach(anim => anim.playbackRate = speed); + } + + get currentTime() { + const controlAnimation = this.controlAnimation; + const timeScale = globals.timeScale; + return this.completed ? this.duration : controlAnimation ? +controlAnimation.currentTime * (timeScale === 1 ? 1 : timeScale) : 0; + } + + set currentTime(time) { + const t = time * (globals.timeScale === 1 ? 1 : K); + this.forEach(anim => { + // Make sure the animation playState is not 'paused' in order to properly trigger an onfinish callback. + // The "paused" play state supersedes the "finished" play state; if the animation is both paused and finished, the "paused" state is the one that will be reported. + // https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish_event + // This is not needed for persisting animations since they never finish. + if (!this.persist && t >= this.duration) anim.play(); + anim.currentTime = t; + }); + } + + get progress() { + return this.currentTime / this.duration; + } + + set progress(progress) { + this.forEach(anim => anim.currentTime = progress * this.duration || 0); + } + + resume() { + if (!this.paused) return this; + this.paused = false; + // TODO: Store the current time, and seek back to the last position + return this.forEach('play'); + } + + pause() { + if (this.paused) return this; + this.paused = true; + return this.forEach('pause'); + } + + alternate() { + this.reversed = !this.reversed; + this.forEach('reverse'); + if (this.paused) this.forEach('pause'); + return this; + } + + play() { + if (this.reversed) this.alternate(); + return this.resume(); + } + + reverse() { + if (!this.reversed) this.alternate(); + return this.resume(); + } + + /** + * @param {Number} time + * @param {Boolean} muteCallbacks + */ + seek(time, muteCallbacks = false) { + if (muteCallbacks) this.muteCallbacks = true; + if (time < this.duration) this.completed = false; + this.currentTime = time; + this.muteCallbacks = false; + if (this.paused) this.pause(); + return this; + } + + restart() { + this.completed = false; + return this.seek(0, true).resume(); + } + + commitStyles() { + return this.forEach('commitStyles'); + } + + complete() { + return this.seek(this.duration); + } + + cancel() { + this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise + this.commitStyles().forEach('cancel'); + this.animations.length = 0; // Needed to release all animations from memory + requestAnimationFrame(() => { + this.targets.forEach(($el) => { // Needed to avoid unecessary inline transorms + if ($el.style.transform === 'none') $el.style.removeProperty('transform'); + }); + }); + return this; + } + + revert() { + // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted, + // This means if you have multiple animations animating different transforms on the same target, + // reverting one of them will also override the transform property of the other animations. + // A better approach would be to store the original custom property values if they exist instead of the entire transform value, + // and update the CSS variables with the orignal value + this.cancel().targets.forEach(($el, i) => { + const targetStyle = $el.style; + const targetInlineStyles = this._inlineStyles[i]; + for (let name in targetInlineStyles) { + const originalInlinedValue = targetInlineStyles[name]; + if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) { + targetStyle.removeProperty(toLowerCase(name)); + } else { + $el.style[name] = originalInlinedValue; + } + } + // Remove style attribute if empty + if ($el.getAttribute('style') === emptyString) $el.removeAttribute('style'); + }); + return this; + } + + /** + * @typedef {this & {then: null}} ResolvedWAAPIAnimation + */ + + /** + * @param {Callback} [callback] + * @return Promise + */ + then(callback = noop) { + const then = this.then; + const onResolve = () => { + this.then = null; + callback(/** @type {ResolvedWAAPIAnimation} */(this)); + this.then = then; + this._resolve = noop; + }; + return new Promise(r => { + this._resolve = () => r(onResolve()); + if (this.completed) this._resolve(); + return this; + }); + } + } + + const waapi = { + /** + * @param {DOMTargetsParam} targets + * @param {WAAPIAnimationParams} params + * @return {WAAPIAnimation} */ - const shuffle = items => { - let m = items.length, t, i; - while (m) { i = random(0, --m); t = items[m]; items[m] = items[i]; items[i] = t; } - return items; + animate: (targets, params) => new WAAPIAnimation(targets, params), + convertEase: easingToLinear }; + + + + + + + + /** - * @overload - * @param {Number} val - * @param {StaggerParams} [params] - * @return {StaggerFunction} + * @typedef {DOMTargetSelector|Array} LayoutChildrenParam */ + /** - * @overload - * @param {String} val - * @param {StaggerParams} [params] - * @return {StaggerFunction} + * @typedef {Object} LayoutAnimationTimingsParams + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] */ + /** - * @overload - * @param {[Number, Number]} val - * @param {StaggerParams} [params] - * @return {StaggerFunction} + * @typedef {Record} LayoutStateAnimationProperties */ + /** - * @overload - * @param {[String, String]} val - * @param {StaggerParams} [params] - * @return {StaggerFunction} + * @typedef {LayoutStateAnimationProperties & LayoutAnimationTimingsParams} LayoutStateParams */ + /** - * @param {Number|String|[Number, Number]|[String, String]} val The staggered value or range - * @param {StaggerParams} [params] The stagger parameters - * @return {StaggerFunction} + * @typedef {Object} LayoutSpecificAnimationParams + * @property {Number|String} [id] + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] + * @property {EasingParam} [playbackEase] + * @property {LayoutStateParams} [swapAt] + * @property {LayoutStateParams} [enterFrom] + * @property {LayoutStateParams} [leaveTo] */ - const stagger = (val, params = {}) => { - let values = []; - let maxValue = 0; - const from = params.from; - const reversed = params.reversed; - const ease = params.ease; - const hasEasing = !isUnd(ease); - const hasSpring = hasEasing && !isUnd(/** @type {Spring} */(ease).ease); - const staggerEase = hasSpring ? /** @type {Spring} */(ease).ease : hasEasing ? parseEase(ease) : null; - const grid = params.grid; - const axis = params.axis; - const customTotal = params.total; - const fromFirst = isUnd(from) || from === 0 || from === 'first'; - const fromCenter = from === 'center'; - const fromLast = from === 'last'; - const fromRandom = from === 'random'; - const isRange = isArr(val); - const useProp = params.use; - const val1 = isRange ? parseNumber(val[0]) : parseNumber(val); - const val2 = isRange ? parseNumber(val[1]) : 0; - const unitMatch = unitsExecRgx.exec((isRange ? val[1] : val) + emptyString); - const start = params.start || 0 + (isRange ? val1 : 0); - let fromIndex = fromFirst ? 0 : isNum(from) ? from : 0; - return (target, i, t, tl) => { - const [ registeredTarget ] = registerTargets(target); - const total = isUnd(customTotal) ? t : customTotal; - const customIndex = !isUnd(useProp) ? isFnc(useProp) ? useProp(registeredTarget, i, total) : getOriginalAnimatableValue(registeredTarget, useProp) : false; - const staggerIndex = isNum(customIndex) || isStr(customIndex) && isNum(+customIndex) ? +customIndex : i; - if (fromCenter) fromIndex = (total - 1) / 2; - if (fromLast) fromIndex = total - 1; - if (!values.length) { - for (let index = 0; index < total; index++) { - if (!grid) { - values.push(abs(fromIndex - index)); - } else { - const fromX = !fromCenter ? fromIndex % grid[0] : (grid[0] - 1) / 2; - const fromY = !fromCenter ? floor(fromIndex / grid[0]) : (grid[1] - 1) / 2; - const toX = index % grid[0]; - const toY = floor(index / grid[0]); - const distanceX = fromX - toX; - const distanceY = fromY - toY; - let value = sqrt(distanceX * distanceX + distanceY * distanceY); - if (axis === 'x') value = -distanceX; - if (axis === 'y') value = -distanceY; - values.push(value); - } - maxValue = max(...values); - } - if (staggerEase) values = values.map(val => staggerEase(val / maxValue) * maxValue); - if (reversed) values = values.map(val => axis ? (val < 0) ? val * -1 : -val : abs(maxValue - val)); - if (fromRandom) values = shuffle(values); - } - const spacing = isRange ? (val2 - val1) / maxValue : val1; - const offset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); - /** @type {String|Number} */ - let output = offset + ((spacing * round$1(values[staggerIndex], 2)) || 0); - if (params.modifier) output = params.modifier(output); - if (unitMatch) output = `${output}${unitMatch[2]}`; - return output; - } - }; - var index$2 = /*#__PURE__*/Object.freeze({ - __proto__: null, - $: registerTargets, - clamp: clamp, - cleanInlineStyles: cleanInlineStyles, - createSeededRandom: createSeededRandom, - damp: damp, - degToRad: degToRad, - get: get, - keepTime: keepTime, - lerp: lerp, - mapRange: mapRange, - padEnd: padEnd, - padStart: padStart, - radToDeg: radToDeg, - random: random, - randomPick: randomPick, - remove: remove, - round: round, - roundPad: roundPad, - set: set, - shuffle: shuffle, - snap: snap, - stagger: stagger, - sync: sync, - wrap: wrap - }); + /** + * @typedef {LayoutSpecificAnimationParams & TimerParams & TickableCallbacks & RenderableCallbacks} LayoutAnimationParams + */ - + /** + * @typedef {Object} LayoutOptions + * @property {LayoutChildrenParam} [children] + * @property {Array} [properties] + */ /** - * @param {TargetsParam} path - * @return {SVGGeometryElement|void} + * @typedef {LayoutAnimationParams & LayoutOptions} AutoLayoutParams */ - const getPath = path => { - const parsedTargets = parseTargets(path); - const $parsedSvg = /** @type {SVGGeometryElement} */(parsedTargets[0]); - if (!$parsedSvg || !isSvg($parsedSvg)) return console.warn(`${path} is not a valid SVGGeometryElement`); - return $parsedSvg; - }; - + /** + * @typedef {Record & { + * transform: String, + * x: Number, + * y: Number, + * left: Number, + * top: Number, + * clientLeft: Number, + * clientTop: Number, + * width: Number, + * height: Number, + * }} LayoutNodeProperties + */ - // Motion path animation + /** + * @typedef {Object} LayoutNode + * @property {String} id + * @property {DOMTarget} $el + * @property {Number} index + * @property {Array} targets + * @property {Number} delay + * @property {Number} duration + * @property {EasingParam} ease + * @property {DOMTarget} $measure + * @property {LayoutSnapshot} state + * @property {AutoLayout} layout + * @property {LayoutNode|null} parentNode + * @property {Boolean} isTarget + * @property {Boolean} isEntering + * @property {Boolean} isLeaving + * @property {Boolean} hasTransform + * @property {Array} inlineStyles + * @property {String|null} inlineTransforms + * @property {String|null} inlineTransition + * @property {Boolean} branchAdded + * @property {Boolean} branchRemoved + * @property {Boolean} branchNotRendered + * @property {Boolean} sizeChanged + * @property {Boolean} isInlined + * @property {Boolean} hasVisibilitySwap + * @property {Boolean} hasDisplayNone + * @property {Boolean} hasVisibilityHidden + * @property {String|null} measuredInlineTransform + * @property {String|null} measuredInlineTransition + * @property {String|null} measuredDisplay + * @property {String|null} measuredVisibility + * @property {String|null} measuredPosition + * @property {Boolean} measuredHasDisplayNone + * @property {Boolean} measuredHasVisibilityHidden + * @property {Boolean} measuredIsVisible + * @property {Boolean} measuredIsRemoved + * @property {Boolean} measuredIsInsideRoot + * @property {LayoutNodeProperties} properties + * @property {LayoutNode|null} _head + * @property {LayoutNode|null} _tail + * @property {LayoutNode|null} _prev + * @property {LayoutNode|null} _next + */ + + /** + * @callback LayoutNodeIterator + * @param {LayoutNode} node + * @param {Number} index + * @return {void} + */ + + let layoutId = 0; + let nodeId = 0; /** - * @param {SVGGeometryElement} $path - * @param {Number} totalLength - * @param {Number} progress - * @param {Number} lookup - * @param {Boolean} shouldClamp - * @return {DOMPoint} + * @param {DOMTarget} root + * @param {DOMTarget} $el + * @return {Boolean} */ - const getPathPoint = ($path, totalLength, progress, lookup, shouldClamp) => { - const point = progress + lookup; - const pointOnPath = shouldClamp - ? Math.max(0, Math.min(point, totalLength)) // Clamp between 0 and totalLength - : (point % totalLength + totalLength) % totalLength; // Wrap around - return $path.getPointAtLength(pointOnPath); + const isElementInRoot = (root, $el) => { + if (!root || !$el) return false; + return root === $el || root.contains($el); }; /** - * @param {SVGGeometryElement} $path - * @param {String} pathProperty - * @param {Number} [offset=0] - * @return {FunctionValue} + * @param {DOMTarget|null} $el + * @return {String|null} */ - const getPathProgess = ($path, pathProperty, offset = 0) => { - return $el => { - const totalLength = +($path.getTotalLength()); - const inSvg = $el[isSvgSymbol]; - const ctm = $path.getCTM(); - const shouldClamp = offset === 0; - /** @type {TweenObjectValue} */ - return { - from: 0, - to: totalLength, - /** @type {TweenModifier} */ - modifier: progress => { - const offsetLength = offset * totalLength; - const newProgress = progress + offsetLength; - if (pathProperty === 'a') { - const p0 = getPathPoint($path, totalLength, newProgress, -1, shouldClamp); - const p1 = getPathPoint($path, totalLength, newProgress, 1, shouldClamp); - return atan2(p1.y - p0.y, p1.x - p0.x) * 180 / PI; - } else { - const p = getPathPoint($path, totalLength, newProgress, 0, shouldClamp); - return pathProperty === 'x' ? - inSvg || !ctm ? p.x : p.x * ctm.a + p.y * ctm.c + ctm.e : - inSvg || !ctm ? p.y : p.x * ctm.b + p.y * ctm.d + ctm.f - } - } - } - } + const muteElementTransition = $el => { + if (!$el) return null; + const style = $el.style; + const transition = style.transition || ''; + style.setProperty('transition', 'none', 'important'); + return transition; }; /** - * @param {TargetsParam} path - * @param {Number} [offset=0] + * @param {DOMTarget|null} $el + * @param {String|null} transition */ - const createMotionPath = (path, offset = 0) => { - const $path = getPath(path); - if (!$path) return; - return { - translateX: getPathProgess($path, 'x', offset), - translateY: getPathProgess($path, 'y', offset), - rotate: getPathProgess($path, 'a', offset), + const restoreElementTransition = ($el, transition) => { + if (!$el) return; + const style = $el.style; + if (transition) { + style.transition = transition; + } else { + style.removeProperty('transition'); } }; - + /** + * @param {LayoutNode} node + */ + const muteNodeTransition = node => { + const store = node.layout.transitionMuteStore; + const $el = node.$el; + const $measure = node.$measure; + if ($el && !store.has($el)) store.set($el, muteElementTransition($el)); + if ($measure && !store.has($measure)) store.set($measure, muteElementTransition($measure)); + }; /** - * @param {SVGGeometryElement} [$el] - * @return {Number} + * @param {Map} store */ - const getScaleFactor = $el => { - let scaleFactor = 1; - if ($el && $el.getCTM) { - const ctm = $el.getCTM(); - if (ctm) { - const scaleX = sqrt(ctm.a * ctm.a + ctm.b * ctm.b); - const scaleY = sqrt(ctm.c * ctm.c + ctm.d * ctm.d); - scaleFactor = (scaleX + scaleY) / 2; - } - } - return scaleFactor; + const restoreLayoutTransition = store => { + store.forEach((value, $el) => restoreElementTransition($el, value)); + store.clear(); }; + const hiddenComputedStyle = /** @type {CSSStyleDeclaration} */({ + display: 'none', + visibility: 'hidden', + opacity: '0', + transform: 'none', + position: 'static', + }); + /** - * Creates a proxy that wraps an SVGGeometryElement and adds drawing functionality. - * @param {SVGGeometryElement} $el - The SVG element to transform into a drawable - * @param {number} start - Starting position (0-1) - * @param {number} end - Ending position (0-1) - * @return {DrawableSVGGeometry} - Returns a proxy that preserves the original element's type with additional 'draw' attribute functionality + * @param {LayoutNode|null} node */ - const createDrawableProxy = ($el, start, end) => { - const pathLength = K; - const computedStyles = getComputedStyle($el); - const strokeLineCap = computedStyles.strokeLinecap; - // @ts-ignore - const $scalled = computedStyles.vectorEffect === 'non-scaling-stroke' ? $el : null; - let currentCap = strokeLineCap; + const detachNode = node => { + if (!node) return; + const parent = node.parentNode; + if (!parent) return; + if (parent._head === node) parent._head = node._next; + if (parent._tail === node) parent._tail = node._prev; + if (node._prev) node._prev._next = node._next; + if (node._next) node._next._prev = node._prev; + node._prev = null; + node._next = null; + node.parentNode = null; + }; - const proxy = new Proxy($el, { - get(target, property) { - const value = target[property]; - if (property === proxyTargetSymbol) return target; - if (property === 'setAttribute') { - return (...args) => { - if (args[0] === 'draw') { - const value = args[1]; - const values = value.split(' '); - const v1 = +values[0]; - const v2 = +values[1]; - // TOTO: Benchmark if performing two slices is more performant than one split - // const spaceIndex = value.indexOf(' '); - // const v1 = round(+value.slice(0, spaceIndex), precision); - // const v2 = round(+value.slice(spaceIndex + 1), precision); - const scaleFactor = getScaleFactor($scalled); - const os = v1 * -pathLength * scaleFactor; - const d1 = (v2 * pathLength * scaleFactor) + os; - const d2 = (pathLength * scaleFactor + - ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10 * scaleFactor) - d1); - if (strokeLineCap !== 'butt') { - const newCap = v1 === v2 ? 'butt' : strokeLineCap; - if (currentCap !== newCap) { - target.style.strokeLinecap = `${newCap}`; - currentCap = newCap; - } - } - target.setAttribute('stroke-dashoffset', `${os}`); - target.setAttribute('stroke-dasharray', `${d1} ${d2}`); - } - return Reflect.apply(value, target, args); - }; - } + /** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @param {LayoutSnapshot} state + * @param {LayoutNode} recycledNode + * @return {LayoutNode} + */ + const createNode = ($el, parentNode, state, recycledNode) => { + let dataId = $el.dataset.layoutId; + if (!dataId) dataId = $el.dataset.layoutId = `node-${nodeId++}`; + const node = recycledNode ? recycledNode : /** @type {LayoutNode} */({}); + node.$el = $el; + node.$measure = $el; + node.id = dataId; + node.index = 0; + node.targets = null; + node.delay = 0; + node.duration = 0; + node.ease = null; + node.state = state; + node.layout = state.layout; + node.parentNode = parentNode || null; + node.isTarget = false; + node.isEntering = false; + node.isLeaving = false; + node.isInlined = false; + node.hasTransform = false; + node.inlineStyles = []; + node.inlineTransforms = null; + node.inlineTransition = null; + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.sizeChanged = false; + node.hasVisibilitySwap = false; + node.hasDisplayNone = false; + node.hasVisibilityHidden = false; + node.measuredInlineTransform = null; + node.measuredInlineTransition = null; + node.measuredDisplay = null; + node.measuredVisibility = null; + node.measuredPosition = null; + node.measuredHasDisplayNone = false; + node.measuredHasVisibilityHidden = false; + node.measuredIsVisible = false; + node.measuredIsRemoved = false; + node.measuredIsInsideRoot = false; + node.properties = /** @type {LayoutNodeProperties} */({ + transform: 'none', + x: 0, + y: 0, + left: 0, + top: 0, + clientLeft: 0, + clientTop: 0, + width: 0, + height: 0, + }); + node.layout.properties.forEach(prop => node.properties[prop] = 0); + node._head = null; + node._tail = null; + node._prev = null; + node._next = null; + return node; + }; - if (isFnc(value)) { - return (...args) => Reflect.apply(value, target, args); + /** + * @param {LayoutNode} node + * @param {DOMTarget} $measure + * @param {CSSStyleDeclaration} computedStyle + * @param {Boolean} skipMeasurements + * @return {LayoutNode} + */ + const recordNodeState = (node, $measure, computedStyle, skipMeasurements) => { + const $el = node.$el; + const root = node.layout.root; + const isRoot = root === $el; + const properties = node.properties; + const rootNode = node.state.rootNode; + const parentNode = node.parentNode; + const computedTransforms = computedStyle.transform; + const inlineTransforms = $el.style.transform; + const parentNotRendered = parentNode ? parentNode.measuredIsRemoved : false; + const position = computedStyle.position; + if (isRoot) node.layout.absoluteCoords = position === 'fixed' || position === 'absolute'; + node.$measure = $measure; + node.inlineTransforms = inlineTransforms; + node.hasTransform = computedTransforms && computedTransforms !== 'none'; + node.measuredIsInsideRoot = isElementInRoot(root, $measure); + node.measuredInlineTransform = null; + node.measuredDisplay = computedStyle.display; + node.measuredVisibility = computedStyle.visibility; + node.measuredPosition = position; + node.measuredHasDisplayNone = computedStyle.display === 'none'; + node.measuredHasVisibilityHidden = computedStyle.visibility === 'hidden'; + node.measuredIsVisible = !(node.measuredHasDisplayNone || node.measuredHasVisibilityHidden); + node.measuredIsRemoved = node.measuredHasDisplayNone || node.measuredHasVisibilityHidden || parentNotRendered; + // Check if element has adjacent text that would reflow when taken out of flow + let hasAdjacentText = false; + let s = $el.previousSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.previousSibling; + if (s && s.nodeType === Node.TEXT_NODE) { + hasAdjacentText = true; + } else { + s = $el.nextSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.nextSibling; + hasAdjacentText = s !== null && s.nodeType === Node.TEXT_NODE; + } + node.isInlined = hasAdjacentText; + + // Mute transforms (and transition to avoid triggering an animation) before the position calculation + if (node.hasTransform && !skipMeasurements) { + const transitionMuteStore = node.layout.transitionMuteStore; + if (!transitionMuteStore.get($el)) node.inlineTransition = muteElementTransition($el); + if ($measure === $el) { + $el.style.transform = 'none'; + } else { + if (!transitionMuteStore.get($measure)) node.measuredInlineTransition = muteElementTransition($measure); + node.measuredInlineTransform = $measure.style.transform; + $measure.style.transform = 'none'; + } + } + + let left = 0; + let top = 0; + let width = 0; + let height = 0; + + if (!skipMeasurements) { + const rect = $measure.getBoundingClientRect(); + left = rect.left; + top = rect.top; + width = rect.width; + height = rect.height; + } + + for (let name in properties) { + const computedProp = name === 'transform' ? computedTransforms : computedStyle[name] || (computedStyle.getPropertyValue && computedStyle.getPropertyValue(name)); + if (!isUnd(computedProp)) properties[name] = computedProp; + } + + properties.left = left; + properties.top = top; + properties.clientLeft = skipMeasurements ? 0 : $measure.clientLeft; + properties.clientTop = skipMeasurements ? 0 : $measure.clientTop; + // Compute local x/y relative to parent + let absoluteLeft, absoluteTop; + if (isRoot) { + if (!node.layout.absoluteCoords) { + absoluteLeft = 0; + absoluteTop = 0; + } else { + absoluteLeft = left; + absoluteTop = top; + } + } else { + const p = parentNode || rootNode; + const parentLeft = p.properties.left; + const parentTop = p.properties.top; + const borderLeft = p.properties.clientLeft; + const borderTop = p.properties.clientTop; + if (!node.layout.absoluteCoords) { + if (p === rootNode) { + const rootLeft = rootNode.properties.left; + const rootTop = rootNode.properties.top; + const rootBorderLeft = rootNode.properties.clientLeft; + const rootBorderTop = rootNode.properties.clientTop; + absoluteLeft = left - rootLeft - rootBorderLeft; + absoluteTop = top - rootTop - rootBorderTop; } else { - return value; + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; } + } else { + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; } - }); + } + properties.x = absoluteLeft; + properties.y = absoluteTop; + properties.width = width; + properties.height = height; + return node; + }; - if ($el.getAttribute('pathLength') !== `${pathLength}`) { - $el.setAttribute('pathLength', `${pathLength}`); - proxy.setAttribute('draw', `${start} ${end}`); + /** + * @param {LayoutNode} node + * @param {LayoutStateAnimationProperties} [props] + */ + const updateNodeProperties = (node, props) => { + if (!props) return; + for (let name in props) { + node.properties[name] = props[name]; } + }; - return /** @type {DrawableSVGGeometry} */(proxy); + /** + * @param {LayoutNode} node + * @param {LayoutAnimationTimingsParams} params + */ + const updateNodeTimingParams = (node, params) => { + const easeFunctionResult = getFunctionValue(params.ease, node.$el, node.index, node.targets, null, null); + const keyEasing = isFnc(easeFunctionResult) ? easeFunctionResult : params.ease; + const hasSpring = !isUnd(keyEasing) && !isUnd(/** @type {Spring} */(keyEasing).ease); + node.ease = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; + node.duration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(params.duration, node.$el, node.index, node.targets, null, null); + node.delay = getFunctionValue(params.delay, node.$el, node.index, node.targets, null, null); }; /** - * Creates drawable proxies for multiple SVG elements. - * @param {TargetsParam} selector - CSS selector, SVG element, or array of elements and selectors - * @param {number} [start=0] - Starting position (0-1) - * @param {number} [end=0] - Ending position (0-1) - * @return {Array} - Array of proxied elements with drawing functionality + * @param {LayoutNode} node */ - const createDrawable = (selector, start = 0, end = 0) => { - const els = parseTargets(selector); - return els.map($el => createDrawableProxy( - /** @type {SVGGeometryElement} */($el), - start, - end - )); + const recordNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + stylesStore.length = 0; + node.layout.recordedProperties.forEach(prop => { + stylesStore.push(prop, style[prop] || ''); + }); }; - + /** + * @param {LayoutNode} node + */ + const restoreNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + for (let i = 0, l = stylesStore.length; i < l; i += 2) { + const property = stylesStore[i]; + const styleValue = stylesStore[i + 1]; + if (styleValue && styleValue !== '') { + style[property] = styleValue; + } else { + style[property] = ''; + style.removeProperty(property); + } + } + }; /** - * @param {TargetsParam} path2 - * @param {Number} [precision] - * @return {FunctionValue} + * @param {LayoutNode} node */ - const morphTo = (path2, precision = .33) => ($path1) => { - const tagName1 = ($path1.tagName || '').toLowerCase(); - if (!tagName1.match(/^(path|polygon|polyline)$/)) { - throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use , or .`); + const restoreNodeTransform = node => { + const inlineTransforms = node.inlineTransforms; + const nodeStyle = node.$el.style; + if (!node.hasTransform || !inlineTransforms || (node.hasTransform && nodeStyle.transform === 'none') || (inlineTransforms && inlineTransforms === 'none')) { + nodeStyle.removeProperty('transform'); + } else if (inlineTransforms) { + nodeStyle.transform = inlineTransforms; } - const $path2 = /** @type {SVGGeometryElement} */(getPath(path2)); - if (!$path2) { - throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing , or SVG element."); + const $measure = node.$measure; + if (node.hasTransform && $measure !== node.$el) { + const measuredStyle = $measure.style; + const measuredInline = node.measuredInlineTransform; + if (measuredInline && measuredInline !== '') { + measuredStyle.transform = measuredInline; + } else { + measuredStyle.removeProperty('transform'); + } } - const tagName2 = ($path2.tagName || '').toLowerCase(); - if (!tagName2.match(/^(path|polygon|polyline)$/)) { - throw new Error(`Can't morph a <${$path2.tagName}> SVG element. Use , or .`); + node.measuredInlineTransform = null; + if (node.inlineTransition !== null) { + restoreElementTransition(node.$el, node.inlineTransition); + node.inlineTransition = null; } - const isPath = $path1.tagName === 'path'; - const separator = isPath ? ' ' : ','; - const previousPoints = $path1[morphPointsSymbol]; - if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints); - - let v1 = '', v2 = ''; + if ($measure !== node.$el && node.measuredInlineTransition !== null) { + restoreElementTransition($measure, node.measuredInlineTransition); + node.measuredInlineTransition = null; + } + }; - if (!precision) { - v1 = $path1.getAttribute(isPath ? 'd' : 'points'); - v2 = $path2.getAttribute(isPath ? 'd' : 'points'); - } else { - const length1 = /** @type {SVGGeometryElement} */($path1).getTotalLength(); - const length2 = $path2.getTotalLength(); - const maxPoints = Math.max(Math.ceil(length1 * precision), Math.ceil(length2 * precision)); - for (let i = 0; i < maxPoints; i++) { - const t = i / (maxPoints - 1); - const pointOnPath1 = /** @type {SVGGeometryElement} */($path1).getPointAtLength(length1 * t); - const pointOnPath2 = $path2.getPointAtLength(length2 * t); - const prefix = isPath ? (i === 0 ? 'M' : 'L') : ''; - v1 += prefix + round$1(pointOnPath1.x, 3) + separator + pointOnPath1.y + ' '; - v2 += prefix + round$1(pointOnPath2.x, 3) + separator + pointOnPath2.y + ' '; + /** + * @param {LayoutNode} node + */ + const restoreNodeVisualState = node => { + if (node.measuredIsRemoved || node.hasVisibilitySwap) { + node.$el.style.removeProperty('display'); + node.$el.style.removeProperty('visibility'); + if (node.hasVisibilitySwap) { + node.$measure.style.removeProperty('display'); + node.$measure.style.removeProperty('visibility'); } } - - $path1[morphPointsSymbol] = v2; - - return [v1, v2]; + // if (node.measuredIsRemoved) { + node.layout.pendingRemoval.delete(node.$el); + // } }; - var index$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - createDrawable: createDrawable, - createMotionPath: createMotionPath, - morphTo: morphTo - }); - - + /** + * @param {LayoutNode} node + * @param {LayoutNode} targetNode + * @param {LayoutSnapshot} newState + * @return {LayoutNode} + */ + const cloneNodeProperties = (node, targetNode, newState) => { + targetNode.properties = /** @type {LayoutNodeProperties} */({ ...node.properties }); + targetNode.state = newState; + targetNode.isTarget = node.isTarget; + targetNode.hasTransform = node.hasTransform; + targetNode.inlineTransforms = node.inlineTransforms; + targetNode.measuredIsVisible = node.measuredIsVisible; + targetNode.measuredDisplay = node.measuredDisplay; + targetNode.measuredIsRemoved = node.measuredIsRemoved; + targetNode.measuredHasDisplayNone = node.measuredHasDisplayNone; + targetNode.measuredHasVisibilityHidden = node.measuredHasVisibilityHidden; + targetNode.hasDisplayNone = node.hasDisplayNone; + targetNode.isInlined = node.isInlined; + targetNode.hasVisibilityHidden = node.hasVisibilityHidden; + return targetNode; + }; - const segmenter = (typeof Intl !== 'undefined') && Intl.Segmenter; - const valueRgx = /\{value\}/g; - const indexRgx = /\{i\}/g; - const whiteSpaceGroupRgx = /(\s+)/; - const whiteSpaceRgx = /^\s+$/; - const lineType = 'line'; - const wordType = 'word'; - const charType = 'char'; - const dataLine = `data-line`; - - /** - * @typedef {Object} Segment - * @property {String} segment - * @property {Boolean} [isWordLike] - */ - - /** - * @typedef {Object} Segmenter - * @property {function(String): Iterable} segment - */ - - /** @type {Segmenter} */ - let wordSegmenter = null; - /** @type {Segmenter} */ - let graphemeSegmenter = null; - let $splitTemplate = null; - - /** - * @param {Segment} seg - * @return {Boolean} - */ - const isSegmentWordLike = seg => { - return seg.isWordLike || - seg.segment === ' ' || // Consider spaces as words first, then handle them diffrently later - isNum(+seg.segment); // Safari doesn't considers numbers as words - }; - - /** - * @param {HTMLElement} $el - */ - const setAriaHidden = $el => $el.setAttribute('aria-hidden', 'true'); - - /** - * @param {DOMTarget} $el - * @param {String} type - * @return {Array} - */ - const getAllTopLevelElements = ($el, type) => [.../** @type {*} */($el.querySelectorAll(`[data-${type}]:not([data-${type}] [data-${type}])`))]; - - const debugColors = { line: '#00D672', word: '#FF4B4B', char: '#5A87FF' }; - - /** - * @param {HTMLElement} $el - */ - const filterEmptyElements = $el => { - if (!$el.childElementCount && !$el.textContent.trim()) { - const $parent = $el.parentElement; - $el.remove(); - if ($parent) filterEmptyElements($parent); + class LayoutSnapshot { + /** + * @param {AutoLayout} layout + */ + constructor(layout) { + /** @type {AutoLayout} */ + this.layout = layout; + /** @type {LayoutNode|null} */ + this.rootNode = null; + /** @type {Set} */ + this.rootNodes = new Set(); + /** @type {Map} */ + this.nodes = new Map(); + /** @type {Number} */ + this.scrollX = 0; + /** @type {Number} */ + this.scrollY = 0; } - }; - /** - * @param {HTMLElement} $el - * @param {Number} lineIndex - * @param {Set} bin - * @returns {Set} - */ - const filterLineElements = ($el, lineIndex, bin) => { - const dataLineAttr = $el.getAttribute(dataLine); - if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') bin.add($el); - let i = $el.childElementCount; - while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin); - return bin; - }; + /** + * @return {this} + */ + revert() { + this.forEachNode(node => { + this.layout.pendingRemoval.delete(node.$el); + node.$el.removeAttribute('data-layout-id'); + node.$measure.removeAttribute('data-layout-id'); + }); + this.rootNode = null; + this.rootNodes.clear(); + this.nodes.clear(); + return this; + } - /** - * @param {'line'|'word'|'char'} type - * @param {SplitTemplateParams} params - * @return {String} - */ - const generateTemplate = (type, params = {}) => { - let template = ``; - const classString = isStr(params.class) ? ` class="${params.class}"` : ''; - const cloneType = setValue(params.clone, false); - const wrapType = setValue(params.wrap, false); - const overflow = wrapType ? wrapType === true ? 'clip' : wrapType : cloneType ? 'clip' : false; - if (wrapType) template += ``; - template += ``; - if (cloneType) { - const left = cloneType === 'left' ? '-100%' : cloneType === 'right' ? '100%' : '0'; - const top = cloneType === 'top' ? '-100%' : cloneType === 'bottom' ? '100%' : '0'; - template += `{value}`; - template += `{value}`; - } else { - template += `{value}`; + /** + * @param {DOMTarget} $el + * @return {LayoutNode} + */ + getNode($el) { + if (!$el || !$el.dataset) return; + return this.nodes.get($el.dataset.layoutId); } - template += ``; - if (wrapType) template += ``; - return template; - }; - /** - * @param {String|SplitFunctionValue} htmlTemplate - * @param {Array} store - * @param {Node|HTMLElement} node - * @param {DocumentFragment} $parentFragment - * @param {'line'|'word'|'char'} type - * @param {Boolean} debug - * @param {Number} lineIndex - * @param {Number} [wordIndex] - * @param {Number} [charIndex] - * @return {HTMLElement} - */ - const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, debug, lineIndex, wordIndex, charIndex) => { - const isLine = type === lineType; - const isChar = type === charType; - const className = `_${type}_`; - const template = isFnc(htmlTemplate) ? htmlTemplate(node) : htmlTemplate; - const displayStyle = isLine ? 'block' : 'inline-block'; - $splitTemplate.innerHTML = template - .replace(valueRgx, ``) - .replace(indexRgx, `${isChar ? charIndex : isLine ? lineIndex : wordIndex}`); - const $content = $splitTemplate.content; - const $highestParent = /** @type {HTMLElement} */($content.firstElementChild); - const $split = /** @type {HTMLElement} */($content.querySelector(`[data-${type}]`)) || $highestParent; - const $replacables = /** @type {NodeListOf} */($content.querySelectorAll(`i.${className}`)); - const replacablesLength = $replacables.length; - if (replacablesLength) { - $highestParent.style.display = displayStyle; - $split.style.display = displayStyle; - $split.setAttribute(dataLine, `${lineIndex}`); - if (!isLine) { - $split.setAttribute('data-word', `${wordIndex}`); - if (isChar) $split.setAttribute('data-char', `${charIndex}`); - } - let i = replacablesLength; - while (i--) { - const $replace = $replacables[i]; - const $closestParent = $replace.parentElement; - $closestParent.style.display = displayStyle; - if (isLine) { - $closestParent.innerHTML = /** @type {HTMLElement} */(node).innerHTML; - } else { - $closestParent.replaceChild(node.cloneNode(true), $replace); - } - } - store.push($split); - $parentFragment.appendChild($content); - } else { - console.warn(`The expression "{value}" is missing from the provided template.`); + /** + * @param {DOMTarget} $el + * @param {String} prop + * @return {Number|String} + */ + getComputedValue($el, prop) { + const node = this.getNode($el); + if (!node) return; + return /** @type {Number|String} */(node.properties[prop]); } - if (debug) $highestParent.style.outline = `1px dotted ${debugColors[type]}`; - return $highestParent; - }; - /** - * A class that splits text into words and wraps them in span elements while preserving the original HTML structure. - * @class - */ - class TextSplitter { /** - * @param {HTMLElement|NodeList|String|Array} target - * @param {TextSplitterParams} [parameters] + * @param {LayoutNode|null} rootNode + * @param {LayoutNodeIterator} cb */ - constructor(target, parameters = {}) { - // Only init segmenters when needed - if (!wordSegmenter) wordSegmenter = segmenter ? new segmenter([], { granularity: wordType }) : { - segment: (text) => { - const segments = []; - const words = text.split(whiteSpaceGroupRgx); - for (let i = 0, l = words.length; i < l; i++) { - const segment = words[i]; - segments.push({ - segment, - isWordLike: !whiteSpaceRgx.test(segment), // Consider non-whitespace as word-like - }); + forEach(rootNode, cb) { + let node = rootNode; + let i = 0; + while (node) { + cb(node, i++); + if (node._head) { + node = node._head; + } else if (node._next) { + node = node._next; + } else { + while (node && !node._next) { + node = node.parentNode; } - return segments; + if (node) node = node._next; } - }; - if (!graphemeSegmenter) graphemeSegmenter = segmenter ? new segmenter([], { granularity: 'grapheme' }) : { - segment: text => [...text].map(char => ({ segment: char })) - }; - if (!$splitTemplate && isBrowser) $splitTemplate = doc.createElement('template'); - if (scope.current) scope.current.register(this); - const { words, chars, lines, accessible, includeSpaces, debug } = parameters; - const $target = /** @type {HTMLElement} */((target = isArr(target) ? target[0] : target) && /** @type {Node} */(target).nodeType ? target : (getNodeList(target) || [])[0]); - const lineParams = lines === true ? {} : lines; - const wordParams = words === true || isUnd(words) ? {} : words; - const charParams = chars === true ? {} : chars; - this.debug = setValue(debug, false); - this.includeSpaces = setValue(includeSpaces, false); - this.accessible = setValue(accessible, true); - this.linesOnly = lineParams && (!wordParams && !charParams); - /** @type {String|false|SplitFunctionValue} */ - this.lineTemplate = isObj(lineParams) ? generateTemplate(lineType, /** @type {SplitTemplateParams} */(lineParams)) : lineParams; - /** @type {String|false|SplitFunctionValue} */ - this.wordTemplate = isObj(wordParams) || this.linesOnly ? generateTemplate(wordType, /** @type {SplitTemplateParams} */(wordParams)) : wordParams; - /** @type {String|false|SplitFunctionValue} */ - this.charTemplate = isObj(charParams) ? generateTemplate(charType, /** @type {SplitTemplateParams} */(charParams)) : charParams; - this.$target = $target; - this.html = $target && $target.innerHTML; - this.lines = []; - this.words = []; - this.chars = []; - this.effects = []; - this.effectsCleanups = []; - this.cache = null; - this.ready = false; - this.width = 0; - this.resizeTimeout = null; - const handleSplit = () => this.html && (lineParams || wordParams || charParams) && this.split(); - // Make sure this is declared before calling handleSplit() in case revert() is called inside an effect callback - this.resizeObserver = new ResizeObserver(() => { - // Use a setTimeout instead of a Timer for better tree shaking - clearTimeout(this.resizeTimeout); - this.resizeTimeout = setTimeout(() => { - const currentWidth = /** @type {HTMLElement} */($target).offsetWidth; - if (currentWidth === this.width) return; - this.width = currentWidth; - handleSplit(); - }, 150); - }); - // Only declare the font ready promise when splitting by lines and not alreay split - if (this.lineTemplate && !this.ready) { - doc.fonts.ready.then(handleSplit); - } else { - handleSplit(); } - $target ? this.resizeObserver.observe($target) : console.warn('No Text Splitter target found.'); } /** - * @param {(...args: any[]) => Tickable | (() => void)} effect - * @return this + * @param {LayoutNodeIterator} cb */ - addEffect(effect) { - if (!isFnc(effect)) return console.warn('Effect must return a function.'); - const refreshableEffect = keepTime(effect); - this.effects.push(refreshableEffect); - if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this); - return this; + forEachRootNode(cb) { + this.forEach(this.rootNode, cb); } - revert() { - clearTimeout(this.resizeTimeout); - this.lines.length = this.words.length = this.chars.length = 0; - this.resizeObserver.disconnect(); - // Make sure to revert the effects after disconnecting the resizeObserver to avoid triggering it in the process - this.effectsCleanups.forEach(cleanup => isFnc(cleanup) ? cleanup(this) : cleanup.revert && cleanup.revert()); - this.$target.innerHTML = this.html; - return this; + /** + * @param {LayoutNodeIterator} cb + */ + forEachNode(cb) { + for (const rootNode of this.rootNodes) { + this.forEach(rootNode, cb); + } } /** - * Recursively processes a node and its children - * @param {Node} node + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @return {LayoutNode|null} */ - splitNode(node) { - const wordTemplate = this.wordTemplate; - const charTemplate = this.charTemplate; - const includeSpaces = this.includeSpaces; - const debug = this.debug; - const nodeType = node.nodeType; - if (nodeType === 3) { - const nodeText = node.nodeValue; - // If the nodeText is only whitespace, leave it as is - if (nodeText.trim()) { - const tempWords = []; - const words = this.words; - const chars = this.chars; - const wordSegments = wordSegmenter.segment(nodeText); - const $wordsFragment = doc.createDocumentFragment(); - let prevSeg = null; - for (const wordSegment of wordSegments) { - const segment = wordSegment.segment; - const isWordLike = isSegmentWordLike(wordSegment); - // Determine if this segment should be a new word, first segment always becomes a new word - if (!prevSeg || (isWordLike && (prevSeg && (isSegmentWordLike(prevSeg))))) { - tempWords.push(segment); - } else { - // Only concatenate if both current and previous are non-word-like and don't contain spaces - const lastWordIndex = tempWords.length - 1; - const lastWord = tempWords[lastWordIndex]; - if (!lastWord.includes(' ') && !segment.includes(' ')) { - tempWords[lastWordIndex] += segment; - } else { - tempWords.push(segment); - } + registerElement($el, parentNode) { + if (!$el || $el.nodeType !== 1) return null; + + if (!this.layout.transitionMuteStore.has($el)) this.layout.transitionMuteStore.set($el, muteElementTransition($el)); + + /** @type {Array} */ + const stack = [$el, parentNode]; + const root = this.layout.root; + let firstNode = null; + + while (stack.length) { + /** @type {LayoutNode|null} */ + const $parent = /** @type {LayoutNode|null} */(stack.pop()); + /** @type {DOMTarget|null} */ + const $current = /** @type {DOMTarget|null} */(stack.pop()); + + if (!$current || $current.nodeType !== 1 || isSvg($current)) continue; + + const skipMeasurements = $parent ? $parent.measuredIsRemoved : false; + const computedStyle = skipMeasurements ? hiddenComputedStyle : getComputedStyle($current); + const hasDisplayNone = skipMeasurements ? true : computedStyle.display === 'none'; + const hasVisibilityHidden = skipMeasurements ? true : computedStyle.visibility === 'hidden'; + const isVisible = !hasDisplayNone && !hasVisibilityHidden; + const existingId = $current.dataset.layoutId; + const isInsideRoot = isElementInRoot(root, $current); + + let node = existingId ? this.nodes.get(existingId) : null; + + if (node && node.$el !== $current) { + const nodeInsideRoot = isElementInRoot(root, node.$el); + const measuredVisible = node.measuredIsVisible; + const shouldReassignNode = !nodeInsideRoot && (isInsideRoot || (!isInsideRoot && !measuredVisible && isVisible)); + const shouldReuseMeasurements = nodeInsideRoot && !measuredVisible && isVisible; + // Rebind nodes that move into the root or whose detached twin just became visible + if (shouldReassignNode) { + detachNode(node); + node = createNode($current, $parent, this, node); + // for hidden element with in-root sibling, keep the hidden node but borrow measurements from its visible in-root twin element + } else if (shouldReuseMeasurements) { + recordNodeState(node, $current, computedStyle, skipMeasurements); + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; } - prevSeg = wordSegment; + if (!firstNode) firstNode = node; + continue; + // No reassignment needed so keep walking descendants under the current parent + } else { + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), $parent); + $child = $child.previousElementSibling; + } + if (!firstNode) firstNode = node; + continue; } + } else { + node = createNode($current, $parent, this, node); + } - for (let i = 0, l = tempWords.length; i < l; i++) { - const word = tempWords[i]; - if (!word.trim()) { - // Preserve whitespace only if includeSpaces is false and if the current space is not the first node - if (i && includeSpaces) continue; - $wordsFragment.appendChild(doc.createTextNode(word)); - } else { - const nextWord = tempWords[i + 1]; - const hasWordFollowingSpace = includeSpaces && nextWord && !nextWord.trim(); - const wordToProcess = word; - const charSegments = charTemplate ? graphemeSegmenter.segment(wordToProcess) : null; - const $charsFragment = charTemplate ? doc.createDocumentFragment() : doc.createTextNode(hasWordFollowingSpace ? word + '\xa0' : word); - if (charTemplate) { - const charSegmentsArray = [...charSegments]; - for (let j = 0, jl = charSegmentsArray.length; j < jl; j++) { - const charSegment = charSegmentsArray[j]; - const isLastChar = j === jl - 1; - // If this is the last character and includeSpaces is true with a following space, append the space - const charText = isLastChar && hasWordFollowingSpace ? charSegment.segment + '\xa0' : charSegment.segment; - const $charNode = doc.createTextNode(charText); - processHTMLTemplate(charTemplate, chars, $charNode, /** @type {DocumentFragment} */($charsFragment), charType, debug, -1, words.length, chars.length); - } - } - if (wordTemplate) { - processHTMLTemplate(wordTemplate, words, $charsFragment, $wordsFragment, wordType, debug, -1, words.length, chars.length); - // Chars elements must be re-parsed in the split() method if both words and chars are parsed - } else if (charTemplate) { - $wordsFragment.appendChild($charsFragment); - } else { - $wordsFragment.appendChild(doc.createTextNode(word)); - } - // Skip the next iteration if we included a space - if (hasWordFollowingSpace) i++; - } + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.isTarget = false; + node.sizeChanged = false; + node.hasVisibilityHidden = hasVisibilityHidden; + node.hasDisplayNone = hasDisplayNone; + node.hasVisibilitySwap = (hasVisibilityHidden && !node.measuredHasVisibilityHidden) || (hasDisplayNone && !node.measuredHasDisplayNone); + + this.nodes.set(node.id, node); + + node.parentNode = $parent || null; + node._prev = null; + node._next = null; + + if ($parent) { + this.rootNodes.delete(node); + if (!$parent._head) { + $parent._head = node; + $parent._tail = node; + } else { + $parent._tail._next = node; + node._prev = $parent._tail; + $parent._tail = node; } - node.parentNode.replaceChild($wordsFragment, node); + } else { + // Each disconnected subtree becomes its own root in the snapshot graph + this.rootNodes.add(node); } - } else if (nodeType === 1) { - // Converting to an array is necessary to work around childNodes pottential mutation - const childNodes = /** @type {Array} */([.../** @type {*} */(node.childNodes)]); - for (let i = 0, l = childNodes.length; i < l; i++) this.splitNode(childNodes[i]); + + recordNodeState(node, node.$el, computedStyle, skipMeasurements); + + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; + } + + if (!firstNode) firstNode = node; } + + return firstNode; } /** - * @param {Boolean} clearCache - * @return {this} + * @param {DOMTarget} $el + * @param {Set} candidates + * @return {LayoutNode|null} */ - split(clearCache = false) { - const $el = this.$target; - const isCached = !!this.cache && !clearCache; - const lineTemplate = this.lineTemplate; - const wordTemplate = this.wordTemplate; - const charTemplate = this.charTemplate; - const fontsReady = doc.fonts.status !== 'loading'; - const canSplitLines = lineTemplate && fontsReady; - this.ready = !lineTemplate || fontsReady; - if (canSplitLines || clearCache) { - // No need to revert effects animations here since it's already taken care by the refreshable - this.effectsCleanups.forEach(cleanup => isFnc(cleanup) && cleanup(this)); - } - if (!isCached) { - if (clearCache) { - $el.innerHTML = this.html; - this.words.length = this.chars.length = 0; + ensureDetachedNode($el, candidates) { + if (!$el || $el === this.layout.root) return null; + const existingId = $el.dataset.layoutId; + const existingNode = existingId ? this.nodes.get(existingId) : null; + if (existingNode && existingNode.$el === $el) return existingNode; + let parentNode = null; + let $ancestor = $el.parentElement; + while ($ancestor && $ancestor !== this.layout.root) { + if (candidates.has($ancestor)) { + parentNode = this.ensureDetachedNode($ancestor, candidates); + break; } - this.splitNode($el); - this.cache = $el.innerHTML; - } - if (canSplitLines) { - if (isCached) $el.innerHTML = this.cache; - this.lines.length = 0; - if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); + $ancestor = $ancestor.parentElement; } - // Always reparse characters after a line reset or if both words and chars are activated - if (charTemplate && (canSplitLines || wordTemplate)) { - this.chars = getAllTopLevelElements($el, charType); + return this.registerElement($el, parentNode); + } + + /** + * @return {this} + */ + record() { + const layout = this.layout; + const children = layout.children; + const root = layout.root; + const toParse = isArr(children) ? children : [children]; + const scoped = []; + const scopeRoot = children === '*' ? root : scope.root; + + // Mute transition and transforms of root ancestors before recording the state + + /** @type {Array} */ + const rootAncestorTransformStore = []; + let $ancestor = root.parentElement; + while ($ancestor && $ancestor.nodeType === 1) { + const computedStyle = getComputedStyle($ancestor); + if (computedStyle.transform && computedStyle.transform !== 'none') { + const inlineTransform = $ancestor.style.transform || ''; + const inlineTransition = muteElementTransition($ancestor); + rootAncestorTransformStore.push($ancestor, inlineTransform, inlineTransition); + $ancestor.style.transform = 'none'; + } + $ancestor = $ancestor.parentElement; } - // Words are used when lines only and prioritized over chars - const elementsArray = this.words.length ? this.words : this.chars; - let y, linesCount = 0; - for (let i = 0, l = elementsArray.length; i < l; i++) { - const $el = elementsArray[i]; - const { top, height } = $el.getBoundingClientRect(); - if (y && top - y > height * .5) linesCount++; - $el.setAttribute(dataLine, `${linesCount}`); - const nested = $el.querySelectorAll(`[${dataLine}]`); - let c = nested.length; - while (c--) nested[c].setAttribute(dataLine, `${linesCount}`); - y = top; + + for (let i = 0, l = toParse.length; i < l; i++) { + const child = toParse[i]; + scoped[i] = isStr(child) ? scopeRoot.querySelectorAll(child) : child; } - if (canSplitLines) { - const linesFragment = doc.createDocumentFragment(); - const parents = new Set(); - const clones = []; - for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) { - const $clone = /** @type {HTMLElement} */($el.cloneNode(true)); - filterLineElements($clone, lineIndex, new Set()).forEach($el => { - const $parent = $el.parentElement; - if ($parent) parents.add($parent); - $el.remove(); - }); - clones.push($clone); + + const parsedChildren = registerTargets(scoped); + + this.nodes.clear(); + this.rootNodes.clear(); + + const rootNode = this.registerElement(root, null); + // Root node are always targets + rootNode.isTarget = true; + this.rootNode = rootNode; + + const inRootNodeIds = new Set(); + // Update index and total for inital timing calculation + let index = 0; + const allNodeTargets = []; + this.nodes.forEach((node) => { allNodeTargets.push(node.$el); }); + this.nodes.forEach((node, id) => { + node.index = index++; + node.targets = allNodeTargets; + // Track ids of nodes that belong to the current root to filter detached matches + if (node && node.measuredIsInsideRoot) { + inRootNodeIds.add(id); } - parents.forEach(filterEmptyElements); - for (let cloneIndex = 0, clonesLength = clones.length; cloneIndex < clonesLength; cloneIndex++) { - processHTMLTemplate(lineTemplate, this.lines, clones[cloneIndex], linesFragment, lineType, this.debug, cloneIndex); + }); + + // Elements with a layout id outside the root that match the children selector + const detachedElementsLookup = new Set(); + const orderedDetachedElements = []; + + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + if (!$el || $el.nodeType !== 1 || $el === root) continue; + const insideRoot = isElementInRoot(root, $el); + if (!insideRoot) { + const layoutNodeId = $el.dataset.layoutId; + if (!layoutNodeId || !inRootNodeIds.has(layoutNodeId)) continue; } - $el.innerHTML = ''; - $el.appendChild(linesFragment); - if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); - if (charTemplate) this.chars = getAllTopLevelElements($el, charType); - } - // Remove the word wrappers and clear the words array if lines split only - if (this.linesOnly) { - const words = this.words; - let w = words.length; - while (w--) { - const $word = words[w]; - $word.replaceWith($word.textContent); + if (!detachedElementsLookup.has($el)) { + detachedElementsLookup.add($el); + orderedDetachedElements.push($el); } - words.length = 0; } - if (this.accessible && (canSplitLines || !isCached)) { - const $accessible = doc.createElement('span'); - // Make the accessible element visually-hidden (https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html) - $accessible.style.cssText = `position:absolute;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);width:1px;height:1px;white-space:nowrap;`; - // $accessible.setAttribute('tabindex', '-1'); - $accessible.innerHTML = this.html; - $el.insertBefore($accessible, $el.firstChild); - this.lines.forEach(setAriaHidden); - this.words.forEach(setAriaHidden); - this.chars.forEach(setAriaHidden); + + for (let i = 0, l = orderedDetachedElements.length; i < l; i++) { + this.ensureDetachedNode(orderedDetachedElements[i], detachedElementsLookup); } - this.width = /** @type {HTMLElement} */($el).offsetWidth; - if (canSplitLines || clearCache) { - this.effects.forEach((effect, i) => this.effectsCleanups[i] = effect(this)); + + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + const node = this.getNode($el); + if (node) { + let cur = node; + while (cur) { + if (cur.isTarget) break; + cur.isTarget = true; + cur = cur.parentNode; + } + } } - return this; - } - refresh() { - this.split(true); + this.scrollX = window.scrollX; + this.scrollY = window.scrollY; + + this.forEachNode(restoreNodeTransform); + + // Restore transition and transforms of root ancestors + + for (let i = 0, l = rootAncestorTransformStore.length; i < l; i += 3) { + const $el = /** @type {DOMTarget} */(rootAncestorTransformStore[i]); + const inlineTransform = /** @type {String} */(rootAncestorTransformStore[i + 1]); + const inlineTransition = /** @type {String|null} */(rootAncestorTransformStore[i + 2]); + if (inlineTransform && inlineTransform !== '') { + $el.style.transform = inlineTransform; + } else { + $el.style.removeProperty('transform'); + } + restoreElementTransition($el, inlineTransition); + } + + return this; } } /** - * @param {HTMLElement|NodeList|String|Array} target - * @param {TextSplitterParams} [parameters] - * @return {TextSplitter} + * @param {LayoutStateParams} params + * @return {[LayoutStateAnimationProperties, LayoutAnimationTimingsParams]} */ - const splitText = (target, parameters) => new TextSplitter(target, parameters); + function splitPropertiesFromParams(params) { + /** @type {LayoutStateAnimationProperties} */ + const properties = {}; + /** @type {LayoutAnimationTimingsParams} */ + const parameters = {}; + for (let name in params) { + const value = params[name]; + const isEase = name === 'ease'; + const isTiming = name === 'duration' || name === 'delay'; + if (isTiming || isEase) { + if (isEase) { + parameters[name] = /** @type {EasingParam} */(value); + } else { + parameters[name] = /** @type {Number|FunctionValue} */(value); + } + } else { + properties[name] = /** @type {Number|String} */(value); + } + } + return [properties, parameters]; + } - /** - * @deprecated text.split() is deprecated, import splitText() directly, or text.splitText() - * - * @param {HTMLElement|NodeList|String|Array} target - * @param {TextSplitterParams} [parameters] - * @return {TextSplitter} - */ - const split = (target, parameters) => { - console.warn('text.split() is deprecated, import splitText() directly, or text.splitText()'); - return new TextSplitter(target, parameters); - }; + class AutoLayout { + /** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + */ + constructor(root, params = {}) { + if (scope.current) scope.current.register(this); + const swapAtSplitParams = splitPropertiesFromParams(params.swapAt); + const enterFromSplitParams = splitPropertiesFromParams(params.enterFrom); + const leaveToSplitParams = splitPropertiesFromParams(params.leaveTo); + const transitionProperties = params.properties; + /** @type {Number|FunctionValue} */ + params.duration = setValue(params.duration, 350); + /** @type {Number|FunctionValue} */ + params.delay = setValue(params.delay, 0); + /** @type {EasingParam|FunctionValue} */ + params.ease = setValue(params.ease, 'inOut(3.5)'); + /** @type {AutoLayoutParams} */ + this.params = params; + /** @type {DOMTarget} */ + this.root = /** @type {DOMTarget} */(registerTargets(root)[0]); + /** @type {Number|String} */ + this.id = params.id || layoutId++; + /** @type {LayoutChildrenParam} */ + this.children = params.children || '*'; + /** @type {Boolean} */ + this.absoluteCoords = false; + /** @type {LayoutStateParams} */ + this.swapAtParams = mergeObjects(params.swapAt || { opacity: 0 }, { ease: 'inOut(1.75)' }); + /** @type {LayoutStateParams} */ + this.enterFromParams = params.enterFrom || { opacity: 0 }; + /** @type {LayoutStateParams} */ + this.leaveToParams = params.leaveTo || { opacity: 0 }; + /** @type {Set} */ + this.properties = new Set([ + 'opacity', + 'fontSize', + 'color', + 'backgroundColor', + 'borderRadius', + 'border', + 'filter', + 'clipPath', + ]); + if (swapAtSplitParams[0]) for (let name in swapAtSplitParams[0]) this.properties.add(name); + if (enterFromSplitParams[0]) for (let name in enterFromSplitParams[0]) this.properties.add(name); + if (leaveToSplitParams[0]) for (let name in leaveToSplitParams[0]) this.properties.add(name); + if (transitionProperties) for (let i = 0, l = transitionProperties.length; i < l; i++) this.properties.add(transitionProperties[i]); + /** @type {Set} */ + this.recordedProperties = new Set([ + 'display', + 'visibility', + 'translate', + 'position', + 'left', + 'top', + 'marginLeft', + 'marginTop', + 'width', + 'height', + 'maxWidth', + 'maxHeight', + 'minWidth', + 'minHeight', + ]); + this.properties.forEach(prop => this.recordedProperties.add(prop)); + /** @type {WeakSet} */ + this.pendingRemoval = new WeakSet(); + /** @type {Map} */ + this.transitionMuteStore = new Map(); + /** @type {LayoutSnapshot} */ + this.oldState = new LayoutSnapshot(this); + /** @type {LayoutSnapshot} */ + this.newState = new LayoutSnapshot(this); + /** @type {Timeline} */ + this.timeline = null; + /** @type {WAAPIAnimation} */ + this.transformAnimation = null; + /** @type {Array} */ + this.animating = []; + /** @type {Array} */ + this.swapping = []; + /** @type {Array} */ + this.leaving = []; + /** @type {Array} */ + this.entering = []; + // Record the current state as the old state to init the data attributes and allow imediate .animate() + this.oldState.record(); + // And all layout transition muted during the record + restoreLayoutTransition(this.transitionMuteStore); + } - var index = /*#__PURE__*/Object.freeze({ - __proto__: null, - TextSplitter: TextSplitter, - split: split, - splitText: splitText - }); + /** + * @return {this} + */ + revert() { + this.root.classList.remove('is-animated'); + if (this.timeline) { + this.timeline.complete(); + this.timeline = null; + } + if (this.transformAnimation) { + this.transformAnimation.complete(); + this.transformAnimation = null; + } + this.animating.length = this.swapping.length = this.leaving.length = this.entering.length = 0; + this.oldState.revert(); + this.newState.revert(); + requestAnimationFrame(() => restoreLayoutTransition(this.transitionMuteStore)); + return this; + } - + /** + * @return {this} + */ + record() { + // Commit transforms before measuring + if (this.transformAnimation) { + this.transformAnimation.cancel(); + this.transformAnimation = null; + } + // Record the old state + this.oldState.record(); + // Cancel any running timeline + if (this.timeline) { + this.timeline.cancel(); + this.timeline = null; + } + // Restore previously captured inline styles + this.newState.forEachRootNode(restoreNodeInlineStyles); + return this; + } - + /** + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + animate(params = {}) { + /** @type { LayoutAnimationTimingsParams } */ + const animationTimings = { + ease: setValue(params.ease, this.params.ease), + delay: setValue(params.delay, this.params.delay), + duration: setValue(params.duration, this.params.duration), + }; + /** @type {TimelineParams} */ + const tlParams = { + id: this.id + }; + const onComplete = setValue(params.onComplete, this.params.onComplete); + const onPause = setValue(params.onPause, this.params.onPause); + for (let name in defaults) { + if (name !== 'ease' && name !== 'duration' && name !== 'delay') { + if (!isUnd(params[name])) { + tlParams[name] = params[name]; + } else if (!isUnd(this.params[name])) { + tlParams[name] = this.params[name]; + } + } + } + tlParams.onComplete = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const ed = globals.editor; + const isScrollControled = (ap && ap.linked) || (ed && ed.showPanel); + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + return; + } + // Make sure to call .cancel() after restoreNodeInlineStyles(node); otehrwise the commited styles get reverted + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(node => { + restoreNodeVisualState(node); + restoreNodeInlineStyles(node); + }); + for (let i = 0, l = transformed.length; i < l; i++) { + const $el = transformed[i]; + $el.style.transform = newState.getComputedValue($el, 'transform'); + } + if (this.root.classList.contains('is-animated')) { + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + } + // Avoid CSS transitions at the end of the animation by restoring them on the next frame + requestAnimationFrame(() => { + if (this.root.classList.contains('is-animated')) return; + restoreLayoutTransition(this.transitionMuteStore); + }); + }; + tlParams.onPause = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const isScrollControled = ap && ap.linked; + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + return; + } + if (!this.root.classList.contains('is-animated')) return; + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(restoreNodeVisualState); + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + }; + tlParams.composition = false; + + const swapAtParams = mergeObjects(mergeObjects(params.swapAt || {}, this.swapAtParams), animationTimings); + const enterFromParams = mergeObjects(mergeObjects(params.enterFrom || {}, this.enterFromParams), animationTimings); + const leaveToParams = mergeObjects(mergeObjects(params.leaveTo || {}, this.leaveToParams), animationTimings); + const [ swapAtProps, swapAtTimings ] = splitPropertiesFromParams(swapAtParams); + const [ enterFromProps, enterFromTimings ] = splitPropertiesFromParams(enterFromParams); + const [ leaveToProps, leaveToTimings ] = splitPropertiesFromParams(leaveToParams); + + const oldState = this.oldState; + const newState = this.newState; + const animating = this.animating; + const swapping = this.swapping; + const entering = this.entering; + const leaving = this.leaving; + const pendingRemoval = this.pendingRemoval; + + animating.length = swapping.length = entering.length = leaving.length = 0; + + // Mute old state CSS transitions to prevent wrong properties calculation + oldState.forEachRootNode(muteNodeTransition); + // Capture the new state before animation + newState.record(); + newState.forEachRootNode(recordNodeInlineStyles); + + const targets = []; + const animated = []; + const transformed = []; + const animatedSwap = []; + const rootNode = newState.rootNode; + const $root = rootNode.$el; + + newState.forEachRootNode(node => { + + const $el = node.$el; + const id = node.id; + const parent = node.parentNode; + const parentAdded = parent ? parent.branchAdded : false; + const parentRemoved = parent ? parent.branchRemoved : false; + const parentNotRendered = parent ? parent.branchNotRendered : false; + + let oldStateNode = oldState.nodes.get(id); + + const hasNoOldState = !oldStateNode; + + if (hasNoOldState) { + oldStateNode = cloneNodeProperties(node, /** @type {LayoutNode} */({}), oldState); + oldState.nodes.set(id, oldStateNode); + oldStateNode.measuredIsRemoved = true; + } else if (oldStateNode.measuredIsRemoved && !node.measuredIsRemoved) { + cloneNodeProperties(node, oldStateNode, oldState); + oldStateNode.measuredIsRemoved = true; + } - + const oldParentNode = oldStateNode.parentNode; + const oldParentId = oldParentNode ? oldParentNode.id : null; + const newParentId = parent ? parent.id : null; + const parentChanged = oldParentId !== newParentId; + const elementChanged = oldStateNode.$el !== node.$el; + const wasRemovedBefore = oldStateNode.measuredIsRemoved; + const isRemovedNow = node.measuredIsRemoved; + + // Recalculate postion relative to their parent for elements that have been moved + if (!oldStateNode.measuredIsRemoved && !isRemovedNow && !hasNoOldState && (parentChanged || elementChanged)) { + const oldAbsoluteLeft = oldStateNode.properties.left; + const oldAbsoluteTop = oldStateNode.properties.top; + const newParent = parent || newState.rootNode; + const oldParent = newParent.id ? oldState.nodes.get(newParent.id) : null; + const parentLeft = oldParent ? oldParent.properties.left : newParent.properties.left; + const parentTop = oldParent ? oldParent.properties.top : newParent.properties.top; + const borderLeft = oldParent ? oldParent.properties.clientLeft : newParent.properties.clientLeft; + const borderTop = oldParent ? oldParent.properties.clientTop : newParent.properties.clientTop; + oldStateNode.properties.x = oldAbsoluteLeft - parentLeft - borderLeft; + oldStateNode.properties.y = oldAbsoluteTop - parentTop - borderTop; + } - /** - * Converts an easing function into a valid CSS linear() timing function string - * @param {EasingFunction} fn - * @param {number} [samples=100] - * @returns {string} CSS linear() timing function - */ - const easingToLinear = (fn, samples = 100) => { - const points = []; - for (let i = 0; i <= samples; i++) points.push(round$1(fn(i / samples), 4)); - return `linear(${points.join(', ')})`; - }; + if (node.hasVisibilitySwap) { + if (node.hasVisibilityHidden) { + node.$el.style.visibility = 'visible'; + node.$measure.style.visibility = 'hidden'; + } + if (node.hasDisplayNone) { + node.$el.style.display = oldStateNode.measuredDisplay || node.measuredDisplay || ''; + // Setting visibility 'hidden' instead of display none to avoid calculation issues + node.$measure.style.visibility = 'hidden'; + // @TODO: check why setting display here can cause calculation issues + // node.$measure.style.display = 'none'; + } + } - const WAAPIEasesLookups = {}; + const wasPendingRemoval = pendingRemoval.has($el); + const wasVisibleBefore = oldStateNode.measuredIsVisible; + const isVisibleNow = node.measuredIsVisible; + const becomeVisible = !wasVisibleBefore && isVisibleNow && !parentNotRendered; + const topLevelAdded = !isRemovedNow && (wasRemovedBefore || wasPendingRemoval) && !parentAdded; + const newlyRemoved = isRemovedNow && !wasRemovedBefore && !parentRemoved; + const topLevelRemoved = newlyRemoved || isRemovedNow && wasPendingRemoval && !parentRemoved; + + node.branchAdded = parentAdded || topLevelAdded; + node.branchRemoved = parentRemoved || topLevelRemoved; + node.branchNotRendered = parentNotRendered || isRemovedNow; + + if (isRemovedNow && wasVisibleBefore) { + node.$el.style.display = oldStateNode.measuredDisplay; + node.$el.style.visibility = 'visible'; + cloneNodeProperties(oldStateNode, node, newState); + } - /** - * @param {EasingParam} ease - * @return {String} - */ - const parseWAAPIEasing = (ease) => { - let parsedEase = WAAPIEasesLookups[ease]; - if (parsedEase) return parsedEase; - parsedEase = 'linear'; - if (isStr(ease)) { - if ( - stringStartsWith(ease, 'linear') || - stringStartsWith(ease, 'cubic-') || - stringStartsWith(ease, 'steps') || - stringStartsWith(ease, 'ease') - ) { - parsedEase = ease; - } else if (stringStartsWith(ease, 'cubicB')) { - parsedEase = toLowerCase(ease); - } else { - const parsed = parseEaseString(ease); - if (isFnc(parsed)) parsedEase = parsed === none ? 'linear' : easingToLinear(parsed); - } - // Only cache string based easing name, otherwise function arguments get lost - WAAPIEasesLookups[ease] = parsedEase; - } else if (isFnc(ease)) { - const easing = easingToLinear(ease); - if (easing) parsedEase = easing; - } else if (/** @type {Spring} */(ease).ease) { - parsedEase = easingToLinear(/** @type {Spring} */(ease).ease); - } - return parsedEase; - }; + // Node is leaving + if (newlyRemoved) { + if (node.isTarget) { + leaving.push($el); + node.isLeaving = true; + } + pendingRemoval.add($el); + } else if (!isRemovedNow && wasPendingRemoval) { + pendingRemoval.delete($el); + } - const transformsShorthands = ['x', 'y', 'z']; - const commonDefaultPXProperties = [ - 'perspective', - 'width', - 'height', - 'margin', - 'padding', - 'top', - 'right', - 'bottom', - 'left', - 'borderWidth', - 'fontSize', - 'borderRadius', - ...transformsShorthands - ]; + // Node is entering + if ((topLevelAdded && !parentNotRendered) || becomeVisible) { + updateNodeProperties(oldStateNode, enterFromProps); + if (node.isTarget) { + entering.push($el); + node.isEntering = true; + } + // Node is leaving + } else if (topLevelRemoved && !parentNotRendered) { + updateNodeProperties(node, leaveToProps); + } - const validIndividualTransforms = /*#__PURE__*/ (() => [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))])(); + // Node is animating + // The animating array is used only to calculate delays and duration on root children + if (node !== rootNode && node.isTarget && !node.isEntering && !node.isLeaving) { + animating.push($el); + } - let transformsPropertiesRegistered = null; + targets.push($el); - /** - * @param {String} propName - * @param {WAAPIKeyframeValue} value - * @param {DOMTarget} $el - * @param {Number} i - * @param {Number} targetsLength - * @return {String} - */ - const normalizeTweenValue = (propName, value, $el, i, targetsLength) => { - // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables - let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, targetsLength); - if (!isNum(v)) return v; - if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`; - if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`; - return `${v}`; - }; + }); - /** - * @param {DOMTarget} $el - * @param {String} propName - * @param {WAAPIKeyframeValue} from - * @param {WAAPIKeyframeValue} to - * @param {Number} i - * @param {Number} targetsLength - * @return {WAAPITweenValue} - */ - const parseIndividualTweenValue = ($el, propName, from, to, i, targetsLength) => { - /** @type {WAAPITweenValue} */ - let tweenValue = '0'; - const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, targetsLength) : getComputedStyle($el)[propName]; - if (!isUnd(from)) { - const computedFrom = normalizeTweenValue(propName, from, $el, i, targetsLength); - tweenValue = [computedFrom, computedTo]; - } else { - tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, targetsLength)) : computedTo; - } - return tweenValue; - }; + let enteringIndex = 0; + let leavingIndex = 0; + let animatingIndex = 0; - class WAAPIAnimation { - /** - * @param {DOMTargetsParam} targets - * @param {WAAPIAnimationParams} params - */ - constructor(targets, params) { + newState.forEachRootNode(node => { - if (scope.current) scope.current.register(this); + const $el = node.$el; + const parent = node.parentNode; + const oldStateNode = oldState.nodes.get(node.id); + const nodeProperties = node.properties; + const oldStateNodeProperties = oldStateNode.properties; - // Skip the registration and fallback to no animation in case CSS.registerProperty is not supported - if (isNil(transformsPropertiesRegistered)) { - if (isBrowser && (isUnd(CSS) || !Object.hasOwnProperty.call(CSS, 'registerProperty'))) { - transformsPropertiesRegistered = false; + // Use closest animated parent index and total values so that children staggered delays are in sync with their parent + let animatedParent = parent !== rootNode && parent; + while (animatedParent && !animatedParent.isTarget && animatedParent !== rootNode) { + animatedParent = animatedParent.parentNode; + } + + // Root is always animated first in sync with the first child (animating.length is the total of children) + if (node === rootNode) { + node.index = 0; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); + } else if (node.isEntering) { + node.index = animatedParent ? animatedParent.index : enteringIndex; + node.targets = animatedParent ? animating : entering; + updateNodeTimingParams(node, enterFromTimings); + enteringIndex++; + } else if (node.isLeaving) { + node.index = animatedParent ? animatedParent.index : leavingIndex; + node.targets = animatedParent ? animating : leaving; + leavingIndex++; + updateNodeTimingParams(node, leaveToTimings); + } else if (node.isTarget) { + node.index = animatingIndex++; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); } else { - validTransforms.forEach(t => { - const isSkew = stringStartsWith(t, 'skew'); - const isScale = stringStartsWith(t, 'scale'); - const isRotate = stringStartsWith(t, 'rotate'); - const isTranslate = stringStartsWith(t, 'translate'); - const isAngle = isRotate || isSkew; - const syntax = isAngle ? '' : isScale ? "" : isTranslate ? "" : "*"; - try { - CSS.registerProperty({ - name: '--' + t, - syntax, - inherits: false, - initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0', - }); - } catch {} }); - transformsPropertiesRegistered = true; + node.index = animatedParent ? animatedParent.index : 0; + node.targets = animating; + updateNodeTimingParams(node, swapAtTimings); } - } - const parsedTargets = registerTargets(targets); - const targetsLength = parsedTargets.length; + // Make sure the old state node has its inex and total values up to date for valid "from" function values calculation + oldStateNode.index = node.index; + oldStateNode.targets = node.targets; - if (!targetsLength) { - console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`); - } + // Computes all values up front so we can check for changes and we don't have to re-compute them inside the animation props + for (let prop in nodeProperties) { + nodeProperties[prop] = getFunctionValue(nodeProperties[prop], $el, node.index, node.targets, null, null); + oldStateNodeProperties[prop] = getFunctionValue(oldStateNodeProperties[prop], $el, oldStateNode.index, oldStateNode.targets, null, null); + } - const ease = setValue(params.ease, parseWAAPIEasing(globals.defaults.ease)); - const spring = /** @type {Spring} */(ease).ease && ease; - const autoplay = setValue(params.autoplay, globals.defaults.autoplay); - const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false; - const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true; - const reversed = params.reversed && /** @type {Boolean} */(params.reversed) === true; - const loop = setValue(params.loop, globals.defaults.loop); - const iterations = /** @type {Number} */((loop === true || loop === Infinity) ? Infinity : isNum(loop) ? loop + 1 : 1); - /** @type {PlaybackDirection} */ - const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal'; - /** @type {FillMode} */ - const fill = 'both'; // We use 'both' here because the animation can be reversed during playback - /** @type {String} */ - const easing = parseWAAPIEasing(ease); - const timeScale = (globals.timeScale === 1 ? 1 : K); + // Use a 1px tolerance to detect dimensions changes to prevent width / height animations on barelly visible elements + const sizeTolerance = 1; + const widthChanged = Math.abs(nodeProperties.width - oldStateNodeProperties.width) > sizeTolerance; + const heightChanged = Math.abs(nodeProperties.height - oldStateNodeProperties.height) > sizeTolerance; - /** @type {DOMTargetsArray}] */ - this.targets = parsedTargets; - /** @type {Array}] */ - this.animations = []; - /** @type {globalThis.Animation}] */ - this.controlAnimation = null; - /** @type {Callback} */ - this.onComplete = params.onComplete || /** @type {Callback} */(/** @type {unknown} */(globals.defaults.onComplete)); - /** @type {Number} */ - this.duration = 0; - /** @type {Boolean} */ - this.muteCallbacks = false; - /** @type {Boolean} */ - this.completed = false; - /** @type {Boolean} */ - this.paused = !autoplay || scroll !== false; - /** @type {Boolean} */ - this.reversed = reversed; - /** @type {Boolean} */ - this.persist = setValue(params.persist, globals.defaults.persist); - /** @type {Boolean|ScrollObserver} */ - this.autoplay = autoplay; - /** @type {Number} */ - this._speed = setValue(params.playbackRate, globals.defaults.playbackRate); - /** @type {Function} */ - this._resolve = noop; // Used by .then() - /** @type {Number} */ - this._completed = 0; - /** @type {Array.} */ - this._inlineStyles = []; + node.sizeChanged = (widthChanged || heightChanged); - parsedTargets.forEach(($el, i) => { + // const hiddenStateChanged = (topLevelAdded || newlyRemoved) && wasRemovedBefore !== isRemovedNow; - const cachedTransforms = $el[transformsSymbol]; - const hasIndividualTransforms = validIndividualTransforms.some(t => params.hasOwnProperty(t)); - const elStyle = $el.style; - const inlineStyles = this._inlineStyles[i] = {}; + if (node.isTarget && (!node.measuredIsRemoved && oldStateNode.measuredIsVisible || node.measuredIsRemoved && node.measuredIsVisible)) { + if (nodeProperties.transform !== 'none' || oldStateNodeProperties.transform !== 'none') { + node.hasTransform = true; + transformed.push($el); + } + for (let prop in nodeProperties) { + // if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop] || hiddenStateChanged)) { + if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop])) { + animated.push($el); + break; + } + } + } - /** @type {Number} */ - const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, targetsLength)) * timeScale; - /** @type {Number} */ - const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, targetsLength) * timeScale; - /** @type {CompositeOperation} */ - const composite = /** @type {CompositeOperation} */(setValue(params.composition, 'replace')); + if (!node.isTarget) { + swapping.push($el); + if (node.sizeChanged && parent && parent.isTarget && parent.sizeChanged) { + if (swapAtProps.transform) { + node.hasTransform = true; + transformed.push($el); + } + animatedSwap.push($el); + } + } - for (let name in params) { - if (!isKey(name)) continue; - /** @type {PropertyIndexedKeyframes} */ - const keyframes = {}; - /** @type {KeyframeAnimationOptions} */ - const tweenParams = { iterations, direction, fill, easing, duration, delay, composite }; - const propertyValue = params[name]; - const individualTransformProperty = hasIndividualTransforms ? validTransforms.includes(name) ? name : shortTransforms.get(name) : false; + }); - const styleName = individualTransformProperty ? 'transform' : name; - if (!inlineStyles[styleName]) { - inlineStyles[styleName] = elStyle[styleName]; + const timingParams = { + delay: (/** @type {HTMLElement} */$el) => newState.getNode($el).delay, + duration: (/** @type {HTMLElement} */$el) => newState.getNode($el).duration, + ease: (/** @type {HTMLElement} */$el) => newState.getNode($el).ease, + }; + + tlParams.defaults = timingParams; + + this.timeline = createTimeline(tlParams); + + // Imediatly return the timeline if no layout changes detected + if (!animated.length && !transformed.length && !swapping.length) { + // Make sure to restore all CSS transition if no animation + restoreLayoutTransition(this.transitionMuteStore); + return this.timeline.complete(); + } + + if (targets.length) { + + this.root.classList.add('is-animated'); + + for (let i = 0, l = targets.length; i < l; i++) { + const $el = targets[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; + + // muteNodeTransition(newNode); + + // Don't animate positions of inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + // Display grid can mess with the absolute positioning, so set it to block during transition + if (oldNode.measuredDisplay === 'grid' || newNode.measuredDisplay === 'grid') $el.style.setProperty('display', 'block', 'important'); + // All children must be in position absolute or fixed + if ($el !== $root || this.absoluteCoords) { + $el.style.position = this.absoluteCoords ? 'fixed' : 'absolute'; + $el.style.left = '0px'; + $el.style.top = '0px'; + $el.style.marginLeft = '0px'; + $el.style.marginTop = '0px'; + $el.style.translate = `${oldNodeState.x}px ${oldNodeState.y}px`; + } + if ($el === $root && newNode.measuredPosition === 'static') { + $el.style.position = 'relative'; + // Cancel left / trop in case the static element had muted values now activated by potision relative + $el.style.left = '0px'; + $el.style.top = '0px'; + } } + // Animate dimensions for all elements (including inlined) + $el.style.width = `${oldNodeState.width}px`; + $el.style.height = `${oldNodeState.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + } - let parsedPropertyValue; - if (isObj(propertyValue)) { - const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue); - const tweenOptionsEase = setValue(tweenOptions.ease, ease); - const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase; - const to = /** @type {WAAPITweenOptions} */(tweenOptions).to; - const from = /** @type {WAAPITweenOptions} */(tweenOptions).from; - /** @type {Number} */ - tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, targetsLength)) * timeScale; - /** @type {Number} */ - tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, targetsLength) * timeScale; - /** @type {CompositeOperation} */ - tweenParams.composite = /** @type {CompositeOperation} */(setValue(tweenOptions.composition, composite)); - /** @type {String} */ - tweenParams.easing = parseWAAPIEasing(tweenOptionsEase); - parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, targetsLength); - if (individualTransformProperty) { - keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; - cachedTransforms[individualTransformProperty] = parsedPropertyValue; - } else { - keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, targetsLength); + // Restore the scroll position if the oldState differs from the current state + if (oldState.scrollX !== window.scrollX || oldState.scrollY !== window.scrollY) { + // Restoring in the next frame avoids race conditions if for example a waapi animation commit styles that affect the root height + requestAnimationFrame(() => window.scrollTo(oldState.scrollX, oldState.scrollY)); + } + + for (let i = 0, l = animated.length; i < l; i++) { + const $el = animated[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; + const newNodeState = newNode.properties; + let nodeHasChanged = false; + /** @type {AnimationParams} */ + const animatedProps = { + composition: 'none', + }; + if (oldNodeState.width !== newNodeState.width) { + animatedProps.width = [oldNodeState.width, newNodeState.width]; + nodeHasChanged = true; + } + if (oldNodeState.height !== newNodeState.height) { + animatedProps.height = [oldNodeState.height, newNodeState.height]; + nodeHasChanged = true; + } + // If the node has transforms we handle the translate animation in waapi otherwise translate and other transforms can be out of sync + // And we don't animate the position of inlined elements + if (!newNode.hasTransform && !newNode.isInlined) { + animatedProps.translate = [`${oldNodeState.x}px ${oldNodeState.y}px`, `${newNodeState.x}px ${newNodeState.y}px`]; + nodeHasChanged = true; + } + this.properties.forEach(prop => { + const oldVal = oldNodeState[prop]; + const newVal = newNodeState[prop]; + if (prop !== 'transform' && oldVal !== newVal) { + animatedProps[prop] = [oldVal, newVal]; + nodeHasChanged = true; } - addWAAPIAnimation(this, $el, name, keyframes, tweenParams); - if (!isUnd(from)) { - if (!individualTransformProperty) { - elStyle[name] = keyframes[name][0]; - } else { - const key = `--${individualTransformProperty}`; - elStyle.setProperty(key, keyframes[key][0]); - } + }); + if (nodeHasChanged) { + this.timeline.add($el, animatedProps, 0); + } + } + + } + + if (swapping.length) { + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const oldNode = oldState.getNode($el); + const oldNodeProps = oldNode.properties; + $el.style.width = `${oldNodeProps.width}px`; + $el.style.height = `${oldNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // We don't animate the position of inlined elements + if (!oldNode.isInlined) { + $el.style.translate = `${oldNodeProps.x}px ${oldNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${oldState.getComputedValue($el, prop)}`; + } + }); + } + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const newNode = newState.getNode($el); + const newNodeProps = newNode.properties; + this.timeline.call(() => { + $el.style.width = `${newNodeProps.width}px`; + $el.style.height = `${newNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // Don't set translate for inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + $el.style.translate = `${newNodeProps.x}px ${newNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${newState.getComputedValue($el, prop)}`; + } + }); + }, newNode.delay + newNode.duration / 2); + } + + if (animatedSwap.length) { + const ease = parseEase(newState.nodes.get(animatedSwap[0].dataset.layoutId).ease); + const inverseEased = t => 1 - ease(1 - t); + const animatedSwapParams = /** @type {AnimationParams} */({}); + if (swapAtProps) { + for (let prop in swapAtProps) { + if (prop !== 'transform') { + animatedSwapParams[prop] = [ + { from: (/** @type {HTMLElement} */$el) => oldState.getComputedValue($el, prop), to: swapAtProps[prop] }, + { from: swapAtProps[prop], to: (/** @type {HTMLElement} */$el) => newState.getComputedValue($el, prop), ease: inverseEased } + ]; + } + } + } + this.timeline.add(animatedSwap, animatedSwapParams, 0); + } + + } + + const transformedLength = transformed.length; + + if (transformedLength) { + // We only need to set the transform property here since translate is already defined in the targets loop + for (let i = 0; i < transformedLength; i++) { + const $el = transformed[i]; + const node = newState.getNode($el); + // Don't set translate for inlined elements (to avoid text reflow) + if (!node.isInlined) { + $el.style.translate = `${oldState.getComputedValue($el, 'x')}px ${oldState.getComputedValue($el, 'y')}px`; + } + $el.style.transform = oldState.getComputedValue($el, 'transform'); + if (animatedSwap.includes($el)) { + node.ease = getFunctionValue(swapAtParams.ease, $el, node.index, node.targets, null, null); + node.duration = getFunctionValue(swapAtParams.duration, $el, node.index, node.targets, null, null); + } + } + this.transformAnimation = waapi.animate(transformed, { + translate: (/** @type {HTMLElement} */$el) => { + const node = newState.getNode($el); + // Don't animate translate for inlined elements (to avoid text reflow) + if (node.isInlined) return '0px 0px'; + return `${newState.getComputedValue($el, 'x')}px ${newState.getComputedValue($el, 'y')}px`; + }, + transform: (/** @type {HTMLElement} */$el) => { + const newValue = newState.getComputedValue($el, 'transform'); + if (!animatedSwap.includes($el)) return newValue; + const oldValue = oldState.getComputedValue($el, 'transform'); + const node = newState.getNode($el); + return [oldValue, getFunctionValue(swapAtProps.transform, $el, node.index, node.targets, null, null), newValue] + }, + autoplay: false, + // persist: true, + ...timingParams, + }); + this.timeline.sync(this.transformAnimation, 0); + } + + return this.timeline.init(); + } + + /** + * @param {(layout: this) => void} callback + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + update(callback, params = {}) { + this.record(); + callback(this); + return this.animate(params); + } + } + + /** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + * @return {AutoLayout} + */ + const createLayout = (root, params) => new AutoLayout(root, params); + + // Chain-able utilities + + const numberUtils = numberImports; // Needed to keep the import when bundling + + const chainables = {}; + + /** + * @callback UtilityFunction + * @param {...*} args + * @return {Number|String} + * + * @param {UtilityFunction} fn + * @param {Number} [last=0] + * @return {function(...(Number|String)): function(Number|String): (Number|String)} + */ + const curry = (fn, last = 0) => (...args) => last ? v => fn(...args, v) : v => fn(v, ...args); + + /** + * @param {Function} fn + * @return {function(...(Number|String))} + */ + const chain = fn => { + return (...args) => { + const result = fn(...args); + return new Proxy(noop, { + apply: (_, __, [v]) => result(v), + get: (_, prop) => { + if (!chainables[prop]) return undefined; + return chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { + const nextResult = chainables[prop](...nextArgs); + return (/**@type {Number|String} */v) => nextResult(result(v)); + }) + } + }); + } + }; + + /** + * @param {UtilityFunction} fn + * @param {String} name + * @param {Number} [right] + * @return {function(...(Number|String)): UtilityFunction} + */ + const makeChainable = (name, fn, right = 0) => { + const chained = (...args) => (args.length < fn.length ? chain(curry(fn, right)) : fn)(...args); + if (!chainables[name]) chainables[name] = chained; + return chained; + }; + + /** + * @typedef {Object} ChainablesMap + * @property {ChainedClamp} clamp + * @property {ChainedRound} round + * @property {ChainedSnap} snap + * @property {ChainedWrap} wrap + * @property {ChainedLerp} lerp + * @property {ChainedDamp} damp + * @property {ChainedMapRange} mapRange + * @property {ChainedRoundPad} roundPad + * @property {ChainedPadStart} padStart + * @property {ChainedPadEnd} padEnd + * @property {ChainedDegToRad} degToRad + * @property {ChainedRadToDeg} radToDeg + */ + + /** + * @callback ChainedUtilsResult + * @param {Number} value - The value to process through the chained operations + * @return {Number} The processed result + */ + + /** + * @typedef {ChainablesMap & ChainedUtilsResult} ChainableUtil + */ + + // Chainable + + /** + * @callback ChainedRoundPad + * @param {Number} decimalLength - Number of decimal places + * @return {ChainableUtil} + */ + const roundPad = /** @type {typeof numberUtils.roundPad & ChainedRoundPad} */(makeChainable('roundPad', numberUtils.roundPad)); + + /** + * @callback ChainedPadStart + * @param {Number} totalLength - Target length + * @param {String} padString - String to pad with + * @return {ChainableUtil} + */ + const padStart = /** @type {typeof numberUtils.padStart & ChainedPadStart} */(makeChainable('padStart', numberUtils.padStart)); + + /** + * @callback ChainedPadEnd + * @param {Number} totalLength - Target length + * @param {String} padString - String to pad with + * @return {ChainableUtil} + */ + const padEnd = /** @type {typeof numberUtils.padEnd & ChainedPadEnd} */(makeChainable('padEnd', numberUtils.padEnd)); + + /** + * @callback ChainedWrap + * @param {Number} min - Minimum boundary + * @param {Number} max - Maximum boundary + * @return {ChainableUtil} + */ + const wrap = /** @type {typeof numberUtils.wrap & ChainedWrap} */(makeChainable('wrap', numberUtils.wrap)); + + /** + * @callback ChainedMapRange + * @param {Number} inLow - Input range minimum + * @param {Number} inHigh - Input range maximum + * @param {Number} outLow - Output range minimum + * @param {Number} outHigh - Output range maximum + * @return {ChainableUtil} + */ + const mapRange = /** @type {typeof numberUtils.mapRange & ChainedMapRange} */(makeChainable('mapRange', numberUtils.mapRange)); + + /** + * @callback ChainedDegToRad + * @return {ChainableUtil} + */ + const degToRad = /** @type {typeof numberUtils.degToRad & ChainedDegToRad} */(makeChainable('degToRad', numberUtils.degToRad)); + + /** + * @callback ChainedRadToDeg + * @return {ChainableUtil} + */ + const radToDeg = /** @type {typeof numberUtils.radToDeg & ChainedRadToDeg} */(makeChainable('radToDeg', numberUtils.radToDeg)); + + /** + * @callback ChainedSnap + * @param {Number|Array} increment - Step size or array of snap points + * @return {ChainableUtil} + */ + const snap = /** @type {typeof numberUtils.snap & ChainedSnap} */(makeChainable('snap', numberUtils.snap)); + + /** + * @callback ChainedClamp + * @param {Number} min - Minimum boundary + * @param {Number} max - Maximum boundary + * @return {ChainableUtil} + */ + const clamp = /** @type {typeof numberUtils.clamp & ChainedClamp} */(makeChainable('clamp', numberUtils.clamp)); + + /** + * @callback ChainedRound + * @param {Number} decimalLength - Number of decimal places + * @return {ChainableUtil} + */ + const round = /** @type {typeof numberUtils.round & ChainedRound} */(makeChainable('round', numberUtils.round)); + + /** + * @callback ChainedLerp + * @param {Number} start - Starting value + * @param {Number} end - Ending value + * @return {ChainableUtil} + */ + const lerp = /** @type {typeof numberUtils.lerp & ChainedLerp} */(makeChainable('lerp', numberUtils.lerp, 1)); + + /** + * @callback ChainedDamp + * @param {Number} start - Starting value + * @param {Number} end - Target value + * @param {Number} deltaTime - Delta time in ms + * @return {ChainableUtil} + */ + const damp = /** @type {typeof numberUtils.damp & ChainedDamp} */(makeChainable('damp', numberUtils.damp, 1)); + + /** + * Generate a random number between optional min and max (inclusive) and decimal precision + * + * @callback RandomNumberGenerator + * @param {Number} [min=0] - The minimum value (inclusive) + * @param {Number} [max=1] - The maximum value (inclusive) + * @param {Number} [decimalLength=0] - Number of decimal places to round to + * @return {Number} A random number between min and max + */ + + /** + * Generates a random number between min and max (inclusive) with optional decimal precision + * + * @type {RandomNumberGenerator} + */ + const random = (min = 0, max = 1, decimalLength = 0) => { + const m = 10 ** decimalLength; + return Math.floor((Math.random() * (max - min + (1 / m)) + min) * m) / m; + }; + + let _seed = 0; + + /** + * Creates a seeded pseudorandom number generator function + * + * @param {Number} [seed] - The seed value for the random number generator + * @param {Number} [seededMin=0] - The minimum default value (inclusive) of the returned function + * @param {Number} [seededMax=1] - The maximum default value (inclusive) of the returned function + * @param {Number} [seededDecimalLength=0] - Default number of decimal places to round to of the returned function + * @return {RandomNumberGenerator} A function to generate a random number between optional min and max (inclusive) and decimal precision + */ + const createSeededRandom = (seed, seededMin = 0, seededMax = 1, seededDecimalLength = 0) => { + let t = seed === undefined ? _seed++ : seed; + return (min = seededMin, max = seededMax, decimalLength = seededDecimalLength) => { + t += 0x6D2B79F5; + t = Math.imul(t ^ t >>> 15, t | 1); + t ^= t + Math.imul(t ^ t >>> 7, t | 61); + const m = 10 ** decimalLength; + return Math.floor(((((t ^ t >>> 14) >>> 0) / 4294967296) * (max - min + (1 / m)) + min) * m) / m; + } + }; + + /** + * Picks a random element from an array or a string + * + * @template T + * @param {String|Array} items - The array or string to pick from + * @return {String|T} A random element from the array or character from the string + */ + const randomPick = items => items[random(0, items.length - 1)]; + + /** + * Shuffles an array in-place using the Fisher-Yates algorithm + * Adapted from https://bost.ocks.org/mike/shuffle/ + * + * @param {Array} items - The array to shuffle (will be modified in-place) + * @return {Array} The same array reference, now shuffled + */ + const shuffle = items => { + let m = items.length, t, i; + while (m) { i = random(0, --m); t = items[m]; items[m] = items[i]; items[i] = t; } + return items; + }; + + + + + + /** + * @overload + * @param {Number} val + * @param {StaggerParams} [params] + * @return {StaggerFunction} + */ + + /** + * @overload + * @param {String} val + * @param {StaggerParams} [params] + * @return {StaggerFunction} + */ + + /** + * @overload + * @param {[Number, Number]} val + * @param {StaggerParams} [params] + * @return {StaggerFunction} + */ + + /** + * @overload + * @param {[String, String]} val + * @param {StaggerParams} [params] + * @return {StaggerFunction} + */ + + /** + * @param {Number|String|[Number, Number]|[String, String]} val The staggered value or range + * @param {StaggerParams} [params] The stagger parameters + * @return {StaggerFunction} + */ + const stagger = (val, params = {}) => { + let values = []; + let maxValue = 0; + let cachedOffset; + const from = params.from; + const reversed = params.reversed; + const ease = params.ease; + const hasEasing = !isUnd(ease); + const hasSpring = hasEasing && !isUnd(/** @type {Spring} */(ease).ease); + const staggerEase = hasSpring ? /** @type {Spring} */(ease).ease : hasEasing ? parseEase(ease) : null; + const grid = params.grid; + const autoGrid = grid === true; + const axis = params.axis; + const customTotal = params.total; + const fromFirst = isUnd(from) || from === 0 || from === 'first'; + const fromCenter = from === 'center'; + const fromLast = from === 'last'; + const fromRandom = from === 'random'; + const fromArr = isArr(from); + const isRange = isArr(val); + const useProp = params.use; + const val1 = isRange ? parseNumber(val[0]) : parseNumber(val); + const val2 = isRange ? parseNumber(val[1]) : 0; + const unitMatch = unitsExecRgx.exec((isRange ? val[1] : val) + emptyString); + const start = params.start || 0 + (isRange ? val1 : 0); + let fromIndex = fromFirst ? 0 : isNum(from) ? from : 0; + return (target, i, t, _, tl) => { + const [ registeredTarget ] = registerTargets(target); + const total = isUnd(customTotal) ? t.length : customTotal; + const customIndex = !isUnd(useProp) ? isFnc(useProp) ? useProp(registeredTarget, i, total) : getOriginalAnimatableValue(registeredTarget, useProp) : false; + const staggerIndex = isNum(customIndex) || isStr(customIndex) && isNum(+customIndex) ? +customIndex : i; + if (fromCenter) fromIndex = (total - 1) / 2; + if (fromLast) fromIndex = total - 1; + if (!values.length) { + if (autoGrid) { + let hasPositions = true; + let minPosX = Infinity; + let minPosY = Infinity; + let maxPosX = -Infinity; + let maxPosY = -Infinity; + const pxArr = []; + const pyArr = []; + for (let index = 0; index < total; index++) { + const el = t[index]; + let px = 0; + let py = 0; + let found = false; + if (el && isFnc(el.getBoundingClientRect)) { + const rect = el.getBoundingClientRect(); + px = rect.left + rect.width / 2; + py = rect.top + rect.height / 2; + found = true; + } else { + const obj = /** @type {JSTarget} */(el); + if (obj && isNum(obj.x) && isNum(obj.y)) { + px = obj.x; + py = obj.y; + found = true; + } + } + if (!found) { + hasPositions = false; + break; + } + pxArr.push(px); + pyArr.push(py); + if (px < minPosX) minPosX = px; + if (py < minPosY) minPosY = py; + if (px > maxPosX) maxPosX = px; + if (py > maxPosY) maxPosY = py; + } + if (hasPositions) { + let fX = pxArr[0]; + let fY = pyArr[0]; + if (fromArr) { + fX = minPosX + from[0] * (maxPosX - minPosX); + fY = minPosY + from[1] * (maxPosY - minPosY); + } else if (fromCenter) { + fX = (minPosX + maxPosX) / 2; + fY = (minPosY + maxPosY) / 2; + } else if (fromLast) { + fX = pxArr[total - 1]; + fY = pyArr[total - 1]; + } else if (isNum(from)) { + fX = pxArr[from]; + fY = pyArr[from]; + } + for (let index = 0; index < total; index++) { + const distanceX = fX - pxArr[index]; + const distanceY = fY - pyArr[index]; + let value = sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values.push(value); + } + let minDist = Infinity; + for (let index = 0, l = values.length; index < l; index++) { + const absVal = abs(values[index]); + if (absVal > 0 && absVal < minDist) minDist = absVal; + } + if (minDist > 0 && minDist < Infinity) { + for (let index = 0, l = values.length; index < l; index++) { + values[index] = values[index] / minDist; + } } } else { - parsedPropertyValue = isArr(propertyValue) ? - propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, targetsLength)) : - normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, targetsLength); - if (individualTransformProperty) { - keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; - cachedTransforms[individualTransformProperty] = parsedPropertyValue; + for (let index = 0; index < total; index++) { + values.push(abs(fromIndex - index)); + } + } + } else { + for (let index = 0; index < total; index++) { + if (!grid) { + values.push(abs(fromIndex - index)); + } else { + let fromX, fromY; + if (fromArr) { + fromX = from[0] * (grid[0] - 1); + fromY = from[1] * (grid[1] - 1); + } else if (fromCenter) { + fromX = (grid[0] - 1) / 2; + fromY = (grid[1] - 1) / 2; + } else { + fromX = fromIndex % grid[0]; + fromY = floor(fromIndex / grid[0]); + } + const toX = index % grid[0]; + const toY = floor(index / grid[0]); + const distanceX = fromX - toX; + const distanceY = fromY - toY; + let value = sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values.push(value); + } + } + } + maxValue = max(...values); + if (staggerEase) values = values.map(val => staggerEase(val / maxValue) * maxValue); + if (reversed) values = values.map(val => axis ? (val < 0) ? val * -1 : -val : abs(maxValue - val)); + if (fromRandom) values = shuffle(values); + } + const spacing = isRange ? (val2 - val1) / maxValue : val1; + if (isUnd(cachedOffset)) { + cachedOffset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); + } + /** @type {String|Number} */ + let output = cachedOffset + ((spacing * round$1(values[staggerIndex], 2)) || 0); + if (params.modifier) output = params.modifier(/** @type {Number} */(output)); + if (unitMatch) output = `${output}${unitMatch[2]}`; + return output; + } + }; + + var index$2 = /*#__PURE__*/Object.freeze({ + __proto__: null, + $: registerTargets, + addChild: addChild, + clamp: clamp, + cleanInlineStyles: cleanInlineStyles, + createSeededRandom: createSeededRandom, + damp: damp, + degToRad: degToRad, + forEachChildren: forEachChildren, + get: get, + keepTime: keepTime, + lerp: lerp, + mapRange: mapRange, + padEnd: padEnd, + padStart: padStart, + radToDeg: radToDeg, + random: random, + randomPick: randomPick, + remove: remove, + removeChild: removeChild, + round: round, + roundPad: roundPad, + set: set, + shuffle: shuffle, + snap: snap, + stagger: stagger, + sync: sync, + wrap: wrap + }); + + + + /** + * @param {TargetsParam} path + * @return {SVGGeometryElement|void} + */ + const getPath = path => { + const parsedTargets = parseTargets(path); + const $parsedSvg = /** @type {SVGGeometryElement} */(parsedTargets[0]); + if (!$parsedSvg || !isSvg($parsedSvg)) return console.warn(`${path} is not a valid SVGGeometryElement`); + return $parsedSvg; + }; + + + + // Motion path animation + + /** + * @param {SVGGeometryElement} $path + * @param {Number} totalLength + * @param {Number} progress + * @param {Number} lookup + * @param {Boolean} shouldClamp + * @return {DOMPoint} + */ + const getPathPoint = ($path, totalLength, progress, lookup, shouldClamp) => { + const point = progress + lookup; + const pointOnPath = shouldClamp + ? Math.max(0, Math.min(point, totalLength)) // Clamp between 0 and totalLength + : (point % totalLength + totalLength) % totalLength; // Wrap around + return $path.getPointAtLength(pointOnPath); + }; + + /** + * @param {SVGGeometryElement} $path + * @param {String} pathProperty + * @param {Number} [offset=0] + * @return {FunctionValue} + */ + const getPathProgess = ($path, pathProperty, offset = 0) => { + return $el => { + const totalLength = +($path.getTotalLength()); + const inSvg = $el[isSvgSymbol]; + const ctm = $path.getCTM(); + const shouldClamp = offset === 0; + /** @type {TweenObjectValue} */ + return { + from: 0, + to: totalLength, + /** @type {TweenModifier} */ + modifier: progress => { + const offsetLength = offset * totalLength; + const newProgress = progress + offsetLength; + if (pathProperty === 'a') { + const p0 = getPathPoint($path, totalLength, newProgress, -1, shouldClamp); + const p1 = getPathPoint($path, totalLength, newProgress, 1, shouldClamp); + return atan2(p1.y - p0.y, p1.x - p0.x) * 180 / PI; + } else { + const p = getPathPoint($path, totalLength, newProgress, 0, shouldClamp); + return pathProperty === 'x' ? + inSvg || !ctm ? p.x : p.x * ctm.a + p.y * ctm.c + ctm.e : + inSvg || !ctm ? p.y : p.x * ctm.b + p.y * ctm.d + ctm.f + } + } + } + } + }; + + /** + * @param {TargetsParam} path + * @param {Number} [offset=0] + */ + const createMotionPath = (path, offset = 0) => { + const $path = getPath(path); + if (!$path) return; + return { + translateX: getPathProgess($path, 'x', offset), + translateY: getPathProgess($path, 'y', offset), + rotate: getPathProgess($path, 'a', offset), + } + }; + + + + /** + * @param {SVGGeometryElement} [$el] + * @return {Number} + */ + const getScaleFactor = $el => { + let scaleFactor = 1; + if ($el && $el.getCTM) { + const ctm = $el.getCTM(); + if (ctm) { + const scaleX = sqrt(ctm.a * ctm.a + ctm.b * ctm.b); + const scaleY = sqrt(ctm.c * ctm.c + ctm.d * ctm.d); + scaleFactor = (scaleX + scaleY) / 2; + } + } + return scaleFactor; + }; + + /** + * Creates a proxy that wraps an SVGGeometryElement and adds drawing functionality. + * @param {SVGGeometryElement} $el - The SVG element to transform into a drawable + * @param {number} start - Starting position (0-1) + * @param {number} end - Ending position (0-1) + * @return {DrawableSVGGeometry} - Returns a proxy that preserves the original element's type with additional 'draw' attribute functionality + */ + const createDrawableProxy = ($el, start, end) => { + const pathLength = K; + const computedStyles = getComputedStyle($el); + const strokeLineCap = computedStyles.strokeLinecap; + // @ts-ignore + const $scalled = computedStyles.vectorEffect === 'non-scaling-stroke' ? $el : null; + let currentCap = strokeLineCap; + + const proxy = new Proxy($el, { + get(target, property) { + const value = target[property]; + if (property === proxyTargetSymbol) return target; + if (property === 'setAttribute') { + return (...args) => { + if (args[0] === 'draw') { + const value = args[1]; + const values = value.split(' '); + const v1 = +values[0]; + const v2 = +values[1]; + // TOTO: Benchmark if performing two slices is more performant than one split + // const spaceIndex = value.indexOf(' '); + // const v1 = round(+value.slice(0, spaceIndex), precision); + // const v2 = round(+value.slice(spaceIndex + 1), precision); + const scaleFactor = getScaleFactor($scalled); + const os = v1 * -pathLength * scaleFactor; + const d1 = (v2 * pathLength * scaleFactor) + os; + const d2 = (pathLength * scaleFactor + + ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10 * scaleFactor) - d1); + if (strokeLineCap !== 'butt') { + const newCap = v1 === v2 ? 'butt' : strokeLineCap; + if (currentCap !== newCap) { + target.style.strokeLinecap = `${newCap}`; + currentCap = newCap; + } + } + target.setAttribute('stroke-dashoffset', `${os}`); + target.setAttribute('stroke-dasharray', `${d1} ${d2}`); + } + return Reflect.apply(value, target, args); + }; + } + + if (isFnc(value)) { + return (...args) => Reflect.apply(value, target, args); + } else { + return value; + } + } + }); + + if ($el.getAttribute('pathLength') !== `${pathLength}`) { + $el.setAttribute('pathLength', `${pathLength}`); + proxy.setAttribute('draw', `${start} ${end}`); + } + + return /** @type {DrawableSVGGeometry} */(proxy); + }; + + /** + * Creates drawable proxies for multiple SVG elements. + * @param {TargetsParam} selector - CSS selector, SVG element, or array of elements and selectors + * @param {number} [start=0] - Starting position (0-1) + * @param {number} [end=0] - Ending position (0-1) + * @return {Array} - Array of proxied elements with drawing functionality + */ + const createDrawable = (selector, start = 0, end = 0) => { + const els = parseTargets(selector); + return els.map($el => createDrawableProxy( + /** @type {SVGGeometryElement} */($el), + start, + end + )); + }; + + + + /** + * @param {TargetsParam} path2 + * @param {Number} [precision] + * @return {FunctionValue} + */ + const morphTo = (path2, precision = .33) => ($path1, index, total, prevTween) => { + const tagName1 = ($path1.tagName || '').toLowerCase(); + if (!tagName1.match(/^(path|polygon|polyline)$/)) { + throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use , or .`); + } + const $path2 = /** @type {SVGGeometryElement} */(getPath(path2)); + if (!$path2) { + throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing , or SVG element."); + } + const tagName2 = ($path2.tagName || '').toLowerCase(); + if (!tagName2.match(/^(path|polygon|polyline)$/)) { + throw new Error(`Can't morph a <${$path2.tagName}> SVG element. Use , or .`); + } + const isPath = $path1.tagName === 'path'; + const separator = isPath ? ' ' : ','; + const previousPoints = prevTween ? prevTween._value : null; + if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints); + + let v1 = '', v2 = ''; + + if (!precision) { + v1 = $path1.getAttribute(isPath ? 'd' : 'points'); + v2 = $path2.getAttribute(isPath ? 'd' : 'points'); + } else { + const length1 = /** @type {SVGGeometryElement} */($path1).getTotalLength(); + const length2 = $path2.getTotalLength(); + const maxPoints = Math.max(Math.ceil(length1 * precision), Math.ceil(length2 * precision)); + for (let i = 0; i < maxPoints; i++) { + const t = i / (maxPoints - 1); + const pointOnPath1 = /** @type {SVGGeometryElement} */($path1).getPointAtLength(length1 * t); + const pointOnPath2 = $path2.getPointAtLength(length2 * t); + const prefix = isPath ? (i === 0 ? 'M' : 'L') : ''; + v1 += prefix + round$1(pointOnPath1.x, 3) + separator + pointOnPath1.y + ' '; + v2 += prefix + round$1(pointOnPath2.x, 3) + separator + pointOnPath2.y + ' '; + } + } + + return [v1, v2]; + }; + + var index$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + createDrawable: createDrawable, + createMotionPath: createMotionPath, + morphTo: morphTo + }); + + + + const segmenter = (typeof Intl !== 'undefined') && Intl.Segmenter; + const valueRgx = /\{value\}/g; + const indexRgx = /\{i\}/g; + const whiteSpaceGroupRgx = /(\s+)/; + const whiteSpaceRgx = /^\s+$/; + const lineType = 'line'; + const wordType = 'word'; + const charType = 'char'; + const dataLine = `data-line`; + + /** + * @typedef {Object} Segment + * @property {String} segment + * @property {Boolean} [isWordLike] + */ + + /** + * @typedef {Object} Segmenter + * @property {function(String): Iterable} segment + */ + + /** @type {Segmenter} */ + let wordSegmenter = null; + /** @type {Segmenter} */ + let graphemeSegmenter = null; + let $splitTemplate = null; + + /** + * @param {Segment} seg + * @return {Boolean} + */ + const isSegmentWordLike = seg => { + return seg.isWordLike || + seg.segment === ' ' || // Consider spaces as words first, then handle them diffrently later + isNum(+seg.segment); // Safari doesn't considers numbers as words + }; + + /** + * @param {HTMLElement} $el + */ + const setAriaHidden = $el => $el.setAttribute('aria-hidden', 'true'); + + /** + * @param {DOMTarget} $el + * @param {String} type + * @return {Array} + */ + const getAllTopLevelElements = ($el, type) => [.../** @type {*} */($el.querySelectorAll(`[data-${type}]:not([data-${type}] [data-${type}])`))]; + + const debugColors = { line: '#00D672', word: '#FF4B4B', char: '#5A87FF' }; + + /** + * @param {HTMLElement} $el + */ + const filterEmptyElements = $el => { + if (!$el.childElementCount && !$el.textContent.trim()) { + const $parent = $el.parentElement; + $el.remove(); + if ($parent) filterEmptyElements($parent); + } + }; + + /** + * @param {HTMLElement} $el + * @param {Number} lineIndex + * @param {Set} bin + * @returns {Set} + */ + const filterLineElements = ($el, lineIndex, bin) => { + const dataLineAttr = $el.getAttribute(dataLine); + if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') { + bin.add($el); + // Also remove adjacent whitespace-only text nodes + const prev = $el.previousSibling; + const next = $el.nextSibling; + if (prev && prev.nodeType === 3 && whiteSpaceRgx.test(prev.textContent)) { + bin.add(prev); + } + if (next && next.nodeType === 3 && whiteSpaceRgx.test(next.textContent)) { + bin.add(next); + } + } + let i = $el.childElementCount; + while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin); + return bin; + }; + + /** + * @param {'line'|'word'|'char'} type + * @param {SplitTemplateParams} params + * @return {String} + */ + const generateTemplate = (type, params = {}) => { + let template = ``; + const classString = isStr(params.class) ? ` class="${params.class}"` : ''; + const cloneType = setValue(params.clone, false); + const wrapType = setValue(params.wrap, false); + const overflow = wrapType ? wrapType === true ? 'clip' : wrapType : cloneType ? 'clip' : false; + if (wrapType) template += ``; + template += ``; + if (cloneType) { + const left = cloneType === 'left' ? '-100%' : cloneType === 'right' ? '100%' : '0'; + const top = cloneType === 'top' ? '-100%' : cloneType === 'bottom' ? '100%' : '0'; + template += `{value}`; + template += `{value}`; + } else { + template += `{value}`; + } + template += ``; + if (wrapType) template += ``; + return template; + }; + + /** + * @param {String|SplitFunctionValue} htmlTemplate + * @param {Array} store + * @param {Node|HTMLElement} node + * @param {DocumentFragment} $parentFragment + * @param {'line'|'word'|'char'} type + * @param {Boolean} debug + * @param {Number} lineIndex + * @param {Number} [wordIndex] + * @param {Number} [charIndex] + * @return {HTMLElement} + */ + const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, debug, lineIndex, wordIndex, charIndex) => { + const isLine = type === lineType; + const isChar = type === charType; + const className = `_${type}_`; + const template = isFnc(htmlTemplate) ? htmlTemplate(node) : htmlTemplate; + const displayStyle = isLine ? 'block' : 'inline-block'; + $splitTemplate.innerHTML = template + .replace(valueRgx, ``) + .replace(indexRgx, `${isChar ? charIndex : isLine ? lineIndex : wordIndex}`); + const $content = $splitTemplate.content; + const $highestParent = /** @type {HTMLElement} */($content.firstElementChild); + const $split = /** @type {HTMLElement} */($content.querySelector(`[data-${type}]`)) || $highestParent; + const $replacables = /** @type {NodeListOf} */($content.querySelectorAll(`i.${className}`)); + const replacablesLength = $replacables.length; + if (replacablesLength) { + $highestParent.style.display = displayStyle; + $split.style.display = displayStyle; + $split.setAttribute(dataLine, `${lineIndex}`); + if (!isLine) { + $split.setAttribute('data-word', `${wordIndex}`); + if (isChar) $split.setAttribute('data-char', `${charIndex}`); + } + let i = replacablesLength; + while (i--) { + const $replace = $replacables[i]; + const $closestParent = $replace.parentElement; + $closestParent.style.display = displayStyle; + if (isLine) { + $closestParent.innerHTML = /** @type {HTMLElement} */(node).innerHTML; + } else { + $closestParent.replaceChild(node.cloneNode(true), $replace); + } + } + store.push($split); + $parentFragment.appendChild($content); + } else { + console.warn(`The expression "{value}" is missing from the provided template.`); + } + if (debug) $highestParent.style.outline = `1px dotted ${debugColors[type]}`; + return $highestParent; + }; + + /** + * A class that splits text into words and wraps them in span elements while preserving the original HTML structure. + * @class + */ + class TextSplitter { + /** + * @param {Element|NodeList|String|Array} target + * @param {TextSplitterParams} [parameters] + */ + constructor(target, parameters = {}) { + // Only init segmenters when needed + if (!wordSegmenter) wordSegmenter = segmenter ? new segmenter([], { granularity: wordType }) : { + segment: (text) => { + const segments = []; + const words = text.split(whiteSpaceGroupRgx); + for (let i = 0, l = words.length; i < l; i++) { + const segment = words[i]; + segments.push({ + segment, + isWordLike: !whiteSpaceRgx.test(segment), // Consider non-whitespace as word-like + }); + } + return segments; + } + }; + if (!graphemeSegmenter) graphemeSegmenter = segmenter ? new segmenter([], { granularity: 'grapheme' }) : { + segment: text => [...text].map(char => ({ segment: char })) + }; + if (!$splitTemplate && isBrowser) $splitTemplate = doc.createElement('template'); + if (scope.current) scope.current.register(this); + const { words, chars, lines, accessible, includeSpaces, debug } = parameters; + const $target = /** @type {HTMLElement} */((target = isArr(target) ? target[0] : target) && /** @type {Node} */(target).nodeType ? target : (getNodeList(target) || [])[0]); + const lineParams = lines === true ? {} : lines; + const wordParams = words === true || isUnd(words) ? {} : words; + const charParams = chars === true ? {} : chars; + this.debug = setValue(debug, false); + this.includeSpaces = setValue(includeSpaces, false); + this.accessible = setValue(accessible, true); + this.linesOnly = lineParams && (!wordParams && !charParams); + /** @type {String|false|SplitFunctionValue} */ + this.lineTemplate = isObj(lineParams) ? generateTemplate(lineType, /** @type {SplitTemplateParams} */(lineParams)) : lineParams; + /** @type {String|false|SplitFunctionValue} */ + this.wordTemplate = isObj(wordParams) || this.linesOnly ? generateTemplate(wordType, /** @type {SplitTemplateParams} */(wordParams)) : wordParams; + /** @type {String|false|SplitFunctionValue} */ + this.charTemplate = isObj(charParams) ? generateTemplate(charType, /** @type {SplitTemplateParams} */(charParams)) : charParams; + this.$target = $target; + this.html = $target && $target.innerHTML; + this.lines = []; + this.words = []; + this.chars = []; + this.effects = []; + this.effectsCleanups = []; + this.cache = null; + this.ready = false; + this.width = 0; + this.resizeTimeout = null; + const handleSplit = () => this.html && (lineParams || wordParams || charParams) && this.split(); + // Make sure this is declared before calling handleSplit() in case revert() is called inside an effect callback + this.resizeObserver = new ResizeObserver(() => { + // Use a setTimeout instead of a Timer for better tree shaking + clearTimeout(this.resizeTimeout); + this.resizeTimeout = setTimeout(() => { + const currentWidth = /** @type {HTMLElement} */($target).offsetWidth; + if (currentWidth === this.width) return; + this.width = currentWidth; + handleSplit(); + }, 150); + }); + // Only declare the font ready promise when splitting by lines and not alreay split + if (this.lineTemplate && !this.ready) { + doc.fonts.ready.then(handleSplit); + } else { + handleSplit(); + } + $target ? this.resizeObserver.observe($target) : console.warn('No Text Splitter target found.'); + } + + /** + * @param {(...args: any[]) => Tickable | (() => void) | void} effect + * @return this + */ + addEffect(effect) { + if (!isFnc(effect)) { console.warn('Effect must return a function.'); return this; } + const refreshableEffect = keepTime(effect); + this.effects.push(refreshableEffect); + if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this); + return this; + } + + revert() { + clearTimeout(this.resizeTimeout); + this.lines.length = this.words.length = this.chars.length = 0; + this.resizeObserver.disconnect(); + // Make sure to revert the effects after disconnecting the resizeObserver to avoid triggering it in the process + this.effectsCleanups.forEach(cleanup => isFnc(cleanup) ? cleanup(this) : cleanup.revert && cleanup.revert()); + this.$target.innerHTML = this.html; + return this; + } + + /** + * Recursively processes a node and its children + * @param {Node} node + */ + splitNode(node) { + const wordTemplate = this.wordTemplate; + const charTemplate = this.charTemplate; + const includeSpaces = this.includeSpaces; + const debug = this.debug; + const nodeType = node.nodeType; + if (nodeType === 3) { + const nodeText = node.nodeValue; + // If the nodeText is only whitespace, leave it as is + if (nodeText.trim()) { + const tempWords = []; + const words = this.words; + const chars = this.chars; + const wordSegments = wordSegmenter.segment(nodeText); + const $wordsFragment = doc.createDocumentFragment(); + let prevSeg = null; + for (const wordSegment of wordSegments) { + const segment = wordSegment.segment; + const isWordLike = isSegmentWordLike(wordSegment); + // Determine if this segment should be a new word, first segment always becomes a new word + if (!prevSeg || (isWordLike && (prevSeg && (isSegmentWordLike(prevSeg))))) { + tempWords.push(segment); } else { - keyframes[name] = parsedPropertyValue; + // Only concatenate if both current and previous are non-word-like and don't contain spaces + const lastWordIndex = tempWords.length - 1; + const lastWord = tempWords[lastWordIndex]; + if (!whiteSpaceGroupRgx.test(lastWord) && !whiteSpaceGroupRgx.test(segment)) { + tempWords[lastWordIndex] += segment; + } else { + tempWords.push(segment); + } + } + prevSeg = wordSegment; + } + + for (let i = 0, l = tempWords.length; i < l; i++) { + const word = tempWords[i]; + if (!word.trim()) { + // Preserve whitespace only if includeSpaces is false and if the current space is not the first node + if (i && includeSpaces) continue; + $wordsFragment.appendChild(doc.createTextNode(word)); + } else { + const nextWord = tempWords[i + 1]; + const hasWordFollowingSpace = includeSpaces && nextWord && !nextWord.trim(); + const wordToProcess = word; + const charSegments = charTemplate ? graphemeSegmenter.segment(wordToProcess) : null; + const $charsFragment = charTemplate ? doc.createDocumentFragment() : doc.createTextNode(hasWordFollowingSpace ? word + '\xa0' : word); + if (charTemplate) { + const charSegmentsArray = [...charSegments]; + for (let j = 0, jl = charSegmentsArray.length; j < jl; j++) { + const charSegment = charSegmentsArray[j]; + const isLastChar = j === jl - 1; + // If this is the last character and includeSpaces is true with a following space, append the space + const charText = isLastChar && hasWordFollowingSpace ? charSegment.segment + '\xa0' : charSegment.segment; + const $charNode = doc.createTextNode(charText); + processHTMLTemplate(charTemplate, chars, $charNode, /** @type {DocumentFragment} */($charsFragment), charType, debug, -1, words.length, chars.length); + } + } + if (wordTemplate) { + processHTMLTemplate(wordTemplate, words, $charsFragment, $wordsFragment, wordType, debug, -1, words.length, chars.length); + // Chars elements must be re-parsed in the split() method if both words and chars are parsed + } else if (charTemplate) { + $wordsFragment.appendChild($charsFragment); + } else { + $wordsFragment.appendChild(doc.createTextNode(word)); + } + // Skip the next iteration if we included a space + if (hasWordFollowingSpace) i++; + } + } + node.parentNode.replaceChild($wordsFragment, node); + } + } else if (nodeType === 1) { + // Converting to an array is necessary to work around childNodes pottential mutation + const childNodes = /** @type {Array} */([.../** @type {*} */(node.childNodes)]); + for (let i = 0, l = childNodes.length; i < l; i++) this.splitNode(childNodes[i]); + } + } + + /** + * @param {Boolean} clearCache + * @return {this} + */ + split(clearCache = false) { + const $el = this.$target; + const isCached = !!this.cache && !clearCache; + const lineTemplate = this.lineTemplate; + const wordTemplate = this.wordTemplate; + const charTemplate = this.charTemplate; + const fontsReady = doc.fonts.status !== 'loading'; + const canSplitLines = lineTemplate && fontsReady; + this.ready = !lineTemplate || fontsReady; + if (canSplitLines || clearCache) { + // No need to revert effects animations here since it's already taken care by the refreshable + this.effectsCleanups.forEach(cleanup => isFnc(cleanup) && cleanup(this)); + } + if (!isCached) { + if (clearCache) { + $el.innerHTML = this.html; + this.words.length = this.chars.length = 0; + } + this.splitNode($el); + this.cache = $el.innerHTML; + } + if (canSplitLines) { + if (isCached) $el.innerHTML = this.cache; + this.lines.length = 0; + if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); + } + // Always reparse characters after a line reset or if both words and chars are activated + if (charTemplate && (canSplitLines || wordTemplate)) { + this.chars = getAllTopLevelElements($el, charType); + } + // Words are used when lines only and prioritized over chars + const elementsArray = this.words.length ? this.words : this.chars; + let y, linesCount = 0; + for (let i = 0, l = elementsArray.length; i < l; i++) { + const $el = elementsArray[i]; + const { top, height } = $el.getBoundingClientRect(); + if (!isUnd(y) && top - y > height * .5) linesCount++; + $el.setAttribute(dataLine, `${linesCount}`); + const nested = $el.querySelectorAll(`[${dataLine}]`); + let c = nested.length; + while (c--) nested[c].setAttribute(dataLine, `${linesCount}`); + y = top; + } + if (canSplitLines) { + const linesFragment = doc.createDocumentFragment(); + const parents = new Set(); + const clones = []; + for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) { + const $clone = /** @type {HTMLElement} */($el.cloneNode(true)); + filterLineElements($clone, lineIndex, new Set()).forEach($el => { + const $parent = $el.parentNode; + if ($parent) { + if ($el.nodeType === 1) parents.add(/** @type {HTMLElement} */($parent)); + $parent.removeChild($el); } - addWAAPIAnimation(this, $el, name, keyframes, tweenParams); - } + }); + clones.push($clone); } - if (hasIndividualTransforms) { - let transforms = emptyString; - for (let t in cachedTransforms) { - transforms += `${transformsFragmentStrings[t]}var(--${t})) `; - } - elStyle.transform = transforms; + parents.forEach(filterEmptyElements); + for (let cloneIndex = 0, clonesLength = clones.length; cloneIndex < clonesLength; cloneIndex++) { + processHTMLTemplate(lineTemplate, this.lines, clones[cloneIndex], linesFragment, lineType, this.debug, cloneIndex); } - }); - - if (scroll) { - /** @type {ScrollObserver} */(this.autoplay).link(this); + $el.innerHTML = ''; + $el.appendChild(linesFragment); + if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); + if (charTemplate) this.chars = getAllTopLevelElements($el, charType); } - } - - /** - * @callback forEachCallback - * @param {globalThis.Animation} animation - */ - - /** - * @param {forEachCallback|String} callback - * @return {this} - */ - forEach(callback) { - const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; - this.animations.forEach(cb); - return this; - } - - get speed() { - return this._speed; - } - - set speed(speed) { - this._speed = +speed; - this.forEach(anim => anim.playbackRate = speed); - } - - get currentTime() { - const controlAnimation = this.controlAnimation; - const timeScale = globals.timeScale; - return this.completed ? this.duration : controlAnimation ? +controlAnimation.currentTime * (timeScale === 1 ? 1 : timeScale) : 0; - } - - set currentTime(time) { - const t = time * (globals.timeScale === 1 ? 1 : K); - this.forEach(anim => { - // Make sure the animation playState is not 'paused' in order to properly trigger an onfinish callback. - // The "paused" play state supersedes the "finished" play state; if the animation is both paused and finished, the "paused" state is the one that will be reported. - // https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish_event - // This is not needed for persisting animations since they never finish. - if (!this.persist && t >= this.duration) anim.play(); - anim.currentTime = t; - }); - } - - get progress() { - return this.currentTime / this.duration; - } - - set progress(progress) { - this.forEach(anim => anim.currentTime = progress * this.duration || 0); - } - - resume() { - if (!this.paused) return this; - this.paused = false; - // TODO: Store the current time, and seek back to the last position - return this.forEach('play'); - } - pause() { - if (this.paused) return this; - this.paused = true; - return this.forEach('pause'); - } - - alternate() { - this.reversed = !this.reversed; - this.forEach('reverse'); - if (this.paused) this.forEach('pause'); + // Remove the word wrappers and clear the words array if lines split only + if (this.linesOnly) { + const words = this.words; + let w = words.length; + while (w--) { + const $word = words[w]; + $word.replaceWith($word.textContent); + } + words.length = 0; + } + if (this.accessible && (canSplitLines || !isCached)) { + const $accessible = doc.createElement('span'); + // Make the accessible element visually-hidden (https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html) + $accessible.style.cssText = `position:absolute;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);width:1px;height:1px;white-space:nowrap;`; + // $accessible.setAttribute('tabindex', '-1'); + $accessible.innerHTML = this.html; + $el.insertBefore($accessible, $el.firstChild); + this.lines.forEach(setAriaHidden); + this.words.forEach(setAriaHidden); + this.chars.forEach(setAriaHidden); + } + this.width = /** @type {HTMLElement} */($el).offsetWidth; + if (canSplitLines || clearCache) { + this.effects.forEach((effect, i) => this.effectsCleanups[i] = effect(this)); + } return this; } - play() { - if (this.reversed) this.alternate(); - return this.resume(); + refresh() { + this.split(true); } + } - reverse() { - if (!this.reversed) this.alternate(); - return this.resume(); - } + /** + * @param {Element|NodeList|String|Array} target + * @param {TextSplitterParams} [parameters] + * @return {TextSplitter} + */ + const splitText = (target, parameters) => new TextSplitter(target, parameters); - /** - * @param {Number} time - * @param {Boolean} muteCallbacks - */ - seek(time, muteCallbacks = false) { - if (muteCallbacks) this.muteCallbacks = true; - if (time < this.duration) this.completed = false; - this.currentTime = time; - this.muteCallbacks = false; - if (this.paused) this.pause(); - return this; - } + /** + * @deprecated text.split() is deprecated, import splitText() directly, or text.splitText() + * + * @param {HTMLElement|NodeList|String|Array} target + * @param {TextSplitterParams} [parameters] + * @return {TextSplitter} + */ + const split = (target, parameters) => { + console.warn('text.split() is deprecated, import splitText() directly, or text.splitText()'); + return new TextSplitter(target, parameters); + }; - restart() { - this.completed = false; - return this.seek(0, true).resume(); - } + - commitStyles() { - return this.forEach('commitStyles'); + /** + * '-' is the range operator; place it at the start or end of the string to use it as a literal (e.g. '-abc' or 'abc-') + * @param {String} str + * @return {String} + */ + const expandCharRanges = (str) => { + let result = ''; + for (let i = 0, l = str.length; i < l; i++) { + if (i + 2 < l && str[i + 1] === '-' && str.charCodeAt(i) < str.charCodeAt(i + 2)) { + const start = str.charCodeAt(i); + const end = str.charCodeAt(i + 2); + for (let c = start; c <= end; c++) result += String.fromCharCode(c); + i += 2; + } else { + result += str[i]; + } } + return result; + }; - complete() { - return this.seek(this.duration); - } + const charSets = { + lowercase: 'a-z', + uppercase: 'A-Z', + numbers: '0-9', + symbols: '!%#_|*+=', + braille: 'â €-⣿', + blocks: 'â–€-â–Ÿ', + shades: 'â–‘-â–“', + }; - cancel() { - this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise - return this.commitStyles().forEach('cancel'); - } + const originalTexts = new WeakMap(); - revert() { - // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted, - // This means if you have multiple animations animating different transforms on the same target, - // reverting one of them will also override the transform property of the other animations. - // A better approach would be to store the original custom property values is they exist instead of the entire transform value, - // and update the CSS variables with the orignal value - this.cancel().targets.forEach(($el, i) => { - const targetStyle = $el.style; - const targetInlineStyles = this._inlineStyles[i]; - for (let name in targetInlineStyles) { - const originalInlinedValue = targetInlineStyles[name]; - if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) { - targetStyle.removeProperty(toLowerCase(name)); - } else { - targetStyle[name] = originalInlinedValue; + /** + * Returns a function-based tween value that scrambles the target's text content, + * progressively revealing the original text. + * + * @param {ScrambleTextParams} [params] + * @return {FunctionValue} + */ + const scrambleText = (params = {}) => { + if (!params) params = {}; + const charsParam = params.chars; + const easeFn = parseEase(params.ease || 'linear'); + const text = params.text; + const fromParam = params.from; + const reversed = params.reversed || false; + const perturbation = params.perturbation || 0; + const cursorParam = params.cursor; + const cursorChars = cursorParam === true ? '_' + : typeof cursorParam === 'number' ? String.fromCharCode(cursorParam) + : typeof cursorParam === 'string' ? cursorParam + : ''; + const cursorLen = cursorChars.length; + const seed = params.seed || 0; + const override = params.override !== undefined ? params.override : true; + const revealRate = params.revealRate || 60; + const interval = 1000 * globals.timeScale / revealRate; + const settleDuration = params.settleDuration || 300 * globals.timeScale; + const settleRate = params.settleRate || 30; + const durationParam = params.duration; + const revealDelayParam = params.revealDelay; + const delayParam = params.delay; + const onChange = params.onChange || noop; + + return (target, index, targets, prevTween) => { + const rawChars = typeof charsParam === 'function' ? charsParam(target, index, targets) : (charsParam || 'a-zA-Z0-9!%#_'); + const characters = expandCharRanges(charSets[rawChars] || rawChars); + const totalChars = characters.length - 1; + const duration = typeof durationParam === 'function' ? durationParam(target, index, targets) : durationParam; + const revealDelay = typeof revealDelayParam === 'function' ? revealDelayParam(target, index, targets) : (revealDelayParam || 0); + const delay = typeof delayParam === 'function' ? delayParam(target, index, targets) : (delayParam || 0); + const rng = seed ? createSeededRandom(seed) : createSeededRandom(); + if (!originalTexts.has(target)) originalTexts.set(target, target.textContent); + const startingText = prevTween ? prevTween._value : target.textContent; + const targetText = text !== undefined + ? (typeof text === 'function' ? text(target, index, targets) : text) + : prevTween ? prevTween._value + : originalTexts.get(target); + const settledText = targetText === ' ' || targetText === ' ' ? ' ' : targetText; + const startLength = startingText === ' ' ? 0 : startingText.length; + const endLength = settledText.length; + const overrideChars = override === true ? characters + : typeof override === 'string' && override.length > 0 ? expandCharRanges(charSets[/** @type {String} */(override)] || /** @type {String} */(override)) + : null; + const totalOverrideChars = overrideChars ? overrideChars.length - 1 : 0; + // Space override uses   so the browser doesn't collapse consecutive spaces in innerHTML + const overrideChar = override === ' ' ? ' ' : null; + // When starting from blank, only animate the target text length to avoid padding beyond it + const animLength = override === '' ? endLength : Math.max(startLength, endLength); + // Compute total duration from interval spacing and settle time, or use the explicit duration + const animDuration = duration > 0 ? duration : (animLength - 1) * interval + settleDuration; + const computedDuration = round$1((animDuration + revealDelay) / globals.timeScale, 0) * globals.timeScale; + const revealDelayRatio = revealDelay > 0 ? round$1(revealDelay / computedDuration, 12) : 0; + // Auto-resolve reveal direction: shrinking text reveals from right, growing from left + const resolvedFrom = fromParam === undefined || fromParam === 'auto' ? (endLength < startLength ? 'right' : 'left') : fromParam; + const charOrder = new Int32Array(animLength); + if (resolvedFrom === 'random') { + for (let i = 0; i < animLength; i++) charOrder[i] = i; + for (let i = animLength - 1; i > 0; i--) { + const j = rng(0, i); + const t = charOrder[i]; charOrder[i] = charOrder[j]; charOrder[j] = t; + } + } else { + const ref = resolvedFrom === 'right' ? (override === '' || !startLength ? animLength : startLength) - 1 + : resolvedFrom === 'center' ? ((override === '' || !startLength ? animLength : startLength) - 1) / 2 + : typeof resolvedFrom === 'number' ? resolvedFrom + : 0; + const abs = Math.abs; + const indices = new Array(animLength); + for (let i = 0; i < animLength; i++) indices[i] = i; + indices.sort((a, b) => abs(a - ref) - abs(b - ref)); + for (let i = 0; i < animLength; i++) charOrder[indices[i]] = i; + } + if (reversed) { + const last = animLength - 1; + for (let i = 0; i < animLength; i++) charOrder[i] = last - charOrder[i]; + } + // settleRatio is the fraction of the animation each character spends in the active scrambling zone + const settleRatio = round$1(settleDuration / animDuration, 12); + // settleSpacing is the time gap between consecutive characters entering the active zone + const settleSpacing = round$1((1 - settleRatio) / animLength, 12); + const cursorZone = cursorLen * settleSpacing; + // stepRatio controls how often scramble characters refresh (based on settleRate) + const stepRatio = round$1(1000 * globals.timeScale / (settleRate * computedDuration), 12); + // Pre-compute per-character start and settle times + const charStarts = new Float32Array(animLength); + const charEnds = new Float32Array(animLength); + const scale = perturbation > 0 ? perturbation * settleRatio : 0; + for (let c = 0; c < animLength; c++) { + const so = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + const eo = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + charStarts[c] = charOrder[c] * settleSpacing + so; + charEnds[c] = Math.ceil((charStarts[c] + settleRatio + eo) / stepRatio) * stepRatio; + } + // When text shrinks with non-sequential from modes, delay target settle times past all extras + if (endLength < animLength && resolvedFrom !== 'left' && resolvedFrom !== 'right' && resolvedFrom !== 'random') { + let maxExtraEnd = 0; + for (let c = endLength; c < animLength; c++) { + if (charEnds[c] > maxExtraEnd) maxExtraEnd = charEnds[c]; + } + const targets = new Array(endLength); + for (let c = 0; c < endLength; c++) targets[c] = c; + targets.sort((a, b) => charOrder[a] - charOrder[b]); + const targetSpacing = (1 - maxExtraEnd) / endLength; + for (let i = 0; i < endLength; i++) { + const revealTime = maxExtraEnd + i * targetSpacing; + if (revealTime > charEnds[targets[i]]) { + charEnds[targets[i]] = revealTime; } } - // Remove style attribute if empty - if ($el.getAttribute('style') === emptyString) $el.removeAttribute('style'); - }); - return this; - } + } + // charCache holds the current scramble character for each position, refreshed at settleRate + const charCache = new Array(animLength); + for (let c = 0; c < animLength; c++) { + charCache[c] = characters[rng(0, totalChars)]; + } + // overrideCache holds scramble characters for the starting text (override: true or custom string) + const overrideCache = overrideChars ? (overrideChars === characters ? charCache : new Array(animLength)) : null; + if (overrideCache && overrideCache !== charCache) { + for (let c = 0; c < animLength; c++) { + overrideCache[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, overrideChars.length - 1)]; + } + } + // Build the initial display text based on override mode + let fillStartText = startingText; + if (!prevTween) { + if (override === '') { + fillStartText = ''; + } else if (overrideChars) { + fillStartText = ''; + for (let c = 0; c < startLength; c++) { + fillStartText += startingText[c] === ' ' ? ' ' : /** @type {Array} */(overrideCache)[c]; + } + } + } - /** - * @typedef {this & {then: null}} ResolvedWAAPIAnimation - */ + let lastValue = -1; + let lastStep = -1; + let scrambled = ''; + const hasOverride = override !== ''; + const hasOverrideChars = !!overrideChars; + const hasCursor = cursorLen > 0; - /** - * @param {Callback} [callback] - * @return Promise - */ - then(callback = noop) { - const then = this.then; - const onResolve = () => { - this.then = null; - callback(/** @type {ResolvedWAAPIAnimation} */(this)); - this.then = then; - this._resolve = noop; - }; - return new Promise(r => { - this._resolve = () => r(onResolve()); - if (this.completed) this._resolve(); - return this; - }); + return { + from: 0, + to: 1, + duration: computedDuration, + delay: delay, + ease: 'linear', + modifier: (v) => { + if (v === lastValue) return scrambled; + lastValue = v; + if (delay > 0 && v <= 0) { scrambled = startingText; return startingText; } + if (v <= 0) { scrambled = fillStartText; return fillStartText; } + if (v >= 1) { scrambled = settledText; return settledText; } + scrambled = ''; + // Only refresh scramble characters when we cross a settleRate step boundary + const currentStep = (v / stepRatio) | 0; + const refreshChars = currentStep !== lastStep; + if (refreshChars) lastStep = currentStep; + // Subtract delay ratio to get the effective animation progress + const linear = revealDelayRatio > 0 ? (v - revealDelayRatio) / (1 - revealDelayRatio) : v; + const t = linear > 0 ? easeFn(linear) : 0; + for (let c = 0; c < animLength; c++) { + // Each character has its own start/end window based on its reveal order + const charStart = charStarts[c]; + const charEnd = charEnds[c]; + // Settled zone: character has finished its transition + if (t >= charEnd) { + if (c < endLength) scrambled += settledText[c]; + continue; + } + // Pre-transition zone: reveal wave hasn't reached this character yet + if (t <= 0 || t < charStart) { + if (hasOverride && c < startLength) { + if (hasOverrideChars) { + if (startingText[c] === ' ') { + scrambled += ' '; + } else { + if (refreshChars) /** @type {Array} */(overrideCache)[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, totalOverrideChars)]; + scrambled += /** @type {Array} */(overrideCache)[c]; + } + } else { + // Default (override: false): show the original starting text + scrambled += startingText[c]; + } + } + continue; + } + // Active zone: character is between charStart and charEnd + const isSpace = (c < endLength && settledText[c] === ' ') || (c < startLength && startingText[c] === ' '); + if (isSpace) { + scrambled += ' '; + } else if (hasCursor && t - charStart < cursorZone) { + // Cursor sub-zone: show cursor character based on position within cursor width + scrambled += cursorChars[cursorLen - 1 - (((t - charStart) / settleSpacing) | 0)]; + } else { + // Scramble zone: show cycling random characters + if (refreshChars) charCache[c] = characters[rng(0, totalChars)]; + scrambled += charCache[c]; + } + } + if (refreshChars) onChange(scrambled, t); + return scrambled; + } + } } - } - - const waapi = { - /** - * @param {DOMTargetsParam} targets - * @param {WAAPIAnimationParams} params - * @return {WAAPIAnimation} - */ - animate: (targets, params) => new WAAPIAnimation(targets, params), - convertEase: easingToLinear }; + var index = /*#__PURE__*/Object.freeze({ + __proto__: null, + TextSplitter: TextSplitter, + scrambleText: scrambleText, + split: split, + splitText: splitText + }); + exports.$ = registerTargets; exports.Animatable = Animatable; + exports.AutoLayout = AutoLayout; exports.Draggable = Draggable; exports.JSAnimation = JSAnimation; exports.Scope = Scope; @@ -8811,12 +11027,14 @@ exports.Timeline = Timeline; exports.Timer = Timer; exports.WAAPIAnimation = WAAPIAnimation; + exports.addChild = addChild; exports.animate = animate; exports.clamp = clamp; exports.cleanInlineStyles = cleanInlineStyles; exports.createAnimatable = createAnimatable; exports.createDraggable = createDraggable; exports.createDrawable = createDrawable; + exports.createLayout = createLayout; exports.createMotionPath = createMotionPath; exports.createScope = createScope; exports.createSeededRandom = createSeededRandom; @@ -8829,7 +11047,9 @@ exports.eases = eases; exports.easings = index$3; exports.engine = engine; + exports.forEachChildren = forEachChildren; exports.get = get; + exports.globals = globals; exports.irregular = irregular; exports.keepTime = keepTime; exports.lerp = lerp; @@ -8843,8 +11063,10 @@ exports.random = random; exports.randomPick = randomPick; exports.remove = remove; + exports.removeChild = removeChild; exports.round = round; exports.roundPad = roundPad; + exports.scrambleText = scrambleText; exports.scrollContainers = scrollContainers; exports.set = set; exports.shuffle = shuffle; diff --git a/dist/bundles/anime.umd.min.js b/dist/bundles/anime.umd.min.js index 904bcc5b4..d4fcde89d 100644 --- a/dist/bundles/anime.umd.min.js +++ b/dist/bundles/anime.umd.min.js @@ -1,7 +1,7 @@ /** * Anime.js - UMD minified bundle - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).anime={})}(this,function(t){"use strict";const e="undefined"!=typeof window,s=e?window:null,i=e?document:null,r={replace:0,none:1,blend:2},n=Symbol(),o=Symbol(),a=Symbol(),h=Symbol(),l=Symbol(),c=Symbol(),d=1e-11,u=1e12,p=1e3,m="",f=(()=>{const t=new Map;return t.set("x","translateX"),t.set("y","translateY"),t.set("z","translateZ"),t})(),g=["translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY","matrix","matrix3d","perspective"],y=g.reduce((t,e)=>({...t,[e]:e+"("}),{}),_=()=>{},v=/(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i,b=/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i,S=/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,w=/hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i,T=/hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,x=/[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi,k=/^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i,$=/([a-z])([A-Z])/g,C=/(\w+)(\([^)]+\)+)/g,E=/(\*=|\+=|-=)/,D=/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/,B={id:null,keyframes:null,playbackEase:null,playbackRate:1,frameRate:120,loop:0,reversed:!1,alternate:!1,autoplay:!0,persist:!1,duration:p,delay:0,loopDelay:0,ease:"out(2)",composition:r.replace,modifier:t=>t,onBegin:_,onBeforeUpdate:_,onUpdate:_,onLoop:_,onPause:_,onComplete:_,onRender:_},L={current:null,root:i},N={defaults:B,precision:4,timeScale:1,tickThreshold:200},A={version:"4.2.2",engine:null};e&&(s.AnimeJS||(s.AnimeJS=[]),s.AnimeJS.push(A));const F=t=>t.replace($,"$1-$2").toLowerCase(),P=(t,e)=>0===t.indexOf(e),O=Date.now,R=Array.isArray,M=t=>t&&t.constructor===Object,z=t=>"number"==typeof t&&!isNaN(t),I=t=>"string"==typeof t,Y=t=>"function"==typeof t,X=t=>void 0===t,V=t=>X(t)||null===t,W=t=>e&&t instanceof SVGElement,H=t=>v.test(t),U=t=>P(t,"rgb"),q=t=>P(t,"hsl"),j=t=>!N.defaults.hasOwnProperty(t),G=["opacity","rotate","overflow","color"],Q=t=>I(t)?parseFloat(t):t,Z=Math.pow,J=Math.sqrt,K=Math.sin,tt=Math.cos,et=Math.abs,st=Math.exp,it=Math.ceil,rt=Math.floor,nt=Math.asin,ot=Math.max,at=Math.atan2,ht=Math.PI,lt=Math.round,ct=(t,e,s)=>ts?s:t,dt={},ut=(t,e)=>{if(e<0)return t;if(!e)return lt(t);let s=dt[e];return s||(s=dt[e]=10**e),lt(t*s)/s},pt=(t,e)=>R(e)?e.reduce((e,s)=>et(s-t)t+(e-t)*s,ft=t=>t===1/0?u:t===-1/0?-u:t,gt=t=>t<=d?d:ft(ut(t,11)),yt=t=>R(t)?[...t]:t,_t=(t,e)=>{const s={...t};for(let i in e){const r=t[i];s[i]=X(r)?e[i]:r}return s},vt=(t,e,s,i="_prev",r="_next")=>{let n=t._head,o=r;for(s&&(n=t._tail,o=i);n;){const t=n[o];e(n),n=t}},bt=(t,e,s="_prev",i="_next")=>{const r=e[s],n=e[i];r?r[i]=n:t._head=n,n?n[s]=r:t._tail=r,e[s]=null,e[i]=null},St=(t,e,s,i="_prev",r="_next")=>{let n=t._tail;for(;n&&s&&s(n,e);)n=n[i];const o=n?n[r]:t._head;n?n[r]=e:t._head=e,o?o[i]=e:t._tail=e,e[i]=n,e[r]=o},wt=(t,e,s)=>(s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+(e-t)*(2/3-s)*6:t),Tt=(t,e)=>X(t)?e:t,xt=(t,e,s,i,r)=>{let n;if(Y(t))n=()=>{const r=t(e,s,i);return isNaN(+r)?r||0:+r};else{if(!I(t)||!P(t,"var("))return t;n=()=>{const s=t.match(D),i=s[1],r=s[2];let n=getComputedStyle(e)?.getPropertyValue(i);return n&&n.trim()!==m||!r||(n=r.trim()),n||0}}return r&&(r.func=n),n()},kt=(t,e)=>t[o]?t[a]&&((t,e)=>{if(G.includes(e))return!1;if(t.getAttribute(e)||e in t){if("scale"===e){const e=t.parentNode;return e&&"filter"===e.tagName}return!0}})(t,e)?1:g.includes(e)||f.get(e)?3:P(e,"--")?4:e in t.style?2:e in t?0:1:0,$t=(t,e,s)=>{const i=t.style[e];i&&s&&(s[e]=i);const r=i||getComputedStyle(t[c]||t).getPropertyValue(e);return"auto"===r?"0":r},Ct=(t,e,s,i)=>{const r=X(s)?kt(t,e):s;return 0===r?t[e]||0:1===r?t.getAttribute(e):3===r?((t,e,s)=>{const i=t.style.transform;let r;if(i){const n=t[h];let o;for(;o=C.exec(i);){const t=o[1],i=o[2].slice(1,-1);n[t]=i,t===e&&(r=i,s&&(s[e]=i))}}return i&&!X(r)?r:P(e,"scale")?"1":P(e,"rotate")||P(e,"skew")?"0deg":"0px"})(t,e,i):4===r?$t(t,e,i).trimStart():$t(t,e,i)},Et=(t,e,s)=>"-"===s?t-e:"+"===s?t+e:t*e,Dt=(t,e)=>{if(e.t=0,e.n=0,e.u=null,e.o=null,e.d=null,e.s=null,!t)return e;const s=+t;if(isNaN(s)){let s=t;"="===s[1]&&(e.o=s[0],s=s.slice(2));const n=!s.includes(" ")&&k.exec(s);if(n)return e.t=1,e.n=+n[1],e.u=n[2],e;if(e.o)return e.n=+s,e;if(H(r=s)||U(r)||q(r))return e.t=2,e.d=U(i=s)?(t=>{const e=b.exec(t)||S.exec(t),s=X(e[4])?1:+e[4];return[+e[1],+e[2],+e[3],s]})(i):H(i)?(t=>{const e=t.length,s=4===e||5===e;return[+("0x"+t[1]+t[s?1:2]),+("0x"+t[s?2:3]+t[s?2:4]),+("0x"+t[s?3:5]+t[s?3:6]),5===e||9===e?+(+("0x"+t[s?4:7]+t[s?4:8])/255).toFixed(3):1]})(i):q(i)?(t=>{const e=w.exec(t)||T.exec(t),s=+e[1]/360,i=+e[2]/100,r=+e[3]/100,n=X(e[4])?1:+e[4];let o,a,h;if(0===i)o=a=h=r;else{const t=r<.5?r*(1+i):r+i-r*i,e=2*r-t;o=ut(255*wt(e,t,s+1/3),0),a=ut(255*wt(e,t,s),0),h=ut(255*wt(e,t,s-1/3),0)}return[o,a,h,n]})(i):[0,0,0,1],e;{const t=s.match(x);return e.t=3,e.d=t?t.map(Number):[],e.s=s.split(x)||[],e}}var i,r;return e.n=s,e},Bt=(t,e)=>(e.t=t._valueType,e.n=t._toNumber,e.u=t._unit,e.o=null,e.d=yt(t._toNumbers),e.s=yt(t._strings),e),Lt={t:0,n:0,u:null,o:null,d:null,s:null},Nt=(t,e,s,i,n)=>{const o=t.parent,a=t.duration,l=t.completed,c=t.iterationDuration,u=t.iterationCount,p=t._currentIteration,f=t._loopDelay,g=t._reversed,_=t._alternate,v=t._hasChildren,b=t._delay,S=t._currentTime,w=b+c,T=e-b,x=ct(S,-b,a),k=ct(T,-b,a),$=T-S,C=k>0,E=k>=a,D=a<=d,B=2===n;let L=0,A=T,F=0;if(u>1){const e=~~(k/(c+(E?0:f)));t._currentIteration=ct(e,0,u),E&&t._currentIteration--,L=t._currentIteration%2,A=k%(c+f)||0}const P=g^(_&&L),O=t._ease;let R=E?P?0:a:P?c-A:A;O&&(R=c*O(R/c)||0);const M=(o?o.backwards:T=b&&e<=w||e<=b&&x>b||e>=w&&x!==a)||R>=w&&x!==a||R<=b&&x>0||e<=x&&x===a&&l||E&&!l&&D){if(C&&(t.computeDeltaTime(x),s||t.onBeforeUpdate(t)),!v){const e=B||(M?-1*$:$)>=N.tickThreshold,n=t._offset+(o?o._offset:0)+b+R;let a,l,c,d,u=t._head,p=0;for(;u;){const t=u._composition,s=u._currentTime,o=u._changeDuration,f=u._absoluteStartTime+u._changeDuration,g=u._nextRep,_=u._prevRep,v=t!==r.none;if((e||(s!==o||n<=f+(g?g._delay:0))&&(0!==s||n>=u._absoluteStartTime))&&(!v||!u._isOverridden&&(!u._isOverlapped||n<=f)&&(!g||g._isOverridden||n<=g._absoluteStartTime)&&(!_||_._isOverridden||n>=_._absoluteStartTime+_._changeDuration+u._delay))){const e=u._currentTime=ct(R-u._startTime,0,o),s=u._ease(e/u._updateDuration),n=u._modifier,m=u._valueType,f=u._tweenType,g=0===f,y=0===m,_=y&&g||0===s||1===s?-1:N.precision;let b,S;if(y)b=S=n(ut(mt(u._fromNumber,u._toNumber,s),_));else if(1===m)S=n(ut(mt(u._fromNumber,u._toNumber,s),_)),b=`${S}${u._unit}`;else if(2===m){const t=u._fromNumbers,e=u._toNumbers,i=ut(ct(n(mt(t[0],e[0],s)),0,255),0),r=ut(ct(n(mt(t[1],e[1],s)),0,255),0),o=ut(ct(n(mt(t[2],e[2],s)),0,255),0),a=ct(n(ut(mt(t[3],e[3],s),_)),0,1);if(b=`rgba(${i},${r},${o},${a})`,v){const t=u._numbers;t[0]=i,t[1]=r,t[2]=o,t[3]=a}}else if(3===m){b=u._strings[0];for(let t=0,e=u._toNumbers.length;t0&&!l||M&&T<=d&&l)&&(t.onComplete(t),t.completed=!M):C&&E?u===1/0?t._startTime+=t.duration:t._currentIteration>=u-1&&(t.paused=!0,l||v||(t.completed=!0,s||o&&(M||!o.began)||(t.onComplete(t),t._resolve(t)))):t.completed=!1,F},At=(t,e,s,i,r)=>{const n=t._currentIteration;if(Nt(t,e,s,i,r),t._hasChildren){const o=t,a=o.backwards,h=i?e:o._iterationTime,l=O();let c=0,u=!0;if(!i&&o._currentIteration!==n){const t=o.iterationDuration;vt(o,e=>{if(a){const i=e.duration,r=e._offset+e._delay;s||!(i<=d)||r&&r+i!==t||e.onComplete(e)}else!e.completed&&!e.backwards&&e._currentTime{const e=ut((h-t._offset)*t._speed,12),n=t._fps=o.duration&&(o.paused=!0,o.completed||(o.completed=!0,s||(o.onComplete(o),o._resolve(o))))}},Ft={},Pt=(t,e,s)=>{if(3===s)return f.get(t)||t;if(2===s||1===s&&W(e)&&t in e.style){const e=Ft[t];if(e)return e;{const e=t?F(t):t;return Ft[t]=e,e}}return t},Ot=t=>{if(t._hasChildren)vt(t,Ot,!0);else{const e=t;e.pause(),vt(e,t=>{const s=t.property,i=t.target;if(i[o]){const r=i.style,n=t._inlineValue,o=V(n)||n===m;if(3===t._tweenType){const e=i[h];if(o?delete e[s]:e[s]=n,t._renderTransforms)if(Object.keys(e).length){let t=m;for(let s in e)t+=y[s]+e[s]+") ";r.transform=t}else r.removeProperty("transform")}else o?r.removeProperty(F(s)):r[s]=n;e._tail===t&&e.targets.forEach(t=>{t.getAttribute&&t.getAttribute("style")===m&&t.removeAttribute("style")})}})}return t};class Rt{constructor(t=0){this.deltaTime=0,this._currentTime=t,this._elapsedTime=t,this._startTime=t,this._lastTime=t,this._scheduledTime=0,this._frameDuration=ut(p/120,0),this._fps=120,this._speed=1,this._hasChildren=!1,this._head=null,this._tail=null}get fps(){return this._fps}set fps(t){const e=this._frameDuration,s=+t,i=se?requestAnimationFrame:setImmediate)(),It=(()=>e?cancelAnimationFrame:clearImmediate)();class Yt extends Rt{constructor(t){super(t),this.useDefaultMainLoop=!0,this.pauseOnDocumentHidden=!0,this.defaults=B,this.paused=!0,this.reqId=0}update(){const t=this._currentTime=O();if(this.requestTick(t)){this.computeDeltaTime(t);const e=this._speed,s=this._fps;let i=this._head;for(;i;){const r=i._next;i.paused?(bt(this,i),this._hasChildren=!!this._tail,i._running=!1,i.completed&&!i._cancelled&&i.cancel()):At(i,(t-i._startTime)*i._speed*e,0,0,i._fpst.resetTime()),this.wake()}get speed(){return this._speed*(1===N.timeScale?1:p)}set speed(t){this._speed=t*N.timeScale,vt(this,t=>t.speed=t._speed)}get timeUnit(){return 1===N.timeScale?"ms":"s"}set timeUnit(t){const e="s"===t,s=e?.001:1;if(N.timeScale!==s){N.timeScale=s,N.tickThreshold=200*s;const t=e?.001:p;this.defaults.duration*=t,this._speed*=t}}get precision(){return N.precision}set precision(t){N.precision=t}}const Xt=(()=>{const t=new Yt(O());return e&&(A.engine=t,i.addEventListener("visibilitychange",()=>{t.pauseOnDocumentHidden&&(i.hidden?t.pause():t.resume())})),t})(),Vt=()=>{Xt._head?(Xt.reqId=zt(Vt),Xt.update()):Xt.reqId=0},Wt=()=>(It(Xt.reqId),Xt.reqId=0,Xt),Ht={_rep:new WeakMap,_add:new Map},Ut=(t,e,s="_rep")=>{const i=Ht[s];let r=i.get(t);return r||(r={},i.set(t,r)),r[e]?r[e]:r[e]={_head:null,_tail:null}},qt=(t,e)=>t._isOverridden||t._absoluteStartTime>e._absoluteStartTime,jt=t=>{t._isOverlapped=1,t._isOverridden=1,t._changeDuration=d,t._currentTime=d},Gt=(t,e)=>{const s=t._composition;if(s===r.replace){const s=t._absoluteStartTime;St(e,t,qt,"_prevRep","_nextRep");const i=t._prevRep;if(i){const e=i.parent,r=i._absoluteStartTime+i._changeDuration;if(t.parent.id!==e.id&&e.iterationCount>1&&r+(e.duration-e.iterationDuration)>s){jt(i);let t=i._prevRep;for(;t&&t.parent.id===e.id;)jt(t),t=t._prevRep}const n=s-t._delay;if(r>n){const t=i._startTime,e=r-(t+i._updateDuration),s=ut(n-e-t,12);i._changeDuration=s,i._currentTime=s,i._isOverlapped=1,s{t._isOverlapped||(o=!1)}),o){const t=e.parent;if(t){let s=!0;vt(t,t=>{t!==e&&vt(t,t=>{t._isOverlapped||(s=!1)})}),s&&t.cancel()}else e.cancel()}}}else if(s===r.blend){const e=Ut(t.target,t.property,"_add"),s=(t=>{let e=Mt.animation;return e||(e={duration:d,computeDeltaTime:_,_offset:0,_delay:0,_head:null,_tail:null},Mt.animation=e,Mt.update=()=>{t.forEach(t=>{for(let e in t){const s=t[e],i=s._head;if(i){const t=i._valueType,e=3===t||2===t?yt(i._fromNumbers):null;let r=i._fromNumber,n=s._tail;for(;n&&n!==i;){if(e)for(let t=0,s=n._numbers.length;t{t._fromNumbers[s]=i._fromNumbers[s]-e,t._toNumbers[s]=0}),i._fromNumbers=e}St(e,t,null,"_prevAdd","_nextAdd")}return t},Qt=t=>{const e=t._composition;if(e!==r.none){const s=t.target,i=t.property,n=Ht._rep.get(s)[i];if(bt(n,t,"_prevRep","_nextRep"),e===r.blend){const e=Ht._add,r=e.get(s);if(!r)return;const n=r[i],o=Mt.animation;bt(n,t,"_prevAdd","_nextAdd");const a=n._head;if(a&&a===n._tail){bt(n,a,"_prevAdd","_nextAdd"),bt(o,a);let t=!0;for(let e in r)if(r[e]._head){t=!1;break}t&&e.delete(s)}}}return t},Zt=(t,e,s)=>{let i=!1;return vt(e,r=>{const n=r.target;if(t.includes(n)){const t=r.property,o=r._tweenType,a=Pt(s,n,o);(!a||a&&a===t)&&(r.parent._tail===r&&3===r._tweenType&&r._prev&&3===r._prev._tweenType&&(r._prev._renderTransforms=1),bt(e,r),Qt(r),i=!0)}},!0),i},Jt=(t,e,s)=>{const i=e||Xt;let r;if(i._hasChildren){let e=0;vt(i,n=>{if(!n._hasChildren)if(r=Zt(t,n,s),r&&!n._head)n.cancel(),bt(i,n);else{const t=n._offset+n._delay+n.duration;t>e&&(e=t)}n._head?Jt(t,n,s):n._hasChildren=!1},!0),X(i.iterationDuration)||(i.iterationDuration=e)}else r=Zt(t,i,s);r&&!i._head&&(i._hasChildren=!1,i.cancel&&i.cancel())},Kt=t=>(t.paused=!0,t.began=!1,t.completed=!1,t),te=t=>t._cancelled?(t._hasChildren?vt(t,te):vt(t,t=>{t._composition!==r.none&&Gt(t,Ut(t.target,t.property))}),t._cancelled=0,t):t;let ee=0;class se extends Rt{constructor(t={},e=null,s=0){super(0);const{id:i,delay:r,duration:n,reversed:o,alternate:a,loop:h,loopDelay:l,autoplay:c,frameRate:u,playbackRate:p,onComplete:m,onLoop:f,onPause:g,onBegin:y,onBeforeUpdate:v,onUpdate:b}=t;L.current&&L.current.register(this);const S=e?0:Xt._elapsedTime,w=e?e.defaults:N.defaults,T=Y(r)||X(r)?w.delay:+r,x=Y(n)||X(n)?1/0:+n,k=Tt(h,w.loop),$=Tt(l,w.loopDelay),C=!0===k||k===1/0||k<0?1/0:k+1;let E=0;e?E=s:(Xt.reqId||Xt.requestTick(O()),E=(Xt._elapsedTime-Xt._startTime)*N.timeScale),this.id=X(i)?++ee:i,this.parent=e,this.duration=ft((x+$)*C-$)||d,this.backwards=!1,this.paused=!0,this.began=!1,this.completed=!1,this.onBegin=y||w.onBegin,this.onBeforeUpdate=v||w.onBeforeUpdate,this.onUpdate=b||w.onUpdate,this.onLoop=f||w.onLoop,this.onPause=g||w.onPause,this.onComplete=m||w.onComplete,this.iterationDuration=x,this.iterationCount=C,this._autoplay=!e&&Tt(c,w.autoplay),this._offset=E,this._delay=T,this._loopDelay=$,this._iterationTime=0,this._currentIteration=0,this._resolve=_,this._running=!1,this._reversed=+Tt(o,w.reversed),this._reverse=this._reversed,this._cancelled=0,this._alternate=Tt(a,w.alternate),this._prev=null,this._next=null,this._elapsedTime=S,this._startTime=S,this._lastTime=S,this._fps=Tt(u,w.frameRate),this._speed=Tt(p,w.playbackRate)}get cancelled(){return!!this._cancelled}set cancelled(t){t?this.cancel():this.reset(!0).play()}get currentTime(){return ct(ut(this._currentTime,N.precision),-this._delay,this.duration)}set currentTime(t){const e=this.paused;this.pause().seek(+t),e||this.resume()}get iterationCurrentTime(){return ut(this._iterationTime,N.precision)}set iterationCurrentTime(t){this.currentTime=this.iterationDuration*this._currentIteration+t}get progress(){return ct(ut(this._currentTime/this.duration,10),0,1)}set progress(t){this.currentTime=this.duration*t}get iterationProgress(){return ct(ut(this._iterationTime/this.iterationDuration,10),0,1)}set iterationProgress(t){const e=this.iterationDuration;this.currentTime=e*this._currentIteration+e*t}get currentIteration(){return this._currentIteration}set currentIteration(t){this.currentTime=this.iterationDuration*ct(+t,0,this.iterationCount-1)}get reversed(){return!!this._reversed}set reversed(t){t?this.reverse():this.play()}get speed(){return super.speed}set speed(t){super.speed=t,this.resetTime()}reset(t=!1){return te(this),this._reversed&&!this._reverse&&(this.reversed=!1),this._iterationTime=this.iterationDuration,At(this,0,1,~~t,2),Kt(this),this._hasChildren&&vt(this,Kt),this}init(t=!1){this.fps=this._fps,this.speed=this._speed,!t&&this._hasChildren&&At(this,this.duration,1,~~t,2),this.reset(t);const e=this._autoplay;return!0===e?this.resume():e&&!X(e.linked)&&e.link(this),this}resetTime(){const t=1/(this._speed*Xt._speed);return this._startTime=O()-(this._currentTime+this._delay)*t,this}pause(){return this.paused||(this.paused=!0,this.onPause(this)),this}resume(){return this.paused?(this.paused=!1,this.duration<=d&&!this._hasChildren?At(this,d,0,0,2):(this._running||(St(Xt,this),Xt._hasChildren=!0,this._running=!0),this.resetTime(),this._startTime-=12,Xt.wake()),this):this}restart(){return this.reset().resume()}seek(t,e=0,s=0){te(this),this.completed=!1;const i=this.paused;return this.paused=!0,At(this,t+this._delay,~~e,~~s,1),i?this:this.resume()}alternate(){const t=this._reversed,e=this.iterationCount,s=this.iterationDuration,i=e===1/0?rt(u/s):e;return this._reversed=+(!this._alternate||i%2?!t:t),e===1/0?this.iterationProgress=this._reversed?1-this.iterationProgress:this.iterationProgress:this.seek(s*i-this._currentTime),this.resetTime(),this}play(){return this._reversed&&this.alternate(),this.resume()}reverse(){return this._reversed||this.alternate(),this.resume()}cancel(){return this._hasChildren?vt(this,t=>t.cancel(),!0):vt(this,Qt),this._cancelled=1,this.pause()}stretch(t){const e=this.duration,s=gt(t);if(e===s)return this;const i=t/e,r=t<=d;return this.duration=r?d:s,this.iterationDuration=r?d:gt(this.iterationDuration*i),this._offset*=i,this._delay*=i,this._loopDelay*=i,this}revert(){At(this,0,1,0,1);const t=this._autoplay;return t&&t.linked&&t.linked===this&&t.revert(),this.cancel()}complete(){return this.seek(this.duration).cancel()}then(t=_){const e=this.then,s=()=>{this.then=null,t(this),this.then=e,this._resolve=_};return new Promise(t=>(this._resolve=()=>t(s()),this.completed&&this._resolve(),this))}}function ie(t){const e=I(t)?L.root.querySelectorAll(t):t;if(e instanceof NodeList||e instanceof HTMLCollection)return e}function re(t){if(V(t))return[];if(!e)return R(t)&&t.flat(1/0)||[t];if(R(t)){const e=t.flat(1/0),s=[];for(let t=0,i=e.length;t{const n=e.u,o=e.n;if(1===e.t&&n===s)return e;const a=o+n+s,h=ae[a];if(X(h)||r){let r;if(n in oe)r=o*oe[n]/oe[s];else{const e=100,a=t.cloneNode(),h=t.parentNode,l=h&&h!==i?h:i.body;l.appendChild(a);const c=a.style;c.width=e+n;const d=a.offsetWidth||e;c.width=e+s;const u=d/(a.offsetWidth||e);l.removeChild(a),r=u*o}e.n=r,ae[a]=r}else e.n=h;return e.t,e.u=s,e},le=t=>t,ce=(t=1.68)=>e=>Z(e,+t),de={in:t=>e=>t(e),out:t=>e=>1-t(1-e),inOut:t=>e=>e<.5?t(2*e)/2:1-t(-2*e+2)/2,outIn:t=>e=>e<.5?(1-t(1-2*e))/2:(t(2*e-1)+1)/2},ue=ht/2,pe=2*ht,me={[m]:ce,Quad:ce(2),Cubic:ce(3),Quart:ce(4),Quint:ce(5),Sine:t=>1-tt(t*ue),Circ:t=>1-J(1-t*t),Expo:t=>t?Z(2,10*t-10):0,Bounce:t=>{let e,s=4;for(;t<((e=Z(2,--s))-1)/11;);return 1/Z(4,3-s)-7.5625*Z((3*e-2)/22-t,2)},Back:(t=1.7)=>e=>(+t+1)*e*e*e-+t*e*e,Elastic:(t=1,e=.3)=>{const s=ct(+t,1,10),i=ct(+e,d,2),r=i/pe*nt(1/s),n=pe/i;return t=>0===t||1===t?t:-s*Z(2,-10*(1-t))*K((1-t-r)*n)}},fe=(()=>{const t={linear:le,none:le};for(let e in de)for(let s in me){const i=me[s],r=de[e];t[e+s]=s===m||"Back"===s||"Elastic"===s?(t,e)=>r(i(t,e)):r(i)}return t})(),ge={linear:le,none:le},ye=t=>{if(ge[t])return ge[t];if(t.indexOf("(")<=-1){const e=de[t]||t.includes("Back")||t.includes("Elastic")?fe[t]():fe[t];return e?ge[t]=e:le}{const e=t.slice(0,-1).split("("),s=fe[e[0]];return s?ge[t]=s(...e[1].split(",")):le}},_e=["steps(","irregular(","linear(","cubicBezier("],ve=t=>{if(I(t))for(let e=0,s=_e.length;e{const s={};if(R(t)){const e=[].concat(...t.map(t=>Object.keys(t))).filter(j);for(let i=0,r=e.length;i{const e={};for(let s in t){const i=t[s];j(s)?s===r&&(e.to=i):e[s]=i}return e});s[r]=n}}else{const i=Tt(e.duration,N.defaults.duration),r=Object.keys(t).map(e=>({o:parseFloat(e)/100,p:t[e]})).sort((t,e)=>t.o-e.o);r.forEach(t=>{const e=t.o,r=t.p;for(let t in r)if(j(t)){let n=s[t];n||(n=s[t]=[]);const o=e*i;let a=n.length,h=n[a-1];const l={to:r[t]};let c=0;for(let t=0;t=p?r.none:X(v)?S.composition:v,B=this._offset+(s?s._offset:0);x&&(g.parent=this);let L=NaN,A=NaN,F=0,P=0;for(let t=0;t2&&e?(Ce=[],h.forEach((t,e)=>{e?1===e?(ke[1]=t,Ce.push(ke)):Ce.push(t):ke[0]=t})):Ce=h}else xe[0]=h,Ce=xe;let f=null,g=null,y=NaN,_=0,v=0;for(let t=Ce.length;v1?xt($,e,i,c)/t:$),e,i,c),w=xt(Tt(Ee.delay,v?0:C),e,i,c),T=xt(Tt(Ee.composition,D),e,i,c),x=z(T)?T:r[T],L=Ee.modifier||E,N=!X(u),A=!X(l),O=R(l),I=O||N&&A,Y=g?_+w:w,W=ut(B+Y,12);P||!N&&!O||(P=1);let H=g;if(x!==r.none){f||(f=Ut(e,a));let t=f._head;for(;t&&!t._isOverridden&&t._absoluteStartTime<=W;)if(H=t,t=t._nextRep,t&&t._absoluteStartTime>=W)for(;t;)jt(t),t=t._nextRep}if(I?(Dt(O?xt(l[0],e,i,c):u,be),Dt(O?xt(l[1],e,i,c,Te):l,Se),0===be.t&&(H?1===H._valueType&&(be.t=1,be.u=H._unit):(Dt(Ct(e,a,o,we),Lt),1===Lt.t&&(be.t=1,be.u=Lt.u)))):(A?Dt(l,Se):g?Bt(g,Se):Dt(s&&H&&H.parent.parent===s?H._value:Ct(e,a,o,we),Se),N?Dt(u,be):g?Bt(g,be):Dt(s&&H&&H.parent.parent===s?H._value:Ct(e,a,o,we),be)),be.o&&(be.n=Et(H?H._toNumber:Dt(Ct(e,a,o,we),Lt).n,be.n,be.o)),Se.o&&(Se.n=Et(be.n,Se.n,Se.o)),be.t!==Se.t)if(3===be.t||3===Se.t){const t=3===be.t?be:Se,e=3===be.t?Se:be;e.t=3,e.s=yt(t.s),e.d=t.d.map(()=>e.n)}else if(1===be.t||1===Se.t){const t=1===be.t?be:Se,e=1===be.t?Se:be;e.t=1,e.u=t.u}else if(2===be.t||2===Se.t){const t=2===be.t?be:Se,e=2===be.t?Se:be;e.t=2,e.s=t.s,e.d=[0,0,0,1]}if(be.u!==Se.u){let t=Se.u?be:Se;t=he(e,t,Se.u?Se.u:be.u,!1)}if(Se.d&&be.d&&Se.d.length!==be.d.length){const t=be.d.length>Se.d.length?be:Se,e=t===be?Se:be;e.d=t.d.map((t,s)=>X(e.d[s])?0:e.d[s]),e.s=yt(t.s)}const U=ut(+S||d,12);let q=we[a];V(q)||(we[a]=null);const j={parent:this,id:De++,property:a,target:e,_value:null,_func:Te.func,_ease:ve(b),_fromNumbers:yt(be.d),_toNumbers:yt(Se.d),_strings:yt(Se.s),_fromNumber:be.n,_toNumber:Se.n,_numbers:yt(be.d),_number:be.n,_unit:Se.u,_modifier:L,_currentTime:0,_startTime:Y,_delay:+w,_updateDuration:U,_changeDuration:U,_absoluteStartTime:W,_tweenType:o,_valueType:Se.t,_composition:x,_isOverlapped:0,_isOverridden:0,_renderTransforms:0,_inlineValue:q,_prevRep:null,_nextRep:null,_prevAdd:null,_nextAdd:null,_prev:null,_next:null};x!==r.none&&Gt(j,f),isNaN(y)&&(y=j._startTime),_=ut(Y+U,12),g=j,F++,St(this,j)}(isNaN(A)||yL)&&(L=_),3===o&&(p=F-v,m=F)}if(!isNaN(p)){let t=0;vt(this,e=>{t>=p&&t{t.id===e.id&&(t._renderTransforms=1)})),t++})}}l||console.warn("No target found. Make sure the element you're trying to animate is accessible before creating your animation."),A?(vt(this,t=>{t._startTime-t._delay||(t._delay-=A),t._startTime-=A}),L-=A):A=0,L||(L=d,this.iterationCount=0),this.targets=h,this.duration=L===d?d:ft((L+this._loopDelay)*this.iterationCount-this._loopDelay)||d,this.onRender=b||S.onRender,this._ease=T,this._delay=A,this.iterationDuration=L,!this._autoplay&&P&&this.onRender(this)}stretch(t){const e=this.duration;if(e===gt(t))return this;const s=t/e;return vt(this,t=>{t._updateDuration=gt(t._updateDuration*s),t._changeDuration=gt(t._changeDuration*s),t._currentTime*=s,t._startTime*=s,t._absoluteStartTime*=s}),super.stretch(t)}refresh(){return vt(this,t=>{const e=t._func;if(e){const s=Ct(t.target,t.property,t._tweenType);Dt(s,Lt),Dt(e(),Se),t._fromNumbers=yt(Lt.d),t._fromNumber=Lt.n,t._toNumbers=yt(Se.d),t._strings=yt(Se.s),t._toNumber=Se.o?Et(Lt.n,Se.n,Se.o):Se.n}}),this.duration===d&&this.restart(),this}revert(){return super.revert(),Ot(this)}then(t){return super.then(t)}}const Le={_head:null,_tail:null},Ne=(t,e,s)=>{let i,r=Le._head;for(;r;){const n=r._next,o=r.$el===t,a=!e||r.property===e,h=!s||r.parent===s;if(o&&a&&h){i=r.animation;try{i.commitStyles()}catch{}i.cancel(),bt(Le,r);const t=r.parent;t&&(t._completed++,t.animations.length===t._completed&&(t.completed=!0,t.paused=!0,t.muteCallbacks||(t.onComplete(t),t._resolve(t))))}r=n}return i},Ae=(t,e,s,i,r)=>{const n=e.animate(i,r),o=r.delay+ +r.duration*r.iterations;n.playbackRate=t._speed,t.paused&&n.pause(),t.duration{Ne(e,s,t)};return n.oncancel=a,n.onremove=a,t.persist||(n.onfinish=a),n};function Fe(t,e,s){const i=ne(t);if(!i.length)return;const[r]=i,n=kt(r,e),o=Pt(e,r,n);let a=Ct(r,o);if(X(s))return a;if(Dt(a,Lt),0===Lt.t||1===Lt.t){if(!1===s)return Lt.n;{const t=he(r,Lt,s,!1);return`${ut(t.n,N.precision)}${t.u}`}}}const Pe=(t,e)=>{if(!X(e))return e.duration=d,e.composition=Tt(e.composition,r.none),new Be(t,e,null,0,!0).resume()},Oe=(t,e,s)=>{const i=re(t);for(let t=0,r=i.length;t{let s=t.iterationDuration;if(s===d&&(s=0),X(e))return s;if(z(+e))return+e;const i=e,r=t?t.labels:null,n=!V(r),o=((t,e)=>{if(P(e,"<")){const s="<"===e[1],i=t._tail,r=i?i._offset+i._delay:0;return s?r:r+i.duration}})(t,i),a=!X(o),h=E.exec(i);if(h){const t=h[0],e=i.split(t),l=n&&e[0]?r[e[0]]:s,c=a?o:n?l:s,d=+e[1];return Et(c,d,t[0])}return a?o:n?X(r[i])?s:r[i]:s};function Me(t,e,s,i,r,n){const o=z(t.duration)&&t.duration<=d?s-d:s;At(e,o,1,1,1);const a=i?new Be(i,t,e,o,!1,r,n):new se(t,e,o);return a.init(!0),St(e,a),vt(e,t=>{const s=t._offset+t._delay+t.duration;s>e.iterationDuration&&(e.iterationDuration=s)}),e.duration=function(t){return ft((t.iterationDuration+t._loopDelay)*t.iterationCount-t._loopDelay)||d}(e),e}class ze extends se{constructor(t={}){super(t,null,0),this.duration=0,this.labels={};const e=t.defaults,s=N.defaults;this.defaults=e?_t(e,s):s,this.onRender=t.onRender||s.onRender;const i=Tt(t.playbackEase,s.playbackEase);this._ease=i?ve(i):null,this.iterationDuration=0}add(t,e,s){const i=M(e),r=M(t);if(i||r){if(this._hasChildren=!0,i){const i=e;if(Y(s)){const e=s,r=re(t),n=this.duration,o=this.iterationDuration,a=i.id;let h=0;const l=r.length;r.forEach(t=>{const s={...i};this.duration=n,this.iterationDuration=o,X(a)||(s.id=a+"-"+h),Me(s,this,Re(this,e(t,h,l,this)),t,h,l),h++})}else Me(i,this,Re(this,s),t)}else Me(t,this,Re(this,e));return this.init(!0)}}sync(t,e){if(X(t)||t&&X(t.pause))return this;t.pause();const s=+(t.effect?t.effect.getTiming().duration:t.duration);return this.add(t,{currentTime:[0,s],duration:s,ease:"linear"},e)}set(t,e,s){return X(e)?this:(e.duration=d,e.composition=r.replace,this.add(t,e,s))}call(t,e){return X(t)||t&&!Y(t)?this:this.add({duration:0,onComplete:()=>t(this)},e)}label(t,e){return X(t)||t&&!I(t)||(this.labels[t]=Re(this,e)),this}remove(t,e){return Jt(re(t),this,e),this}stretch(t){const e=this.duration;if(e===gt(t))return this;const s=t/e,i=this.labels;vt(this,t=>t.stretch(t.duration*s));for(let t in i)i[t]*=s;return super.stretch(t)}refresh(){return vt(this,t=>{t.refresh&&t.refresh()}),this}revert(){return super.revert(),vt(this,t=>t.revert,!0),Ot(this)}then(t){return super.then(t)}}class Ie{constructor(t,e){L.current&&L.current.register(this);const s=()=>{if(this.callbacks.completed)return;let t=!0;for(let e in this.animations)if(!this.animations[e].paused&&t){t=!1;break}t&&this.callbacks.complete()},i={onBegin:()=>{this.callbacks.completed&&this.callbacks.reset(),this.callbacks.play()},onComplete:s,onPause:s},n={v:1,autoplay:!1},o={};if(this.targets=[],this.animations={},this.callbacks=null,!X(t)&&!X(e)){for(let t in e){const s=e[t];j(t)?o[t]=s:P(t,"on")?n[t]=s:i[t]=s}this.callbacks=new Be({v:0},n);for(let e in o){const s=o[e],n=M(s);let a={},h="+=0";if(n){const t=s.unit;I(t)&&(h+=t)}else a.duration=s;a[e]=n?_t({to:h},s):h;const l=_t(i,a);l.composition=r.replace,l.autoplay=!1;const c=this.animations[e]=new Be(t,l,null,0,!1).init();this.targets.length||this.targets.push(...c.targets),this[e]=(t,e,s)=>{const i=c._head;if(X(t)&&i){const t=i._numbers;return t&&t.length?t:i._modifier(i._number)}return vt(c,e=>{if(R(t))for(let s=0,i=t.length;si+(t-e)/(s-e)*(r-i);var Xe=Object.freeze({__proto__:null,clamp:ct,damp:(t,e,s,i)=>i?1===i?e:mt(t,e,1-Math.exp(-i*s*.1)):t,degToRad:t=>t*Math.PI/180,lerp:mt,mapRange:Ye,padEnd:(t,e,s)=>`${t}`.padEnd(e,s),padStart:(t,e,s)=>`${t}`.padStart(e,s),radToDeg:t=>180*t/Math.PI,round:ut,roundPad:(t,e)=>(+t).toFixed(e),snap:pt,wrap:(t,e,s)=>((t-e)%(s-e)+(s-e))%(s-e)+e});const Ve=10*p;class We{constructor(t={}){const e=!X(t.bounce)||!X(t.duration);this.timeStep=.02,this.restThreshold=5e-4,this.restDuration=200,this.maxDuration=6e4,this.maxRestSteps=this.restDuration/this.timeStep/p,this.maxIterations=this.maxDuration/this.timeStep/p,this.bn=ct(Tt(t.bounce,.5),-1,1),this.pd=ct(Tt(t.duration,628),10*N.timeScale,Ve*N.timeScale),this.m=ct(Tt(t.mass,1),1,Ve),this.s=ct(Tt(t.stiffness,100),d,Ve),this.d=ct(Tt(t.damping,10),d,Ve),this.v=ct(Tt(t.velocity,0),-1e4,Ve),this.w0=0,this.zeta=0,this.wd=0,this.b=0,this.completed=!1,this.solverDuration=0,this.settlingDuration=0,this.parent=null,this.onComplete=t.onComplete||_,e&&this.calculateSDFromBD(),this.compute(),this.ease=t=>{const e=t*this.settlingDuration,s=this.completed,i=this.pd;return e>=i&&!s&&(this.completed=!0,this.onComplete(this.parent)),e=0?this.d=4*(1-this.bn)*ht/t:this.d=4*ht/(t*(1+this.bn)),this.s=ut(ct(this.s,d,Ve),3),this.d=ut(ct(this.d,d,300),3)}calculateBDFromSD(){const t=2*ht/J(this.s);this.pd=t*(1===N.timeScale?p:1);const e=this.d/(2*J(this.s));this.bn=e<=1?1-this.d*t/(4*ht):4*ht/(this.d*t)-1,this.bn=ut(ct(this.bn,-1,1),3),this.pd=ut(ct(this.pd,10*N.timeScale,Ve*N.timeScale),3)}compute(){const{maxRestSteps:t,maxIterations:e,restThreshold:s,timeStep:i,m:r,d:n,s:o,v:a}=this,h=this.w0=ct(J(o/r),d,p),l=this.zeta=n/(2*J(o*r));l<1?(this.wd=h*J(1-l*l),this.b=(l*h-a)/this.wd):1===l?(this.wd=0,this.b=-a+h):(this.wd=h*J(l*l-1),this.b=(l*h-a)/this.wd);let c=0,u=0,m=0;for(;u<=t&&m<=e;)et(1-this.solve(c))new We(t),Ue=t=>(console.warn("createSpring() is deprecated use spring() instead"),new We(t)),qe=t=>{t.cancelable&&t.preventDefault()};class je{constructor(t){this.el=t,this.zIndex=0,this.parentElement=null,this.classList={add:_,remove:_}}get x(){return this.el.x||0}set x(t){this.el.x=t}get y(){return this.el.y||0}set y(t){this.el.y=t}get width(){return this.el.width||0}set width(t){this.el.width=t}get height(){return this.el.height||0}set height(t){this.el.height=t}getBoundingClientRect(){return{top:this.y,right:this.x,bottom:this.y+this.height,left:this.x+this.width}}}class Ge{constructor(t){this.$el=t,this.inlineTransforms=[],this.point=new DOMPoint,this.inversedMatrix=this.getMatrix().inverse()}normalizePoint(t,e){return this.point.x=t,this.point.y=e,this.point.matrixTransform(this.inversedMatrix)}traverseUp(t){let e=this.$el.parentElement,s=0;for(;e&&e!==i;)t(e,s),e=e.parentElement,s++}getMatrix(){const t=new DOMMatrix;return this.traverseUp(e=>{const s=getComputedStyle(e).transform;if(s){const e=new DOMMatrix(s);t.preMultiplySelf(e)}}),t}remove(){this.traverseUp((t,e)=>{this.inlineTransforms[e]=t.style.transform,t.style.transform="none"})}revert(){this.traverseUp((t,e)=>{const s=this.inlineTransforms[e];""===s?t.style.removeProperty("transform"):t.style.transform=s})}}const Qe=(t,e)=>t&&Y(t)?t(e):t;let Ze=0;class Je{constructor(t,e={}){if(!t)return;L.current&&L.current.register(this);const r=e.x,n=e.y,o=e.trigger,a=e.modifier,h=e.releaseEase,l=h&&ve(h),c=!X(h)&&!X(h.ease),d=M(r)&&!X(r.mapTo)?r.mapTo:"translateX",p=M(n)&&!X(n.mapTo)?n.mapTo:"translateY",m=Qe(e.container,this);this.containerArray=R(m)?m:null,this.$container=m&&!this.containerArray?re(m)[0]:i.body,this.useWin=this.$container===i.body,this.$scrollContainer=this.useWin?s:this.$container,this.$target=M(t)?new je(t):re(t)[0],this.$trigger=re(o||t)[0],this.fixed="fixed"===Fe(this.$target,"position"),this.isFinePointer=!0,this.containerPadding=[0,0,0,0],this.containerFriction=0,this.releaseContainerFriction=0,this.snapX=0,this.snapY=0,this.scrollSpeed=0,this.scrollThreshold=0,this.dragSpeed=0,this.dragThreshold=3,this.maxVelocity=0,this.minVelocity=0,this.velocityMultiplier=0,this.cursor=!1,this.releaseXSpring=c?h:He({mass:Tt(e.releaseMass,1),stiffness:Tt(e.releaseStiffness,80),damping:Tt(e.releaseDamping,20)}),this.releaseYSpring=c?h:He({mass:Tt(e.releaseMass,1),stiffness:Tt(e.releaseStiffness,80),damping:Tt(e.releaseDamping,20)}),this.releaseEase=l||fe.outQuint,this.hasReleaseSpring=c,this.onGrab=e.onGrab||_,this.onDrag=e.onDrag||_,this.onRelease=e.onRelease||_,this.onUpdate=e.onUpdate||_,this.onSettle=e.onSettle||_,this.onSnap=e.onSnap||_,this.onResize=e.onResize||_,this.onAfterResize=e.onAfterResize||_,this.disabled=[0,0];const f={};if(a&&(f.modifier=a),X(r)||!0===r)f[d]=0;else if(M(r)){const t=r,e={};t.modifier&&(e.modifier=t.modifier),t.composition&&(e.composition=t.composition),f[d]=e}else!1===r&&(f[d]=0,this.disabled[0]=1);if(X(n)||!0===n)f[p]=0;else if(M(n)){const t=n,e={};t.modifier&&(e.modifier=t.modifier),t.composition&&(e.composition=t.composition),f[p]=e}else!1===n&&(f[p]=0,this.disabled[1]=1);this.animate=new Ie(this.$target,f),this.xProp=d,this.yProp=p,this.destX=0,this.destY=0,this.deltaX=0,this.deltaY=0,this.scroll={x:0,y:0},this.coords=[this.x,this.y,0,0],this.snapped=[0,0],this.pointer=[0,0,0,0,0,0,0,0],this.scrollView=[0,0],this.dragArea=[0,0,0,0],this.containerBounds=[-u,u,u,-u],this.scrollBounds=[0,0,0,0],this.targetBounds=[0,0,0,0],this.window=[0,0],this.velocityStack=[0,0,0],this.velocityStackIndex=0,this.velocityTime=O(),this.velocity=0,this.angle=0,this.cursorStyles=null,this.triggerStyles=null,this.bodyStyles=null,this.targetStyles=null,this.touchActionStyles=null,this.transforms=new Ge(this.$target),this.overshootCoords={x:0,y:0},this.overshootTicker=new se({autoplay:!1,onUpdate:()=>{this.updated=!0,this.manual=!0,this.disabled[0]||this.animate[this.xProp](this.overshootCoords.x,1),this.disabled[1]||this.animate[this.yProp](this.overshootCoords.y,1)},onComplete:()=>{this.manual=!1,this.disabled[0]||this.animate[this.xProp](this.overshootCoords.x,0),this.disabled[1]||this.animate[this.yProp](this.overshootCoords.y,0)}},null,0).init(),this.updateTicker=new se({autoplay:!1,onUpdate:()=>this.update()},null,0).init(),this.contained=!X(m),this.manual=!1,this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.enabled=!1,this.initialized=!1,this.activeProp=this.disabled[1]?d:p,this.animate.callbacks.onRender=()=>{const t=this.updated,e=!(this.grabbed&&t)&&this.released,s=this.x,i=this.y,r=s-this.coords[2],n=i-this.coords[3];this.deltaX=r,this.deltaY=n,this.coords[2]=s,this.coords[3]=i,t&&(r||n)&&this.onUpdate(this),e?(this.computeVelocity(r,n),this.angle=at(n,r)):this.updated=!1},this.animate.callbacks.onComplete=()=>{!this.grabbed&&this.released&&(this.released=!1),this.manual||(this.deltaX=0,this.deltaY=0,this.velocity=0,this.velocityStack[0]=0,this.velocityStack[1]=0,this.velocityStack[2]=0,this.velocityStackIndex=0,this.onSettle(this))},this.resizeTicker=new se({autoplay:!1,duration:150*N.timeScale,onComplete:()=>{this.onResize(this),this.refresh(),this.onAfterResize(this)}}).init(),this.parameters=e,this.resizeObserver=new ResizeObserver(()=>{this.initialized?this.resizeTicker.restart():this.initialized=!0}),this.enable(),this.refresh(),this.resizeObserver.observe(this.$container),M(t)||this.resizeObserver.observe(this.$target)}computeVelocity(t,e){const s=this.velocityTime,i=O(),r=i-s;if(r<17)return this.velocity;this.velocityTime=i;const n=this.velocityStack,o=this.velocityMultiplier,a=this.minVelocity,h=this.maxVelocity,l=this.velocityStackIndex;n[l]=ut(ct(J(t*t+e*e)/r*o,a,h),5);const c=ot(n[0],n[1],n[2]);return this.velocity=c,this.velocityStackIndex=(l+1)%3,c}setX(t,e=!1){if(this.disabled[0])return;const s=ut(t,5);return this.overshootTicker.pause(),this.manual=!0,this.updated=!e,this.destX=s,this.snapped[0]=pt(s,this.snapX),this.animate[this.xProp](s,0),this.manual=!1,this}setY(t,e=!1){if(this.disabled[1])return;const s=ut(t,5);return this.overshootTicker.pause(),this.manual=!0,this.updated=!e,this.destY=s,this.snapped[1]=pt(s,this.snapY),this.animate[this.yProp](s,0),this.manual=!1,this}get x(){return ut(this.animate[this.xProp](),N.precision)}set x(t){this.setX(t,!1)}get y(){return ut(this.animate[this.yProp](),N.precision)}set y(t){this.setY(t,!1)}get progressX(){return Ye(this.x,this.containerBounds[3],this.containerBounds[1],0,1)}set progressX(t){this.setX(Ye(t,0,1,this.containerBounds[3],this.containerBounds[1]),!1)}get progressY(){return Ye(this.y,this.containerBounds[0],this.containerBounds[2],0,1)}set progressY(t){this.setY(Ye(t,0,1,this.containerBounds[0],this.containerBounds[2]),!1)}updateScrollCoords(){const t=ut(this.useWin?s.scrollX:this.$container.scrollLeft,0),e=ut(this.useWin?s.scrollY:this.$container.scrollTop,0),[i,r,n,o]=this.containerPadding,a=this.scrollThreshold;this.scroll.x=t,this.scroll.y=e,this.scrollBounds[0]=e-this.targetBounds[0]+i-a,this.scrollBounds[1]=t-this.targetBounds[1]-r+a,this.scrollBounds[2]=e-this.targetBounds[2]-n+a,this.scrollBounds[3]=t-this.targetBounds[3]+o-a}updateBoundingValues(){const t=this.$container;if(!t)return;const e=this.x,r=this.y,n=this.coords[2],o=this.coords[3];this.coords[2]=0,this.coords[3]=0,this.setX(0,!0),this.setY(0,!0),this.transforms.remove();const a=this.window[0]=s.innerWidth,h=this.window[1]=s.innerHeight,l=this.useWin,c=t.scrollWidth,d=t.scrollHeight,u=this.fixed,p=t.getBoundingClientRect(),[m,f,g,y]=this.containerPadding;this.dragArea[0]=l?0:p.left,this.dragArea[1]=l?0:p.top,this.scrollView[0]=l?ct(c,a,c):c,this.scrollView[1]=l?ct(d,h,d):d,this.updateScrollCoords();const{width:_,height:v,left:b,top:S,right:w,bottom:T}=t.getBoundingClientRect();this.dragArea[2]=ut(l?ct(_,a,a):_,0),this.dragArea[3]=ut(l?ct(v,h,h):v,0);const x=Fe(t,"overflow"),k="visible"===x,$="hidden"===x;if(this.canScroll=!u&&this.contained&&(t===i.body&&k||!$&&!k)&&(c>this.dragArea[2]+y-f||d>this.dragArea[3]+m-g)&&(!this.containerArray||this.containerArray&&!R(this.containerArray)),this.contained){const e=this.scroll.x,s=this.scroll.y,i=this.canScroll,r=this.$target.getBoundingClientRect(),n=i?l?0:t.scrollLeft:0,o=i?l?0:t.scrollTop:0,c=i?this.scrollView[0]-n-_:0,d=i?this.scrollView[1]-o-v:0;this.targetBounds[0]=ut(r.top+s-(l?0:S),0),this.targetBounds[1]=ut(r.right+e-(l?a:w),0),this.targetBounds[2]=ut(r.bottom+s-(l?h:T),0),this.targetBounds[3]=ut(r.left+e-(l?0:b),0),this.containerArray?(this.containerBounds[0]=this.containerArray[0]+m,this.containerBounds[1]=this.containerArray[1]-f,this.containerBounds[2]=this.containerArray[2]-g,this.containerBounds[3]=this.containerArray[3]+y):(this.containerBounds[0]=-ut(r.top-(u?ct(S,0,h):S)+o-m,0),this.containerBounds[1]=-ut(r.right-(u?ct(w,0,a):w)-c+f,0),this.containerBounds[2]=-ut(r.bottom-(u?ct(T,0,h):T)-d+g,0),this.containerBounds[3]=-ut(r.left-(u?ct(b,0,a):b)+n-y,0))}this.transforms.revert(),this.coords[2]=n,this.coords[3]=o,this.setX(e,!0),this.setY(r,!0)}isOutOfBounds(t,e,s){if(!this.contained)return 0;const[i,r,n,o]=t,[a,h]=this.disabled,l=!a&&er,c=!h&&sn;return l&&!c?1:!l&&c?2:l&&c?3:0}refresh(){const t=this.parameters,e=t.x,r=t.y,n=Qe(t.container,this),o=Qe(t.containerPadding,this)||0,a=R(o)?o:[o,o,o,o],h=this.x,l=this.y,c=Qe(t.cursor,this),d={onHover:"grab",onGrab:"grabbing"};if(c){const{onHover:t,onGrab:e}=c;t&&(d.onHover=t),e&&(d.onGrab=e)}const u=Qe(t.dragThreshold,this),p={mouse:3,touch:7};if(z(u))p.mouse=u,p.touch=u;else if(u){const{mouse:t,touch:e}=u;X(t)||(p.mouse=t),X(e)||(p.touch=e)}this.containerArray=R(n)?n:null,this.$container=n&&!this.containerArray?re(n)[0]:i.body,this.useWin=this.$container===i.body,this.$scrollContainer=this.useWin?s:this.$container,this.isFinePointer=matchMedia("(pointer:fine)").matches,this.containerPadding=Tt(a,[0,0,0,0]),this.containerFriction=ct(Tt(Qe(t.containerFriction,this),.8),0,1),this.releaseContainerFriction=ct(Tt(Qe(t.releaseContainerFriction,this),this.containerFriction),0,1),this.snapX=Qe(M(e)&&!X(e.snap)?e.snap:t.snap,this),this.snapY=Qe(M(r)&&!X(r.snap)?r.snap:t.snap,this),this.scrollSpeed=Tt(Qe(t.scrollSpeed,this),1.5),this.scrollThreshold=Tt(Qe(t.scrollThreshold,this),20),this.dragSpeed=Tt(Qe(t.dragSpeed,this),1),this.dragThreshold=this.isFinePointer?p.mouse:p.touch,this.minVelocity=Tt(Qe(t.minVelocity,this),0),this.maxVelocity=Tt(Qe(t.maxVelocity,this),50),this.velocityMultiplier=Tt(Qe(t.velocityMultiplier,this),1),this.cursor=!1!==c&&d,this.updateBoundingValues();const[m,f,g,y]=this.containerBounds;this.setX(ct(h,y,f),!0),this.setY(ct(l,m,g),!0)}update(){if(this.updateScrollCoords(),this.canScroll){const[t,e,s,i]=this.containerPadding,[r,n]=this.scrollView,o=this.dragArea[2],a=this.dragArea[3],h=this.scroll.x,l=this.scroll.y,c=this.$container.scrollWidth,d=this.$container.scrollHeight,u=this.useWin?ct(c,this.window[0],c):c,p=this.useWin?ct(d,this.window[1],d):d,m=r-u,f=n-p;this.dragged&&m>0&&(this.coords[0]-=m,this.scrollView[0]=u),this.dragged&&f>0&&(this.coords[1]-=f,this.scrollView[1]=p);const g=10*this.scrollSpeed,y=this.scrollThreshold,[_,v]=this.coords,[b,S,w,T]=this.scrollBounds,x=ut(ct((v-b+t)/y,-1,0)*g,0),k=ut(ct((_-S-e)/y,0,1)*g,0),$=ut(ct((v-w-s)/y,0,1)*g,0),C=ut(ct((_-T+i)/y,-1,0)*g,0);if(x||$||C||k){const[t,e]=this.disabled;let s=h,i=l;t||(s=ut(ct(h+(C||k),0,r-o),0),this.coords[0]-=h-s),e||(i=ut(ct(l+(x||$),0,n-a),0),this.coords[1]-=l-i),this.useWin?this.$scrollContainer.scrollBy(-(h-s),-(l-i)):this.$scrollContainer.scrollTo(s,i)}}const[t,e,s,i]=this.containerBounds,[r,n,o,a,h,l]=this.pointer;this.coords[0]+=(r-h)*this.dragSpeed,this.coords[1]+=(n-l)*this.dragSpeed,this.pointer[4]=r,this.pointer[5]=n;const[c,d]=this.coords,[u,p]=this.snapped,m=(1-this.containerFriction)*this.dragSpeed;this.setX(c>e?e+(c-e)*m:cs?s+(d-s)*m:d{this.canScroll=!1,this.$scrollContainer.scrollTo(n.x,n.y)}}).init().then(()=>{this.canScroll=a})}return this}handleHover(){this.isFinePointer&&this.cursor&&!this.cursorStyles&&(this.cursorStyles=Pe(this.$trigger,{cursor:this.cursor.onHover}))}animateInView(t,e=0,s=fe.inOutQuad){this.stop(),this.updateBoundingValues();const i=this.x,r=this.y,[n,o,a,h]=this.containerPadding,l=this.scroll.y-this.targetBounds[0]+n+e,c=this.scroll.x-this.targetBounds[1]-o-e,d=this.scroll.y-this.targetBounds[2]-a-e,u=this.scroll.x-this.targetBounds[3]+h+e,p=this.isOutOfBounds([l,c,d,u],i,r);if(p){const[e,n]=this.disabled,o=ct(pt(i,this.snapX),u,c),a=ct(pt(r,this.snapY),l,d),h=X(t)?350*N.timeScale:t;e||1!==p&&3!==p||this.animate[this.xProp](o,h,s),n||2!==p&&3!==p||this.animate[this.yProp](a,h,s)}return this}handleDown(t){const e=t.target;if(this.grabbed||"range"===e.type)return;t.stopPropagation(),this.grabbed=!0,this.released=!1,this.stop(),this.updateBoundingValues();const s=t.changedTouches,r=s?s[0].clientX:t.clientX,n=s?s[0].clientY:t.clientY,{x:o,y:a}=this.transforms.normalizePoint(r,n),[h,l,c,d]=this.containerBounds,u=(1-this.containerFriction)*this.dragSpeed,p=this.x,m=this.y;this.coords[0]=this.coords[2]=u?p>l?l+(p-l)/u:pc?c+(m-c)/u:mZe?f:Ze)+1,this.targetStyles=Pe(this.$target,{zIndex:Ze}),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.cursorStyles&&(this.cursorStyles.revert(),this.cursorStyles=null),this.isFinePointer&&this.cursor&&(this.bodyStyles=Pe(i.body,{cursor:this.cursor.onGrab})),this.scrollInView(100,0,fe.out(3)),this.onGrab(this),i.addEventListener("touchmove",this),i.addEventListener("touchend",this),i.addEventListener("touchcancel",this),i.addEventListener("mousemove",this),i.addEventListener("mouseup",this),i.addEventListener("selectstart",this)}handleMove(t){if(!this.grabbed)return;const e=t.changedTouches,s=e?e[0].clientX:t.clientX,i=e?e[0].clientY:t.clientY,{x:r,y:n}=this.transforms.normalizePoint(s,i),o=r-this.pointer[6],a=n-this.pointer[7];let h=t.target,l=!1,c=!1,d=!1;for(;e&&h&&h!==this.$trigger;){const t=Fe(h,"overflow-y");if("hidden"!==t&&"visible"!==t){const{scrollTop:t,scrollHeight:e,clientHeight:s}=h;if(e>s){d=!0,l=t<=3,c=t>=e-s-3;break}}h=h.parentElement}d&&(!l&&!c||l&&a<0||c&&a>0)?(this.pointer[0]=r,this.pointer[1]=n,this.pointer[2]=r,this.pointer[3]=n,this.pointer[4]=r,this.pointer[5]=n,this.pointer[6]=r,this.pointer[7]=n):(qe(t),this.triggerStyles||(this.triggerStyles=Pe(this.$trigger,{pointerEvents:"none"})),this.$trigger.addEventListener("touchstart",qe,{passive:!1}),this.$trigger.addEventListener("touchmove",qe,{passive:!1}),this.$trigger.addEventListener("touchend",qe),(this.dragged||!this.disabled[0]&&et(o)>this.dragThreshold||!this.disabled[1]&&et(a)>this.dragThreshold)&&(this.updateTicker.resume(),this.pointer[2]=this.pointer[0],this.pointer[3]=this.pointer[1],this.pointer[0]=r,this.pointer[1]=n,this.dragged=!0,this.released=!1,this.onDrag(this)))}handleUp(){if(!this.grabbed)return;this.updateTicker.pause(),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.bodyStyles&&(this.bodyStyles.revert(),this.bodyStyles=null);const[t,e]=this.disabled,[s,n,o,a,h,l]=this.pointer,[c,d,u,p]=this.containerBounds,[m,f]=this.snapped,g=this.releaseXSpring,y=this.releaseYSpring,_=this.releaseEase,v=this.hasReleaseSpring,b=this.overshootCoords,S=this.x,w=this.y,T=this.computeVelocity(s-h,n-l),x=this.angle=at(n-a,s-o),k=150*T,$=(1-this.releaseContainerFriction)*this.dragSpeed,C=S+tt(x)*k,E=w+K(x)*k,D=C>d?d+(C-d)*$:Cu?u+(E-u)*$:Ed?-1:1:Sz&&(z=P)}if(!e){const e=A===u?w>u?-1:1:wz&&(z=O)}if(!v&&F&&$&&(P||O)){const t=r.blend;new Be(b,{x:{to:D,duration:.65*P},y:{to:B,duration:.65*O},ease:_,composition:t}).init(),new Be(b,{x:{to:L,duration:P},y:{to:A,duration:O},ease:_,composition:t}).init(),this.overshootTicker.stretch(ot(P,O)).restart()}else t||this.animate[this.xProp](L,P,R),e||this.animate[this.yProp](A,O,M);this.scrollInView(z,this.scrollThreshold,_);let I=!1;L!==m&&(this.snapped[0]=L,this.snapX&&(I=!0)),A!==f&&this.snapY&&(this.snapped[1]=A,this.snapY&&(I=!0)),I&&this.onSnap(this),this.grabbed=!1,this.dragged=!1,this.updated=!0,this.released=!0,this.onRelease(this),this.$trigger.removeEventListener("touchstart",qe),this.$trigger.removeEventListener("touchmove",qe),this.$trigger.removeEventListener("touchend",qe),i.removeEventListener("touchmove",this),i.removeEventListener("touchend",this),i.removeEventListener("touchcancel",this),i.removeEventListener("mousemove",this),i.removeEventListener("mouseup",this),i.removeEventListener("selectstart",this)}reset(){return this.stop(),this.resizeTicker.pause(),this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.setX(0,!0),this.setY(0,!0),this.coords[0]=0,this.coords[1]=0,this.pointer[0]=0,this.pointer[1]=0,this.pointer[2]=0,this.pointer[3]=0,this.pointer[4]=0,this.pointer[5]=0,this.pointer[6]=0,this.pointer[7]=0,this.velocity=0,this.velocityStack[0]=0,this.velocityStack[1]=0,this.velocityStack[2]=0,this.velocityStackIndex=0,this.angle=0,this}enable(){return this.enabled||(this.enabled=!0,this.$target.classList.remove("is-disabled"),this.touchActionStyles=Pe(this.$trigger,{touchAction:this.disabled[0]?"pan-x":this.disabled[1]?"pan-y":"none"}),this.$trigger.addEventListener("touchstart",this,{passive:!0}),this.$trigger.addEventListener("mousedown",this,{passive:!0}),this.$trigger.addEventListener("mouseenter",this)),this}disable(){return this.enabled=!1,this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.touchActionStyles.revert(),this.cursorStyles&&(this.cursorStyles.revert(),this.cursorStyles=null),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.bodyStyles&&(this.bodyStyles.revert(),this.bodyStyles=null),this.targetStyles&&(this.targetStyles.revert(),this.targetStyles=null),this.$target.classList.add("is-disabled"),this.$trigger.removeEventListener("touchstart",this),this.$trigger.removeEventListener("mousedown",this),this.$trigger.removeEventListener("mouseenter",this),i.removeEventListener("touchmove",this),i.removeEventListener("touchend",this),i.removeEventListener("touchcancel",this),i.removeEventListener("mousemove",this),i.removeEventListener("mouseup",this),i.removeEventListener("selectstart",this),this}revert(){return this.reset(),this.disable(),this.$target.classList.remove("is-disabled"),this.updateTicker.revert(),this.overshootTicker.revert(),this.resizeTicker.revert(),this.animate.revert(),this.resizeObserver.disconnect(),this}handleEvent(t){switch(t.type){case"mousedown":case"touchstart":this.handleDown(t);break;case"mousemove":case"touchmove":this.handleMove(t);break;case"mouseup":case"touchend":case"touchcancel":this.handleUp();break;case"mouseenter":this.handleHover();break;case"selectstart":qe(t)}}}const Ke=(t=_)=>new se({duration:1*N.timeScale,onComplete:t},null,0).resume(),ts=t=>{let e;return(...s)=>{let i,r,n,o;e&&(i=e.currentIteration,r=e.iterationProgress,n=e.reversed,o=e._alternate,e.revert());const a=t(...s);return a&&!Y(a)&&a.revert&&(e=a),X(r)||(e.currentIteration=i,e.iterationProgress=(o&&i%2?!n:n)?1-r:r),a||_}};class es{constructor(t={}){L.current&&L.current.register(this);const e=t.root;let r=i;e&&(r=e.current||e.nativeElement||re(e)[0]||i);const n=t.defaults,o=N.defaults,a=t.mediaQueries;if(this.defaults=n?_t(n,o):o,this.root=r,this.constructors=[],this.revertConstructors=[],this.revertibles=[],this.constructorsOnce=[],this.revertConstructorsOnce=[],this.revertiblesOnce=[],this.once=!1,this.onceIndex=0,this.methods={},this.matches={},this.mediaQueryLists={},this.data={},a)for(let t in a){const e=s.matchMedia(a[t]);this.mediaQueryLists[t]=e,e.addEventListener("change",this)}}register(t){(this.once?this.revertiblesOnce:this.revertibles).push(t)}execute(t){let e=L.current,s=L.root,i=N.defaults;L.current=this,L.root=this.root,N.defaults=this.defaults;const r=this.mediaQueryLists;for(let t in r)this.matches[t]=r[t].matches;const n=t(this);return L.current=e,L.root=s,N.defaults=i,n}refresh(){return this.onceIndex=0,this.execute(()=>{let t=this.revertibles.length,e=this.revertConstructors.length;for(;t--;)this.revertibles[t].revert();for(;e--;)this.revertConstructors[e](this);this.revertibles.length=0,this.revertConstructors.length=0,this.constructors.forEach(t=>{const e=t(this);Y(e)&&this.revertConstructors.push(e)})}),this}add(t,e){if(this.once=!1,Y(t)){const e=t;this.constructors.push(e),this.execute(()=>{const t=e(this);Y(t)&&this.revertConstructors.push(t)})}else this.methods[t]=(...t)=>this.execute(()=>e(...t));return this}addOnce(t){if(this.once=!0,Y(t)){const e=this.onceIndex++;if(this.constructorsOnce[e])return this;const s=t;this.constructorsOnce[e]=s,this.execute(()=>{const t=s(this);Y(t)&&this.revertConstructorsOnce.push(t)})}return this}keepTime(t){this.once=!0;const e=this.onceIndex++,s=this.constructorsOnce[e];if(Y(s))return s(this);const i=ts(t);let r;return this.constructorsOnce[e]=i,this.execute(()=>{r=i(this)}),r}handleEvent(t){"change"===t.type&&this.refresh()}revert(){const t=this.revertibles,e=this.revertConstructors,s=this.revertiblesOnce,i=this.revertConstructorsOnce,r=this.mediaQueryLists;let n=t.length,o=e.length,a=s.length,h=i.length;for(;n--;)t[n].revert();for(;o--;)e[o](this);for(;a--;)s[a].revert();for(;h--;)i[h](this);for(let t in r)r[t].removeEventListener("change",this);t.length=0,e.length=0,this.constructors.length=0,s.length=0,i.length=0,this.constructorsOnce.length=0,this.onceIndex=0,this.matches={},this.methods={},this.mediaQueryLists={},this.data={}}}const ss=(t,e)=>t&&Y(t)?t(e):t,is=new Map;class rs{constructor(t){this.element=t,this.useWin=this.element===i.body,this.winWidth=0,this.winHeight=0,this.width=0,this.height=0,this.left=0,this.top=0,this.scale=1,this.zIndex=0,this.scrollX=0,this.scrollY=0,this.prevScrollX=0,this.prevScrollY=0,this.scrollWidth=0,this.scrollHeight=0,this.velocity=0,this.backwardX=!1,this.backwardY=!1,this.scrollTicker=new se({autoplay:!1,onBegin:()=>this.dataTimer.resume(),onUpdate:()=>{const t=this.backwardX||this.backwardY;vt(this,t=>t.handleScroll(),t)},onComplete:()=>this.dataTimer.pause()}).init(),this.dataTimer=new se({autoplay:!1,frameRate:30,onUpdate:t=>{const e=t.deltaTime,s=this.prevScrollX,i=this.prevScrollY,r=this.scrollX,n=this.scrollY,o=s-r,a=i-n;this.prevScrollX=r,this.prevScrollY=n,o&&(this.backwardX=s>r),a&&(this.backwardY=i>n),this.velocity=ut(e>0?Math.sqrt(o*o+a*a)/e:0,5)}}).init(),this.resizeTicker=new se({autoplay:!1,duration:250*N.timeScale,onComplete:()=>{this.updateWindowBounds(),this.refreshScrollObservers(),this.handleScroll()}}).init(),this.wakeTicker=new se({autoplay:!1,duration:500*N.timeScale,onBegin:()=>{this.scrollTicker.resume()},onComplete:()=>{this.scrollTicker.pause()}}).init(),this._head=null,this._tail=null,this.updateScrollCoords(),this.updateWindowBounds(),this.updateBounds(),this.refreshScrollObservers(),this.handleScroll(),this.resizeObserver=new ResizeObserver(()=>this.resizeTicker.restart()),this.resizeObserver.observe(this.element),(this.useWin?s:this.element).addEventListener("scroll",this,!1)}updateScrollCoords(){const t=this.useWin,e=this.element;this.scrollX=ut(t?s.scrollX:e.scrollLeft,0),this.scrollY=ut(t?s.scrollY:e.scrollTop,0)}updateWindowBounds(){this.winWidth=s.innerWidth,this.winHeight=(()=>{const t=i.createElement("div");i.body.appendChild(t),t.style.height="100lvh";const e=t.offsetHeight;return i.body.removeChild(t),e})()}updateBounds(){const t=getComputedStyle(this.element),e=this.element;let s,i;if(this.scrollWidth=e.scrollWidth+parseFloat(t.marginLeft)+parseFloat(t.marginRight),this.scrollHeight=e.scrollHeight+parseFloat(t.marginTop)+parseFloat(t.marginBottom),this.updateWindowBounds(),this.useWin)s=this.winWidth,i=this.winHeight;else{const t=e.getBoundingClientRect();s=e.clientWidth,i=e.clientHeight,this.top=t.top,this.left=t.left,this.scale=t.width?s/t.width:t.height?i/t.height:1}this.width=s,this.height=i}refreshScrollObservers(){vt(this,t=>{t._debug&&t.removeDebug()}),this.updateBounds(),vt(this,t=>{t.refresh(),t._debug&&t.debug()})}refresh(){this.updateWindowBounds(),this.updateBounds(),this.refreshScrollObservers(),this.handleScroll()}handleScroll(){this.updateScrollCoords(),this.wakeTicker.restart()}handleEvent(t){"scroll"===t.type&&this.handleScroll()}revert(){this.scrollTicker.cancel(),this.dataTimer.cancel(),this.resizeTicker.cancel(),this.wakeTicker.cancel(),this.resizeObserver.disconnect(),(this.useWin?s:this.element).removeEventListener("scroll",this),is.delete(this.element)}}const ns=(t,e,s,i,r)=>{const n="min"===e,o="max"===e,a="top"===e||"left"===e||"start"===e||n?0:"bottom"===e||"right"===e||"end"===e||o?"100%":"center"===e?"50%":e,{n:h,u:l}=Dt(a,Lt);let c=h;return"%"===l?c=h/100*s:l&&(c=he(t,Lt,"px",!0).n),o&&i<0&&(c+=i),n&&r>0&&(c+=r),c},os=(t,e,s,i,r)=>{let n;if(I(e)){const o=E.exec(e);if(o){const a=o[0],h=a[0],l=e.split(a),c="min"===l[0],d="max"===l[0],u=ns(t,l[0],s,i,r),p=ns(t,l[1],s,i,r);if(c){const e=Et(ns(t,"min",s),p,h);n=eu?u:e}else n=Et(u,p,h)}else n=ns(t,e,s,i,r)}else n=e;return ut(n,0)},as=t=>{let e;const s=t.targets;for(let t=0,i=s.length;t()=>{const e=this.linked;return e&&e[t]?e[t]():null}):null,l=a&&h.length>2;this.index=hs++,this.id=X(t.id)?this.index:t.id,this.container=(t=>{const e=t&&re(t)[0]||i.body;let s=is.get(e);return s||(s=new rs(e),is.set(e,s)),s})(t.container),this.target=null,this.linked=null,this.repeat=null,this.horizontal=null,this.enter=null,this.leave=null,this.sync=n||o||!!h,this.syncEase=n?s:null,this.syncSmooth=o?!0===e||r?1:e:null,this.onSyncEnter=h&&!l&&h[0]?h[0]:_,this.onSyncLeave=h&&!l&&h[1]?h[1]:_,this.onSyncEnterForward=h&&l&&h[0]?h[0]:_,this.onSyncLeaveForward=h&&l&&h[1]?h[1]:_,this.onSyncEnterBackward=h&&l&&h[2]?h[2]:_,this.onSyncLeaveBackward=h&&l&&h[3]?h[3]:_,this.onEnter=t.onEnter||_,this.onLeave=t.onLeave||_,this.onEnterForward=t.onEnterForward||_,this.onLeaveForward=t.onLeaveForward||_,this.onEnterBackward=t.onEnterBackward||_,this.onLeaveBackward=t.onLeaveBackward||_,this.onUpdate=t.onUpdate||_,this.onSyncComplete=t.onSyncComplete||_,this.reverted=!1,this.ready=!1,this.completed=!1,this.began=!1,this.isInView=!1,this.forceEnter=!1,this.hasEntered=!1,this.offset=0,this.offsetStart=0,this.offsetEnd=0,this.distance=0,this.prevProgress=0,this.thresholds=["start","end","end","start"],this.coords=[0,0,0,0],this.debugStyles=null,this.$debug=null,this._params=t,this._debug=Tt(t.debug,!1),this._next=null,this._prev=null,St(this.container,this),Ke(()=>{if(!this.reverted){if(!this.target){const e=re(t.target)[0];this.target=e||i.body,this.refresh()}this._debug&&this.debug()}})}link(t){if(t&&(t.pause(),this.linked=t,X(t)||(t.persist=!0),!this._params.target)){let e;X(t.targets)?vt(t,t=>{t.targets&&!e&&(e=as(t))}):e=as(t),this.target=e||i.body,this.refresh()}return this}get velocity(){return this.container.velocity}get backward(){return this.horizontal?this.container.backwardX:this.container.backwardY}get scroll(){return this.horizontal?this.container.scrollX:this.container.scrollY}get progress(){const t=(this.scroll-this.offsetStart)/this.distance;return t===1/0||isNaN(t)?0:ut(ct(t,0,1),6)}refresh(){this.ready=!0,this.reverted=!1;const t=this._params;return this.repeat=Tt(ss(t.repeat,this),!0),this.horizontal="x"===Tt(ss(t.axis,this),"y"),this.enter=Tt(ss(t.enter,this),"end start"),this.leave=Tt(ss(t.leave,this),"start end"),this.updateBounds(),this.handleScroll(),this}removeDebug(){return this.$debug&&(this.$debug.parentNode.removeChild(this.$debug),this.$debug=null),this.debugStyles&&(this.debugStyles.revert(),this.$debug=null),this}debug(){this.removeDebug();const t=this.container,e=this.horizontal,s=t.element.querySelector(":scope > .animejs-onscroll-debug"),r=i.createElement("div"),n=i.createElement("div"),o=i.createElement("div"),a=ls[this.index%ls.length],h=t.useWin,l=h?t.winWidth:t.width,c=h?t.winHeight:t.height,d=t.scrollWidth,u=t.scrollHeight,p=this.container.width>360?320:260,m=e?0:10,f=e?10:0,g=e?24:p/2,y=e?g:15,_=e?60:g,v=e?_:y,b=e?"repeat-x":"repeat-y",S=t=>e?"0px "+t+"px":t+"px 2px",w=t=>`linear-gradient(${e?90:0}deg, ${t} 2px, transparent 1px)`,T=(t,e,s,i,r)=>`position:${t};left:${e}px;top:${s}px;width:${i}px;height:${r}px;`;r.style.cssText=`${T("absolute",m,f,e?d:p,e?p:u)}\n pointer-events: none;\n z-index: ${this.container.zIndex++};\n display: flex;\n flex-direction: ${e?"column":"row"};\n filter: drop-shadow(0px 1px 0px rgba(0,0,0,.75));\n `,n.style.cssText=`${T("sticky",0,0,e?l:g,e?g:c)}`,s||(n.style.cssText+=`background:\n ${w("#FFFF")}${S(g-10)} / 100px 100px ${b},\n ${w("#FFF8")}${S(g-10)} / 10px 10px ${b};\n `),o.style.cssText=`${T("relative",0,0,e?d:g,e?g:u)}`,s||(o.style.cssText+=`background:\n ${w("#FFFF")}${S(0)} / ${e?"100px 10px":"10px 100px"} ${b},\n ${w("#FFF8")}${S(0)} / ${e?"10px 0px":"0px 10px"} ${b};\n `);const x=[" enter: "," leave: "];this.coords.forEach((t,s)=>{const r=s>1,h=(r?0:this.offset)+t,m=s%2,f=h(r?e?l:c:e?d:u)-v,b=(r?m&&!f:!m&&!f)||g,S=i.createElement("div"),w=i.createElement("div"),k=e?b?"right":"left":b?"bottom":"top",$=b?(e?_:y)+(r?e?-1:g?0:-2:e?-1:-2):e?1:0;w.innerHTML=`${this.id}${x[m]}${this.thresholds[s]}`,S.style.cssText=`${T("absolute",0,0,_,y)}\n display: flex;\n flex-direction: ${e?"column":"row"};\n justify-content: flex-${r?"start":"end"};\n align-items: flex-${b?"end":"start"};\n border-${k}: 2px solid ${a};\n `,w.style.cssText=`\n overflow: hidden;\n max-width: ${p/2-10}px;\n height: ${y};\n margin-${e?b?"right":"left":b?"bottom":"top"}: -2px;\n padding: 1px;\n font-family: ui-monospace, monospace;\n font-size: 10px;\n letter-spacing: -.025em;\n line-height: 9px;\n font-weight: 600;\n text-align: ${e&&b||!e&&!r?"right":"left"};\n white-space: pre;\n text-overflow: ellipsis;\n color: ${m?a:"rgba(0,0,0,.75)"};\n background-color: ${m?"rgba(0,0,0,.65)":a};\n border: 2px solid ${m?a:"transparent"};\n border-${e?b?"top-left":"top-right":b?"top-left":"bottom-left"}-radius: 5px;\n border-${e?b?"bottom-left":"bottom-right":b?"top-right":"bottom-right"}-radius: 5px;\n `,S.appendChild(w);let C=h-$+(e?1:0);S.style[e?"left":"top"]=`${C}px`,(r?n:o).appendChild(S)}),r.appendChild(n),r.appendChild(o),t.element.appendChild(r),s||r.classList.add("animejs-onscroll-debug"),this.$debug=r,"static"===Fe(t.element,"position")&&(this.debugStyles=Pe(t.element,{position:"relative "}))}updateBounds(){let t;this._debug&&this.removeDebug();const e=this.target,s=this.container,r=this.horizontal,n=this.linked;let o,a=e;for(n&&(o=n.currentTime,n.seek(0,!0)),a.parentElement;a&&a!==s.element&&a!==i.body;){const e="sticky"===Fe(a,"position")&&Pe(a,{position:"static"});a=a.parentElement,e&&(t||(t=[]),t.push(e))}const h=e.getBoundingClientRect(),l=s.scale,c=(r?h.left+s.scrollX-s.left:h.top+s.scrollY-s.top)*l,d=(r?h.width:h.height)*l,u=r?s.width:s.height,p=(r?s.scrollWidth:s.scrollHeight)-u,m=this.enter,f=this.leave;let g="start",y="end",_="end",v="start";if(I(m)){const t=m.split(" ");_=t[0],g=t.length>1?t[1]:g}else if(M(m)){const t=m;X(t.container)||(_=t.container),X(t.target)||(g=t.target)}else z(m)&&(_=m);if(I(f)){const t=f.split(" ");v=t[0],y=t.length>1?t[1]:y}else if(M(f)){const t=f;X(t.container)||(v=t.container),X(t.target)||(y=t.target)}else z(f)&&(v=f);const b=os(e,g,d),S=os(e,y,d),w=b+c-u,T=S+c-p,x=os(e,_,u,w,T),k=os(e,v,u,w,T),$=b+c-x,C=S+c-k,E=C-$;this.offset=c,this.offsetStart=$,this.offsetEnd=C,this.distance=E<=0?0:E,this.thresholds=[g,y,_,v],this.coords=[b,S,x,k],t&&t.forEach(t=>t.revert()),n&&n.seek(o,!0),this._debug&&this.debug()}handleScroll(){if(!this.ready)return;const t=this.linked,e=this.sync,s=this.syncEase,i=this.syncSmooth,r=t&&(s||i),n=this.horizontal,o=this.container,a=this.scroll,h=a<=this.offsetStart,l=a>=this.offsetEnd,c=!h&&!l,d=a===this.offsetStart||a===this.offsetEnd,u=!this.hasEntered&&d,p=this._debug&&this.$debug;let m=!1,f=!1,g=this.progress;if(h&&this.began&&(this.began=!1),g>0&&!this.began&&(this.began=!0),r){const e=t.progress;if(i&&z(i)){if(i<1){const t=1e-4,s=eg&&!g?-t:0;g=ut(mt(e,g,mt(.01,.2,i))+s,6)}}else s&&(g=s(g));m=g!==this.prevProgress,f=1===e,m&&!f&&i&&e&&o.wakeTicker.restart()}if(p){const t=n?o.scrollY:o.scrollX;p.style[n?"top":"left"]=t+10+"px"}(c&&!this.isInView||u&&!this.forceEnter&&!this.hasEntered)&&(c&&(this.isInView=!0),this.forceEnter&&this.hasEntered?c&&(this.forceEnter=!1):(p&&c&&(p.style.zIndex=""+this.container.zIndex++),this.onSyncEnter(this),this.onEnter(this),this.backward?(this.onSyncEnterBackward(this),this.onEnterBackward(this)):(this.onSyncEnterForward(this),this.onEnterForward(this)),this.hasEntered=!0,u&&(this.forceEnter=!0))),(c||!c&&this.isInView)&&(m=!0),m&&(r&&t.seek(t.duration*g),this.onUpdate(this)),!c&&this.isInView&&(this.isInView=!1,this.onSyncLeave(this),this.onLeave(this),this.backward?(this.onSyncLeaveBackward(this),this.onLeaveBackward(this)):(this.onSyncLeaveForward(this),this.onLeaveForward(this)),e&&!i&&(f=!0)),g>=1&&this.began&&!this.completed&&(e&&f||!e)&&(e&&this.onSyncComplete(this),this.completed=!0,(!this.repeat&&!t||!this.repeat&&t&&t.completed)&&this.revert()),g<1&&this.completed&&(this.completed=!1),this.prevProgress=g}revert(){if(this.reverted)return;const t=this.container;return bt(t,this),t._head||t.revert(),this._debug&&this.removeDebug(),this.reverted=!0,this.ready=!1,this}}const ds=(t,e,s)=>(((1-3*s+3*e)*t+(3*s-6*e))*t+3*e)*t,us=(t=.5,e=0,s=.5,i=1)=>t===e&&s===i?le:r=>0===r||1===r?r:ds(((t,e,s)=>{let i,r,n=0,o=1,a=0;do{r=n+(o-n)/2,i=ds(r,e,s)-t,i>0?o=r:n=r}while(et(i)>1e-7&&++a<100);return r})(r,t,s),e,i),ps=(t=10,e)=>{const s=e?it:rt;return e=>s(ct(e,0,1)*t)*(1/t)},ms=(...t)=>{const e=t.length;if(!e)return le;const s=e-1,i=t[0],r=t[s],n=[0],o=[Q(i)];for(let e=1;e{const s=[0],i=t-1;for(let t=1;t(...e)=>{const s=t(...e);return new Proxy(_,{apply:(t,e,[i])=>s(i),get:(t,e)=>vs((...t)=>{const i=_s[e](...t);return t=>i(s(t))})})},bs=(t,e,s=0)=>{const i=(...t)=>(t.length(...s)=>e?e=>t(...s,e):e=>t(e,...s))(e,s)):e)(...t);return _s[t]||(_s[t]=i),i},Ss=bs("roundPad",ys.roundPad),ws=bs("padStart",ys.padStart),Ts=bs("padEnd",ys.padEnd),xs=bs("wrap",ys.wrap),ks=bs("mapRange",ys.mapRange),$s=bs("degToRad",ys.degToRad),Cs=bs("radToDeg",ys.radToDeg),Es=bs("snap",ys.snap),Ds=bs("clamp",ys.clamp),Bs=bs("round",ys.round),Ls=bs("lerp",ys.lerp,1),Ns=bs("damp",ys.damp,1),As=(t=0,e=1,s=0)=>{const i=10**s;return Math.floor((Math.random()*(e-t+1/i)+t)*i)/i};let Fs=0;const Ps=(t,e=0,s=1,i=0)=>{let r=void 0===t?Fs++:t;return(t=e,n=s,o=i)=>{r+=1831565813,r=Math.imul(r^r>>>15,1|r),r^=r+Math.imul(r^r>>>7,61|r);const a=10**o;return Math.floor((((r^r>>>14)>>>0)/4294967296*(n-t+1/a)+t)*a)/a}},Os=t=>t[As(0,t.length-1)],Rs=t=>{let e,s,i=t.length;for(;i;)s=As(0,--i),e=t[i],t[i]=t[s],t[s]=e;return t},Ms=(t,e={})=>{let s=[],i=0;const r=e.from,n=e.reversed,o=e.ease,a=!X(o),h=a&&!X(o.ease)?o.ease:a?ve(o):null,l=e.grid,c=e.axis,d=e.total,u=X(r)||0===r||"first"===r,p="center"===r,f="last"===r,g="random"===r,y=R(t),_=e.use,v=Q(y?t[0]:t),b=y?Q(t[1]):0,S=k.exec((y?t[1]:t)+m),w=e.start||0+(y?v:0);let T=u?0:z(r)?r:0;return(t,r,o,a)=>{const[u]=ne(t),m=X(d)?o:d,x=!X(_)&&(Y(_)?_(u,r,m):Ct(u,_)),k=z(x)||I(x)&&z(+x)?+x:r;if(p&&(T=(m-1)/2),f&&(T=m-1),!s.length){for(let t=0;th(t/i)*i)),n&&(s=s.map(t=>c?t<0?-1*t:-t:et(i-t))),g&&(s=Rs(s))}const $=y?(b-v)/i:v;let C=(a?Re(a,X(e.start)?a.iterationDuration:w):w)+($*ut(s[k],2)||0);return e.modifier&&(C=e.modifier(C)),S&&(C=`${C}${S[2]}`),C}};var zs=Object.freeze({__proto__:null,$:ne,clamp:Ds,cleanInlineStyles:Ot,createSeededRandom:Ps,damp:Ns,degToRad:$s,get:Fe,keepTime:ts,lerp:Ls,mapRange:ks,padEnd:Ts,padStart:ws,radToDeg:Cs,random:As,randomPick:Os,remove:Oe,round:Bs,roundPad:Ss,set:Pe,shuffle:Rs,snap:Es,stagger:Ms,sync:Ke,wrap:xs});const Is=t=>{const e=re(t)[0];return e&&W(e)?e:console.warn(`${t} is not a valid SVGGeometryElement`)},Ys=(t,e,s,i,r)=>{const n=s+i,o=r?Math.max(0,Math.min(n,e)):(n%e+e)%e;return t.getPointAtLength(o)},Xs=(t,e,s=0)=>i=>{const r=+t.getTotalLength(),n=i[a],o=t.getCTM(),h=0===s;return{from:0,to:r,modifier:i=>{const a=i+s*r;if("a"===e){const e=Ys(t,r,a,-1,h),s=Ys(t,r,a,1,h);return 180*at(s.y-e.y,s.x-e.x)/ht}{const s=Ys(t,r,a,0,h);return"x"===e?n||!o?s.x:s.x*o.a+s.y*o.c+o.e:n||!o?s.y:s.x*o.b+s.y*o.d+o.f}}}},Vs=(t,e=0)=>{const s=Is(t);if(s)return{translateX:Xs(s,"x",e),translateY:Xs(s,"y",e),rotate:Xs(s,"a",e)}},Ws=(t,e=0,s=0)=>re(t).map(t=>((t,e,s)=>{const i=p,r=getComputedStyle(t),n=r.strokeLinecap,o="non-scaling-stroke"===r.vectorEffect?t:null;let a=n;const h=new Proxy(t,{get(t,e){const s=t[e];return e===c?t:"setAttribute"===e?(...e)=>{if("draw"===e[0]){const s=e[1].split(" "),r=+s[0],h=+s[1],l=(t=>{let e=1;if(t&&t.getCTM){const s=t.getCTM();s&&(e=(J(s.a*s.a+s.b*s.b)+J(s.c*s.c+s.d*s.d))/2)}return e})(o),c=-1e3*r*l,d=h*i*l+c,u=i*l+(0===r&&1===h||1===r&&0===h?0:10*l)-d;if("butt"!==n){const e=r===h?"butt":n;a!==e&&(t.style.strokeLinecap=`${e}`,a=e)}t.setAttribute("stroke-dashoffset",`${c}`),t.setAttribute("stroke-dasharray",`${d} ${u}`)}return Reflect.apply(s,t,e)}:Y(s)?(...e)=>Reflect.apply(s,t,e):s}});return"1000"!==t.getAttribute("pathLength")&&(t.setAttribute("pathLength","1000"),h.setAttribute("draw",`${e} ${s}`)),h})(t,e,s)),Hs=(t,e=.33)=>s=>{if(!(s.tagName||"").toLowerCase().match(/^(path|polygon|polyline)$/))throw new Error(`Can't morph a <${s.tagName}> SVG element. Use , or .`);const i=Is(t);if(!i)throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing , or SVG element.");if(!(i.tagName||"").toLowerCase().match(/^(path|polygon|polyline)$/))throw new Error(`Can't morph a <${i.tagName}> SVG element. Use , or .`);const r="path"===s.tagName,n=r?" ":",",o=s[l];o&&s.setAttribute(r?"d":"points",o);let a="",h="";if(e){const t=s.getTotalLength(),o=i.getTotalLength(),l=Math.max(Math.ceil(t*e),Math.ceil(o*e));for(let e=0;et.isWordLike||" "===t.segment||z(+t.segment),oi=t=>t.setAttribute("aria-hidden","true"),ai=(t,e)=>[...t.querySelectorAll(`[data-${e}]:not([data-${e}] [data-${e}])`)],hi={line:"#00D672",word:"#FF4B4B",char:"#5A87FF"},li=t=>{if(!t.childElementCount&&!t.textContent.trim()){const e=t.parentElement;t.remove(),e&&li(e)}},ci=(t,e,s)=>{const i=t.getAttribute(ei);(null!==i&&+i!==e||"BR"===t.tagName)&&s.add(t);let r=t.childElementCount;for(;r--;)ci(t.children[r],e,s);return s},di=(t,e={})=>{let s="";const i=I(e.class)?` class="${e.class}"`:"",r=Tt(e.clone,!1),n=Tt(e.wrap,!1),o=n?!0===n?"clip":n:!!r&&"clip";return n&&(s+=``),s+=``,r?(s+="{value}",s+=`{value}`):s+="{value}",s+="",n&&(s+=""),s},ui=(t,e,s,i,r,n,o,a,h)=>{const l=r===Js,c=r===ti,d=`_${r}_`,u=Y(t)?t(s):t,p=l?"block":"inline-block";ri.innerHTML=u.replace(js,``).replace(Gs,`${c?h:l?o:a}`);const m=ri.content,f=m.firstElementChild,g=m.querySelector(`[data-${r}]`)||f,y=m.querySelectorAll(`i.${d}`),_=y.length;if(_){f.style.display=p,g.style.display=p,g.setAttribute(ei,`${o}`),l||(g.setAttribute("data-word",`${a}`),c&&g.setAttribute("data-char",`${h}`));let t=_;for(;t--;){const e=y[t],i=e.parentElement;i.style.display=p,l?i.innerHTML=s.innerHTML:i.replaceChild(s.cloneNode(!0),e)}e.push(g),i.appendChild(m)}else console.warn('The expression "{value}" is missing from the provided template.');return n&&(f.style.outline=`1px dotted ${hi[r]}`),f};class pi{constructor(t,s={}){si||(si=qs?new qs([],{granularity:Ks}):{segment:t=>{const e=[],s=t.split(Qs);for(let t=0,i=s.length;t[...t].map(t=>({segment:t}))}),!ri&&e&&(ri=i.createElement("template")),L.current&&L.current.register(this);const{words:r,chars:n,lines:o,accessible:a,includeSpaces:h,debug:l}=s,c=(t=R(t)?t[0]:t)&&t.nodeType?t:(ie(t)||[])[0],d=!0===o?{}:o,u=!0===r||X(r)?{}:r,p=!0===n?{}:n;this.debug=Tt(l,!1),this.includeSpaces=Tt(h,!1),this.accessible=Tt(a,!0),this.linesOnly=d&&!u&&!p,this.lineTemplate=M(d)?di(Js,d):d,this.wordTemplate=M(u)||this.linesOnly?di(Ks,u):u,this.charTemplate=M(p)?di(ti,p):p,this.$target=c,this.html=c&&c.innerHTML,this.lines=[],this.words=[],this.chars=[],this.effects=[],this.effectsCleanups=[],this.cache=null,this.ready=!1,this.width=0,this.resizeTimeout=null;const m=()=>this.html&&(d||u||p)&&this.split();this.resizeObserver=new ResizeObserver(()=>{clearTimeout(this.resizeTimeout),this.resizeTimeout=setTimeout(()=>{const t=c.offsetWidth;t!==this.width&&(this.width=t,m())},150)}),this.lineTemplate&&!this.ready?i.fonts.ready.then(m):m(),c?this.resizeObserver.observe(c):console.warn("No Text Splitter target found.")}addEffect(t){if(!Y(t))return console.warn("Effect must return a function.");const e=ts(t);return this.effects.push(e),this.ready&&(this.effectsCleanups[this.effects.length-1]=e(this)),this}revert(){return clearTimeout(this.resizeTimeout),this.lines.length=this.words.length=this.chars.length=0,this.resizeObserver.disconnect(),this.effectsCleanups.forEach(t=>Y(t)?t(this):t.revert&&t.revert()),this.$target.innerHTML=this.html,this}splitNode(t){const e=this.wordTemplate,s=this.charTemplate,r=this.includeSpaces,n=this.debug,o=t.nodeType;if(3===o){const o=t.nodeValue;if(o.trim()){const a=[],h=this.words,l=this.chars,c=si.segment(o),d=i.createDocumentFragment();let u=null;for(const t of c){const e=t.segment,s=ni(t);if(!u||s&&u&&ni(u))a.push(e);else{const t=a.length-1;a[t].includes(" ")||e.includes(" ")?a.push(e):a[t]+=e}u=t}for(let t=0,o=a.length;tY(t)&&t(this)),s||(t&&(e.innerHTML=this.html,this.words.length=this.chars.length=0),this.splitNode(e),this.cache=e.innerHTML),h&&(s&&(e.innerHTML=this.cache),this.lines.length=0,n&&(this.words=ai(e,Ks))),o&&(h||n)&&(this.chars=ai(e,ti));const l=this.words.length?this.words:this.chars;let c,d=0;for(let t=0,e=l.length;t.5*i&&d++,e.setAttribute(ei,`${d}`);const r=e.querySelectorAll(`[${ei}]`);let n=r.length;for(;n--;)r[n].setAttribute(ei,`${d}`);c=s}if(h){const t=i.createDocumentFragment(),s=new Set,a=[];for(let t=0;t{const e=t.parentElement;e&&s.add(e),t.remove()}),a.push(i)}s.forEach(li);for(let e=0,s=a.length;ethis.effectsCleanups[e]=t(this)),this}refresh(){this.split(!0)}}const mi=(t,e)=>new pi(t,e),fi=(t,e)=>(console.warn("text.split() is deprecated, import splitText() directly, or text.splitText()"),new pi(t,e));var gi=Object.freeze({__proto__:null,TextSplitter:pi,split:fi,splitText:mi});const yi=(t,e=100)=>{const s=[];for(let i=0;i<=e;i++)s.push(ut(t(i/e),4));return`linear(${s.join(", ")})`},_i={},vi=t=>{let e=_i[t];if(e)return e;if(e="linear",I(t)){if(P(t,"linear")||P(t,"cubic-")||P(t,"steps")||P(t,"ease"))e=t;else if(P(t,"cubicB"))e=F(t);else{const s=ye(t);Y(s)&&(e=s===le?"linear":yi(s))}_i[t]=e}else if(Y(t)){const s=yi(t);s&&(e=s)}else t.ease&&(e=yi(t.ease));return e},bi=["x","y","z"],Si=["perspective","width","height","margin","padding","top","right","bottom","left","borderWidth","fontSize","borderRadius",...bi],wi=(()=>[...bi,...g.filter(t=>["X","Y","Z"].some(e=>t.endsWith(e)))])();let Ti=null;const xi=(t,e,s,i,r)=>{let n=I(e)?e:xt(e,s,i,r);return z(n)?Si.includes(t)||P(t,"translate")?`${n}px`:P(t,"rotate")||P(t,"skew")?`${n}deg`:`${n}`:n},ki=(t,e,s,i,r,n)=>{let o="0";const a=X(i)?getComputedStyle(t)[e]:xi(e,i,t,r,n);return o=X(s)?R(i)?i.map(s=>xi(e,s,t,r,n)):a:[xi(e,s,t,r,n),a],o};class $i{constructor(t,s){L.current&&L.current.register(this),V(Ti)&&(!e||!X(CSS)&&Object.hasOwnProperty.call(CSS,"registerProperty")?(g.forEach(t=>{const e=P(t,"skew"),s=P(t,"scale"),i=P(t,"rotate"),r=P(t,"translate"),n=i||e,o=n?"":s?"":r?"":"*";try{CSS.registerProperty({name:"--"+t,syntax:o,inherits:!1,initialValue:r?"0px":n?"0deg":s?"1":"0"})}catch{}}),Ti=!0):Ti=!1);const i=ne(t),r=i.length;r||console.warn("No target found. Make sure the element you're trying to animate is accessible before creating your animation.");const n=Tt(s.ease,vi(N.defaults.ease)),o=n.ease&&n,a=Tt(s.autoplay,N.defaults.autoplay),l=!(!a||!a.link)&&a,c=s.alternate&&!0===s.alternate,d=s.reversed&&!0===s.reversed,u=Tt(s.loop,N.defaults.loop),v=!0===u||u===1/0?1/0:z(u)?u+1:1,b=c?d?"alternate-reverse":"alternate":d?"reverse":"normal",S=vi(n),w=1===N.timeScale?1:p;this.targets=i,this.animations=[],this.controlAnimation=null,this.onComplete=s.onComplete||N.defaults.onComplete,this.duration=0,this.muteCallbacks=!1,this.completed=!1,this.paused=!a||!1!==l,this.reversed=d,this.persist=Tt(s.persist,N.defaults.persist),this.autoplay=a,this._speed=Tt(s.playbackRate,N.defaults.playbackRate),this._resolve=_,this._completed=0,this._inlineStyles=[],i.forEach((t,e)=>{const i=t[h],a=wi.some(t=>s.hasOwnProperty(t)),l=t.style,c=this._inlineStyles[e]={},d=(o?o.settlingDuration:xt(Tt(s.duration,N.defaults.duration),t,e,r))*w,u=xt(Tt(s.delay,N.defaults.delay),t,e,r)*w,p=Tt(s.composition,"replace");for(let o in s){if(!j(o))continue;const h={},m={iterations:v,direction:b,fill:"both",easing:S,duration:d,delay:u,composite:p},y=s[o],_=!!a&&(g.includes(o)?o:f.get(o)),T=_?"transform":o;let x;if(c[T]||(c[T]=l[T]),M(y)){const s=y,a=Tt(s.ease,n),c=a.ease&&a,f=s.to,g=s.from;if(m.duration=(c?c.settlingDuration:xt(Tt(s.duration,d),t,e,r))*w,m.delay=xt(Tt(s.delay,u),t,e,r)*w,m.composite=Tt(s.composition,p),m.easing=vi(a),x=ki(t,o,g,f,e,r),_?(h[`--${_}`]=x,i[_]=x):h[o]=ki(t,o,g,f,e,r),Ae(this,t,o,h,m),!X(g))if(_){const t=`--${_}`;l.setProperty(t,h[t][0])}else l[o]=h[o][0]}else x=R(y)?y.map(s=>xi(o,s,t,e,r)):xi(o,y,t,e,r),_?(h[`--${_}`]=x,i[_]=x):h[o]=x,Ae(this,t,o,h,m)}if(a){let t=m;for(let e in i)t+=`${y[e]}var(--${e})) `;l.transform=t}}),l&&this.autoplay.link(this)}forEach(t){const e=I(t)?e=>e[t]():t;return this.animations.forEach(e),this}get speed(){return this._speed}set speed(t){this._speed=+t,this.forEach(e=>e.playbackRate=t)}get currentTime(){const t=this.controlAnimation,e=N.timeScale;return this.completed?this.duration:t?+t.currentTime*(1===e?1:e):0}set currentTime(t){const e=t*(1===N.timeScale?1:p);this.forEach(t=>{!this.persist&&e>=this.duration&&t.play(),t.currentTime=e})}get progress(){return this.currentTime/this.duration}set progress(t){this.forEach(e=>e.currentTime=t*this.duration||0)}resume(){return this.paused?(this.paused=!1,this.forEach("play")):this}pause(){return this.paused?this:(this.paused=!0,this.forEach("pause"))}alternate(){return this.reversed=!this.reversed,this.forEach("reverse"),this.paused&&this.forEach("pause"),this}play(){return this.reversed&&this.alternate(),this.resume()}reverse(){return this.reversed||this.alternate(),this.resume()}seek(t,e=!1){return e&&(this.muteCallbacks=!0),t{const s=t.style,i=this._inlineStyles[e];for(let t in i){const e=i[t];X(e)||e===m?s.removeProperty(F(t)):s[t]=e}t.getAttribute("style")===m&&t.removeAttribute("style")}),this}then(t=_){const e=this.then,s=()=>{this.then=null,t(this),this.then=e,this._resolve=_};return new Promise(t=>(this._resolve=()=>t(s()),this.completed&&this._resolve(),this))}}const Ci={animate:(t,e)=>new $i(t,e),convertEase:yi};t.$=ne,t.Animatable=Ie,t.Draggable=Je,t.JSAnimation=Be,t.Scope=es,t.ScrollObserver=cs,t.Spring=We,t.TextSplitter=pi,t.Timeline=ze,t.Timer=se,t.WAAPIAnimation=$i,t.animate=(t,e)=>new Be(t,e,null,0,!1).init(),t.clamp=Ds,t.cleanInlineStyles=Ot,t.createAnimatable=(t,e)=>new Ie(t,e),t.createDraggable=(t,e)=>new Je(t,e),t.createDrawable=Ws,t.createMotionPath=Vs,t.createScope=t=>new es(t),t.createSeededRandom=Ps,t.createSpring=Ue,t.createTimeline=t=>new ze(t).init(),t.createTimer=t=>new se(t,null,0).init(),t.cubicBezier=us,t.damp=Ns,t.degToRad=$s,t.eases=fe,t.easings=gs,t.engine=Xt,t.get=Fe,t.irregular=fs,t.keepTime=ts,t.lerp=Ls,t.linear=ms,t.mapRange=ks,t.morphTo=Hs,t.onScroll=(t={})=>new cs(t),t.padEnd=Ts,t.padStart=ws,t.radToDeg=Cs,t.random=As,t.randomPick=Os,t.remove=Oe,t.round=Bs,t.roundPad=Ss,t.scrollContainers=is,t.set=Pe,t.shuffle=Rs,t.snap=Es,t.split=fi,t.splitText=mi,t.spring=He,t.stagger=Ms,t.steps=ps,t.svg=Us,t.sync=Ke,t.text=gi,t.utils=zs,t.waapi=Ci,t.wrap=xs}); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).anime={})}(this,function(t){"use strict";const e="undefined"!=typeof window,s=e?window:null,i=e?document:null,r={replace:0,none:1,blend:2},n=Symbol(),o=Symbol(),a=Symbol(),l=Symbol(),h=Symbol(),d=1e-11,c=1e12,u=1e3,p="",m=(()=>{const t=new Map;return t.set("x","translateX"),t.set("y","translateY"),t.set("z","translateZ"),t})(),f=["perspective","translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY"],g=f.reduce((t,e)=>({...t,[e]:e+"("}),{}),y=()=>{},v=/\)\s*[-.\d]/,b=/(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i,_=/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i,T=/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,x=/hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i,w=/hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,S=/[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi,$=/^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i,C=/([a-z])([A-Z])/g,E=/(\*=|\+=|-=)/,k=/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/,N={id:null,keyframes:null,playbackEase:null,playbackRate:1,frameRate:240,loop:0,reversed:!1,alternate:!1,autoplay:!0,persist:!1,duration:u,delay:0,loopDelay:0,ease:"out(2)",composition:r.replace,modifier:t=>t,onBegin:y,onBeforeUpdate:y,onUpdate:y,onLoop:y,onPause:y,onComplete:y,onRender:y},D={current:null,root:i},A={defaults:N,precision:4,timeScale:1,tickThreshold:200,editor:null},I={version:"4.4.1",engine:null};e&&(s.AnimeJS||(s.AnimeJS=[]),s.AnimeJS.push(I));const R=t=>t.replace(C,"$1-$2").toLowerCase(),L=(t,e)=>0===t.indexOf(e),B=Date.now,P=Array.isArray,F=t=>t&&t.constructor===Object,M=t=>"number"==typeof t&&!isNaN(t),V=t=>"string"==typeof t,O=t=>"function"==typeof t,z=t=>void 0===t,H=t=>z(t)||null===t,X=t=>e&&t instanceof SVGElement,Y=t=>b.test(t),W=t=>L(t,"rgb"),U=t=>L(t,"hsl"),q=t=>!A.defaults.hasOwnProperty(t),j=["opacity","rotate","overflow","color"],G=t=>V(t)?parseFloat(t):t,Z=Math.pow,Q=Math.sqrt,J=Math.sin,K=Math.cos,tt=Math.abs,et=Math.exp,st=Math.ceil,it=Math.floor,rt=Math.asin,nt=Math.max,ot=Math.atan2,at=Math.PI,lt=Math.round,ht=(t,e,s)=>ts?s:t,dt=(t,e)=>{if(e<0)return t;if(!e)return lt(t);const s=10**e;return lt(t*s)/s},ct=(t,e)=>P(e)?e.reduce((e,s)=>tt(s-t)t+(e-t)*s,pt=t=>t===1/0?c:t===-1/0?-c:t,mt=t=>t<=d?d:pt(dt(t,11)),ft=t=>P(t)?[...t]:t,gt=(t,e)=>{const s={...t};for(let i in e){const r=t[i];s[i]=z(r)?e[i]:r}return s},yt=(t,e,s,i="_prev",r="_next")=>{let n=t._head,o=r;for(s&&(n=t._tail,o=i);n;){const t=n[o];e(n),n=t}},vt=(t,e,s="_prev",i="_next")=>{const r=e[s],n=e[i];r?r[i]=n:t._head=n,n?n[s]=r:t._tail=r,e[s]=null,e[i]=null},bt=(t,e,s,i="_prev",r="_next")=>{let n=t._tail;for(;n&&s&&s(n,e);)n=n[i];const o=n?n[r]:t._head;n?n[r]=e:t._head=e,o?o[i]=e:t._tail=e,e[i]=n,e[r]=o},_t=t=>{let e=p;for(let s=0,i=f.length;s(s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+(e-t)*(2/3-s)*6:t),xt=(t,e)=>z(t)?e:t,wt=(t,e,s,i,r,n)=>{let o;if(O(t))o=()=>{const r=t(e,s,i,n);return isNaN(+r)?r||0:+r};else{if(!V(t)||!L(t,"var("))return t;o=()=>{const s=t.match(k),i=s[1],r=s[2];let n=getComputedStyle(e)?.getPropertyValue(i);return n&&n.trim()!==p||!r||(n=r.trim()),n||0}}return r&&(r.func=o),o()},St=(t,e)=>t[o]?t[a]&&((t,e)=>{if(j.includes(e))return!1;if(t.getAttribute(e)||e in t){if("scale"===e){const e=t.parentNode;return e&&"filter"===e.tagName}return!0}})(t,e)?1:f.includes(e)||m.get(e)?3:L(e,"--")?4:e in t.style?2:e in t?0:1:0,$t=(t,e,s)=>{const i=t.style[e];i&&s&&(s[e]=i);const r=i||getComputedStyle(t[h]||t).getPropertyValue(e);return"auto"===r?"0":r},Ct=(t,e,s,i)=>{const r=z(s)?St(t,e):s;if(0===r){const s=t[e];return s&&i&&(i[e]=s),s||0}if(1===r){const s=t.getAttribute(e);return s&&i&&(i[e]=s),s}return 3===r?((t,e,s)=>{const i=t.style.transform;if(i){const r=t[l];let n=0;const o=i.length;let a;for(;n=o)break;const t=n;for(;n=o)break;const e=i.substring(t,n);let s=1;const l=n+1;let h=-1,d=-1;for(n++;n0;){const t=i.charCodeAt(n);40===t?s++:41===t?s--:44===t&&1===s&&(-1===h?h=n:-1===d&&(d=n)),n++}const c=n-1;"translate"===e||"translate3d"===e?(-1===h?r.translateX=i.substring(l,c).trim():(r.translateX=i.substring(l,h).trim(),-1===d?r.translateY=i.substring(h+1,c).trim():(r.translateY=i.substring(h+1,d).trim(),r.translateZ=i.substring(d+1,c).trim())),a=i.substring(l,c)):"scale"===e||"scale3d"===e?-1===h?r.scale=i.substring(l,c).trim():(r.scaleX=i.substring(l,h).trim(),-1===d?r.scaleY=i.substring(h+1,c).trim():(r.scaleY=i.substring(h+1,d).trim(),r.scaleZ=i.substring(d+1,c).trim())):r[e]=i.substring(l,c)}if("translate3d"===e&&a)return s&&(s[e]=a),a;const h=r[e];if(!z(h))return s&&(s[e]=h),h}return"translate3d"===e?"0px, 0px, 0px":"rotate3d"===e?"0, 0, 0, 0deg":L(e,"scale")?"1":L(e,"rotate")||L(e,"skew")?"0deg":"0px"})(t,e,i):4===r?$t(t,e,i).trimStart():$t(t,e,i)},Et=(t,e,s)=>"-"===s?t-e:"+"===s?t+e:t*e,kt=(t,e)=>{if(e.t=0,e.n=0,e.u=null,e.o=null,e.d=null,e.s=null,!t)return e;const s=+t;if(isNaN(s)){let s=t;"="===s[1]&&(e.o=s[0],s=s.slice(2));const n=!s.includes(" ")&&$.exec(s);if(n)return e.t=1,e.n=+n[1],e.u=n[2],e;if(e.o)return e.n=+s,e;if(Y(r=s)||(W(r)||U(r))&&(")"===r[r.length-1]||!v.test(r)))return e.t=2,e.d=W(i=s)?(t=>{const e=_.exec(t)||T.exec(t),s=z(e[4])?1:+e[4];return[+e[1],+e[2],+e[3],s]})(i):Y(i)?(t=>{const e=t.length,s=4===e||5===e;return[+("0x"+t[1]+t[s?1:2]),+("0x"+t[s?2:3]+t[s?2:4]),+("0x"+t[s?3:5]+t[s?3:6]),5===e||9===e?+(+("0x"+t[s?4:7]+t[s?4:8])/255).toFixed(3):1]})(i):U(i)?(t=>{const e=x.exec(t)||w.exec(t),s=+e[1]/360,i=+e[2]/100,r=+e[3]/100,n=z(e[4])?1:+e[4];let o,a,l;if(0===i)o=a=l=r;else{const t=r<.5?r*(1+i):r+i-r*i,e=2*r-t;o=dt(255*Tt(e,t,s+1/3),0),a=dt(255*Tt(e,t,s),0),l=dt(255*Tt(e,t,s-1/3),0)}return[o,a,l,n]})(i):[0,0,0,1],e;{const t=s.match(S);return e.t=3,e.d=t?t.map(Number):[],e.s=s.split(S)||[],e}}var i,r;return e.n=s,e},Nt=(t,e)=>(e.t=t._valueType,e.n=t._toNumber,e.u=t._unit,e.o=null,e.d=ft(t._toNumbers),e.s=ft(t._strings),e),Dt={t:0,n:0,u:null,o:null,d:null,s:null},At=(t,e,s)=>{const i=t._modifier,n=t._fromNumbers,o=t._toNumbers,a=dt(ht(i(ut(n[0],o[0],e)),0,255),0),l=dt(ht(i(ut(n[1],o[1],e)),0,255),0),h=dt(ht(i(ut(n[2],o[2],e)),0,255),0),d=ht(i(dt(ut(n[3],o[3],e),s)),0,1);if(t._composition!==r.none){const e=t._numbers;e[0]=a,e[1]=l,e[2]=h,e[3]=d}return`rgba(${a},${l},${h},${d})`},It=(t,e,s)=>{const i=t._modifier,n=t._fromNumbers,o=t._toNumbers,a=t._strings,l=t._composition!==r.none;let h=a[0];for(let r=0,d=o.length;r{const o=t.parent,a=t.duration,h=t.completed,c=t.iterationDuration,u=t.iterationCount,p=t._currentIteration,m=t._loopDelay,f=t._reversed,g=t._alternate,y=t._hasChildren,v=t._delay,b=t._currentTime,_=v+c,T=e-v,x=ht(b,-v,a),w=ht(T,-v,a),S=T-b,$=w>0,C=w>=a,E=a<=d,k=2===n;let N=0,D=T,I=0;if(u>1){const e=~~(w/(c+(C?0:m)));t._currentIteration=ht(e,0,u),C&&t._currentIteration--,N=t._currentIteration%2,D=w%(c+m)||0}const R=f^(g&&N),L=t._ease;let B=C?R?0:a:R?c-D:D;L&&(B=c*L(B/c)||0);const P=(o?o.backwards:T=v&&e<=_||e<=v&&x>v||e>=_&&x!==a)||B>=_&&x!==a||B<=v&&x>0||e<=x&&x===a&&h||C&&!h&&E){if($&&(t.computeDeltaTime(x),s||t.onBeforeUpdate(t)),!y){const e=k||(P?-1*S:S)>=A.tickThreshold,n=t._offset+(o?o._offset:0)+v+B;let a,h,d,c,u=t._head,p=0;for(;u;){const t=u._composition,s=u._currentTime,o=u._changeDuration,m=u._absoluteStartTime+u._changeDuration,f=u._nextRep,g=u._prevRep,y=t!==r.none;if((e||(s!==o||n<=m+(f?f._delay:0))&&(0!==s||n>=u._absoluteStartTime))&&(!y||!u._isOverridden&&(!u._isOverlapped||n<=m)&&(!f||f._isOverridden||n<=f._absoluteStartTime)&&(!g||g._isOverridden||n>=g._absoluteStartTime+g._changeDuration+u._delay))){const e=u._currentTime=ht(B-u._startTime,0,o),s=u._ease(e/u._updateDuration),n=u._modifier,m=u._valueType,f=u._tweenType,g=0===f,v=0===m,b=v&&g||0===s||1===s?-1:A.precision;let _,T;if(v?_=T=n(dt(ut(u._fromNumber,u._toNumber,s),b)):1===m?(T=n(dt(ut(u._fromNumber,u._toNumber,s),b)),_=`${T}${u._unit}`):2===m?_=At(u,s,b):3===m&&(_=It(u,s,b)),y&&(u._number=T),i||t===r.blend)u._value=_;else{const t=u.property;a=u.target,g?a[t]=_:1===f?a.setAttribute(t,_):(h=a.style,3===f?(a!==d&&(d=a,c=a[l]),c[t]=_,p=1):2===f?h[t]=_:4===f&&h.setProperty(t,_)),$&&(I=1)}}p&&u._renderTransforms&&(h.transform=_t(c),p=0),u=u._next}!s&&I&&t.onRender(t)}!s&&$&&t.onUpdate(t)}return o&&E?!s&&(o.began&&!P&&T>0&&!h||P&&T<=d&&h)&&(t.onComplete(t),t.completed=!P):$&&C?u===1/0?t._startTime+=t.duration:t._currentIteration>=u-1&&(t.paused=!0,h||y||(t.completed=!0,s||o&&(P||!o.began)||(t.onComplete(t),t._resolve(t)))):t.completed=!1,I},Lt=(t,e,s,i,r)=>{const n=t._currentIteration;if(Rt(t,e,s,i,r),t._hasChildren){const o=t,a=o.backwards,l=i?e:o._iterationTime,h=B();let c=0,u=!0;if(!i&&o._currentIteration!==n){const t=o.iterationDuration;yt(o,e=>{if(a){const i=e.duration,r=e._offset+e._delay;s||!(i<=d)||r&&r+i!==t||e.onComplete(e)}else!e.completed&&!e.backwards&&e._currentTime{const e=dt((l-t._offset)*t._speed,12),n=t._fps=o.duration&&(o.paused=!0,o.completed||(o.completed=!0,s||(o.onComplete(o),o._resolve(o))))}},Bt={},Pt=(t,e,s)=>{if(3===s)return m.get(t)||t;if(2===s||1===s&&X(e)&&t in e.style){const e=Bt[t];if(e)return e;{const e=t?R(t):t;return Bt[t]=e,e}}return t},Ft=(t,e=!1)=>{if(t._hasChildren)yt(t,t=>Ft(t,e),!0);else{const s=t;s.pause(),yt(s,t=>{const i=t.property,r=t.target,n=t._tweenType,a=t._inlineValue,h=H(a)||a===p;if(0===n)e||h||(r[i]=a);else if(r[o])if(1===n)e||(h?r.removeAttribute(i):r.setAttribute(i,a));else{const e=r.style;if(3===n){const s=r[l];h?delete s[i]:s[i]=a,t._renderTransforms&&(Object.keys(s).length?e.transform=_t(s):e.removeProperty("transform"))}else h?e.removeProperty(R(i)):e[i]=a}r[o]&&s._tail===t&&s.targets.forEach(t=>{t.getAttribute&&t.getAttribute("style")===p&&t.removeAttribute("style")})})}return t},Mt=t=>Ft(t,!0);class Vt{constructor(t=0){this.deltaTime=0,this._currentTime=t,this._lastTickTime=t,this._startTime=t,this._lastTime=t,this._scheduledTime=0,this._frameDuration=u/240,this._fps=240,this._speed=1,this._hasChildren=!1,this._head=null,this._tail=null}get fps(){return this._fps}set fps(t){const e=this._frameDuration,s=+t,i=sN.frameRate&&(N.frameRate=i),this._fps=i,this._frameDuration=r,this._scheduledTime+=r-e}get speed(){return this._speed}set speed(t){const e=+t;this._speed=ee?requestAnimationFrame:setImmediate)(),Ht=(()=>e?cancelAnimationFrame:clearImmediate)();class Xt extends Vt{constructor(t){super(t),this.useDefaultMainLoop=!0,this.pauseOnDocumentHidden=!0,this.defaults=N,this.paused=!0,this.reqId=0}update(){const t=this._currentTime=B();if(this.requestTick(t)){this.computeDeltaTime(t);const e=this._speed,s=this._fps;let i=this._head;for(;i;){const r=i._next;i.paused?(vt(this,i),this._hasChildren=!!this._tail,i._running=!1,i.completed&&!i._cancelled&&i.cancel()):Lt(i,(t-i._startTime)*i._speed*e,0,0,i._fpst.resetTime()),this.wake()}get speed(){return this._speed*(1===A.timeScale?1:u)}set speed(t){this._speed=t*A.timeScale,yt(this,t=>t.speed=t._speed)}get timeUnit(){return 1===A.timeScale?"ms":"s"}set timeUnit(t){const e="s"===t,s=e?.001:1;if(A.timeScale!==s){A.timeScale=s,A.tickThreshold=200*s;const t=e?.001:u;this.defaults.duration*=t,this._speed*=t}}get precision(){return A.precision}set precision(t){A.precision=t}}const Yt=(()=>{const t=new Xt(B());return e&&(I.engine=t,i.addEventListener("visibilitychange",()=>{t.pauseOnDocumentHidden&&(i.hidden?t.pause():t.resume())})),t})(),Wt=()=>{Yt._head?(Yt.reqId=zt(Wt),Yt.update()):Yt.reqId=0},Ut=()=>(Ht(Yt.reqId),Yt.reqId=0,Yt),qt={_rep:new WeakMap,_add:new Map},jt=(t,e,s="_rep")=>{const i=qt[s];let r=i.get(t);return r||(r={},i.set(t,r)),r[e]?r[e]:r[e]={_head:null,_tail:null}},Gt=(t,e)=>t._isOverridden||t._absoluteStartTime>e._absoluteStartTime,Zt=t=>{t._isOverlapped=1,t._isOverridden=1,t._changeDuration=d,t._currentTime=d},Qt=(t,e)=>{const s=t._composition;if(s===r.replace){const s=t._absoluteStartTime;bt(e,t,Gt,"_prevRep","_nextRep");const i=t._prevRep;if(i){const e=i.parent,r=i._absoluteStartTime+i._changeDuration;if(t.parent.id!==e.id&&e.iterationCount>1&&r+(e.duration-e.iterationDuration)>s){Zt(i);let t=i._prevRep;for(;t&&t.parent.id===e.id;)Zt(t),t=t._prevRep}const n=s-t._delay;if(r>n){const t=i._startTime,e=r-(t+i._updateDuration),s=dt(n-e-t,12);i._changeDuration=s,i._currentTime=s,i._isOverlapped=1,s{t._isOverlapped||(o=!1)}),o){const t=e.parent;if(t){let s=!0;yt(t,t=>{t!==e&&yt(t,t=>{t._isOverlapped||(s=!1)})}),s&&t.cancel()}else e.cancel()}}}else if(s===r.blend){const e=jt(t.target,t.property,"_add"),s=(t=>{let e=Ot.animation;return e||(e={duration:d,computeDeltaTime:y,_offset:0,_delay:0,_head:null,_tail:null},Ot.animation=e,Ot.update=()=>{t.forEach(t=>{for(let e in t){const s=t[e],i=s._head;if(i){const t=i._valueType,e=3===t||2===t?ft(i._fromNumbers):null;let r=i._fromNumber,n=s._tail;for(;n&&n!==i;){if(e)for(let t=0,s=n._numbers.length;t{t._fromNumbers[s]=i._fromNumbers[s]-e,t._toNumbers[s]=0}),i._fromNumbers=e}bt(e,t,null,"_prevAdd","_nextAdd")}return t},Jt=t=>{const e=t._composition;if(e!==r.none){const s=t.target,i=t.property,n=qt._rep.get(s)[i];if(vt(n,t,"_prevRep","_nextRep"),e===r.blend){const e=qt._add,r=e.get(s);if(!r)return;const n=r[i],o=Ot.animation;vt(n,t,"_prevAdd","_nextAdd");const a=n._head;if(a&&a===n._tail){vt(n,a,"_prevAdd","_nextAdd"),vt(o,a);let t=!0;for(let e in r)if(r[e]._head){t=!1;break}t&&e.delete(s)}}}return t},Kt=(t,e,s)=>{let i=!1;return yt(e,r=>{const n=r.target;if(t.includes(n)){const t=r.property,o=r._tweenType,a=Pt(s,n,o);(!a||a&&a===t)&&(r.parent._tail===r&&3===r._tweenType&&r._prev&&3===r._prev._tweenType&&(r._prev._renderTransforms=1),vt(e,r),Jt(r),i=!0)}},!0),i},te=(t,e,s)=>{const i=e||Yt;let r;if(i._hasChildren){let e=0;yt(i,n=>{if(!n._hasChildren)if(r=Kt(t,n,s),r&&!n._head)n.cancel(),vt(i,n);else{const t=n._offset+n._delay+n.duration;t>e&&(e=t)}n._head?te(t,n,s):n._hasChildren=!1},!0),z(i.iterationDuration)||(i.iterationDuration=e)}else r=Kt(t,i,s);r&&!i._head&&(i._hasChildren=!1,i.cancel&&i.cancel())},ee=t=>(t.paused=!0,t.began=!1,t.completed=!1,t),se=t=>t._cancelled?(t._hasChildren?yt(t,se):yt(t,t=>{t._composition!==r.none&&Qt(t,jt(t.target,t.property))}),t._cancelled=0,t):t;let ie=0;const re=(t,e)=>t._priority>e._priority;class ne extends Vt{constructor(t={},e=null,s=0){super(0),++ie;const{id:i,delay:r,duration:n,reversed:o,alternate:a,loop:l,loopDelay:h,autoplay:c,frameRate:u,playbackRate:p,priority:m,onComplete:f,onLoop:g,onPause:v,onBegin:b,onBeforeUpdate:_,onUpdate:T}=t;D.current&&D.current.register(this);const x=e?0:Yt._lastTickTime,w=e?e.defaults:A.defaults,S=O(r)||z(r)?w.delay:+r,$=O(n)||z(n)?1/0:+n,C=xt(l,w.loop),E=xt(h,w.loopDelay);let k=!0===C||C===1/0||C<0?1/0:C+1,N=0;e?N=s:(Yt.reqId||Yt.requestTick(B()),N=(Yt._lastTickTime-Yt._startTime)*A.timeScale),this.id=z(i)?ie:i,this.parent=e,this.duration=pt(($+E)*k-E)||d,this.backwards=!1,this.paused=!0,this.began=!1,this.completed=!1,this.onBegin=b||w.onBegin,this.onBeforeUpdate=_||w.onBeforeUpdate,this.onUpdate=T||w.onUpdate,this.onLoop=g||w.onLoop,this.onPause=v||w.onPause,this.onComplete=f||w.onComplete,this.iterationDuration=$,this.iterationCount=k,this._autoplay=!e&&xt(c,w.autoplay),this._offset=N,this._delay=S,this._loopDelay=E,this._iterationTime=0,this._currentIteration=0,this._resolve=y,this._running=!1,this._reversed=+xt(o,w.reversed),this._reverse=this._reversed,this._cancelled=0,this._alternate=xt(a,w.alternate),this._prev=null,this._next=null,this._lastTickTime=x,this._startTime=x,this._lastTime=x,this._fps=xt(u,w.frameRate),this._speed=xt(p,w.playbackRate),this._priority=+xt(m,1)}get cancelled(){return!!this._cancelled}set cancelled(t){t?this.cancel():this.reset(!0).play()}get currentTime(){return ht(dt(this._currentTime,A.precision),-this._delay,this.duration)}set currentTime(t){const e=this.paused;this.pause().seek(+t),e||this.resume()}get iterationCurrentTime(){return ht(dt(this._iterationTime,A.precision),0,this.iterationDuration)}set iterationCurrentTime(t){this.currentTime=this.iterationDuration*this._currentIteration+t}get progress(){return ht(dt(this._currentTime/this.duration,10),0,1)}set progress(t){this.currentTime=this.duration*t}get iterationProgress(){return ht(dt(this._iterationTime/this.iterationDuration,10),0,1)}set iterationProgress(t){const e=this.iterationDuration;this.currentTime=e*this._currentIteration+e*t}get currentIteration(){return this._currentIteration}set currentIteration(t){this.currentTime=this.iterationDuration*ht(+t,0,this.iterationCount-1)}get reversed(){return!!this._reversed}set reversed(t){t?this.reverse():this.play()}get speed(){return super.speed}set speed(t){super.speed=t,this.resetTime()}reset(t=!1){return se(this),this._reversed&&!this._reverse&&(this.reversed=!1),this._iterationTime=this.iterationDuration,Lt(this,0,1,~~t,2),ee(this),this._hasChildren&&yt(this,ee),this}init(t=!1){this.fps=this._fps,this.speed=this._speed,!t&&this._hasChildren&&Lt(this,this.duration,1,~~t,2),this.reset(t);const e=this._autoplay;return!0===e?this.resume():e&&!z(e.linked)&&e.link(this),this}resetTime(){const t=1/(this._speed*Yt._speed);return this._startTime=B()-(this._currentTime+this._delay)*t,this}pause(){return this.paused||(this.paused=!0,this.onPause(this)),this}resume(){return this.paused?(this.paused=!1,this.duration<=d&&!this._hasChildren?Lt(this,d,0,0,2):(this._running||(bt(Yt,this,re),Yt._hasChildren=!0,this._running=!0),this.resetTime(),this._startTime-=12,Yt.wake()),this):this}restart(){return this.reset().resume()}seek(t,e=0,s=0){se(this),this.completed=!1;const i=this.paused;return this.paused=!0,Lt(this,t+this._delay,~~e,~~s,1),i?this:this.resume()}alternate(){const t=this._reversed,e=this.iterationCount,s=this.iterationDuration,i=e===1/0?it(c/s):e;return this._reversed=+(!this._alternate||i%2?!t:t),e===1/0?this.iterationProgress=this._reversed?1-this.iterationProgress:this.iterationProgress:this.seek(s*i-this._currentTime),this.resetTime(),this}play(){return this._reversed&&this.alternate(),this.resume()}reverse(){return this._reversed||this.alternate(),this.resume()}cancel(){return this._hasChildren?yt(this,t=>t.cancel(),!0):yt(this,Jt),this._cancelled=1,this.pause()}stretch(t){const e=this.duration,s=mt(t);if(e===s)return this;const i=t/e,r=t<=d;return this.duration=r?d:s,this.iterationDuration=r?d:mt(this.iterationDuration*i),this._offset*=i,this._delay*=i,this._loopDelay*=i,this}revert(){Lt(this,0,1,0,1);const t=this._autoplay;return t&&t.linked&&t.linked===this&&t.revert(),this.cancel()}complete(t=0){return this.seek(this.duration,t).cancel()}then(t=y){const e=this.then,s=()=>{this.then=null,t(this),this.then=e,this._resolve=y};return new Promise(t=>(this._resolve=()=>t(s()),this.completed&&this._resolve(),this))}}function oe(t){const e=V(t)?D.root.querySelectorAll(t):t;if(e instanceof NodeList||e instanceof HTMLCollection)return e}function ae(t){if(H(t))return[];if(!e)return P(t)&&t.flat(1/0)||[t];if(P(t)){const e=t.flat(1/0),s=[];for(let t=0,i=e.length;t{const n=e.u,o=e.n;if(1===e.t&&n===s)return e;const a=o+n+s,l=de[a];if(z(l)||r){let r;if(n in he)r=o*he[n]/he[s];else{const e=100,a=t.cloneNode(),l=t.parentNode,h=l&&l!==i?l:i.body;h.appendChild(a);const d=a.style;d.width=e+n;const c=a.offsetWidth||e;d.width=e+s;const u=c/(a.offsetWidth||e);h.removeChild(a),r=u*o}e.n=r,de[a]=r}else e.n=l;return e.t,e.u=s,e},ue=t=>t,pe=(t=1.68)=>e=>Z(e,+t),me={in:t=>e=>t(e),out:t=>e=>1-t(1-e),inOut:t=>e=>e<.5?t(2*e)/2:1-t(-2*e+2)/2,outIn:t=>e=>e<.5?(1-t(1-2*e))/2:(t(2*e-1)+1)/2},fe=at/2,ge=2*at,ye={[p]:pe,Quad:pe(2),Cubic:pe(3),Quart:pe(4),Quint:pe(5),Sine:t=>1-K(t*fe),Circ:t=>1-Q(1-t*t),Expo:t=>t?Z(2,10*t-10):0,Bounce:t=>{let e,s=4;for(;t<((e=Z(2,--s))-1)/11;);return 1/Z(4,3-s)-7.5625*Z((3*e-2)/22-t,2)},Back:(t=1.7)=>e=>(+t+1)*e*e*e-+t*e*e,Elastic:(t=1,e=.3)=>{const s=ht(+t,1,10),i=ht(+e,d,2),r=i/ge*rt(1/s),n=ge/i;return t=>0===t||1===t?t:-s*Z(2,-10*(1-t))*J((1-t-r)*n)}},ve=(()=>{const t={linear:ue,none:ue};for(let e in me)for(let s in ye){const i=ye[s],r=me[e];t[e+s]=s===p||"Back"===s||"Elastic"===s?(t,e)=>r(i(t,e)):r(i)}return t})(),be={linear:ue,none:ue},_e=t=>{if(be[t])return be[t];if(t.indexOf("(")<=-1){const e=me[t]||t.includes("Back")||t.includes("Elastic")?ve[t]():ve[t];return e?be[t]=e:ue}{const e=t.slice(0,-1).split("("),s=ve[e[0]];return s?be[t]=s(...e[1].split(",")):ue}},Te=["steps(","irregular(","linear(","cubicBezier("],xe=t=>{if(V(t))for(let e=0,s=Te.length;e{const s={};if(P(t)){const e=[].concat(...t.map(t=>Object.keys(t))).filter(q);for(let i=0,r=e.length;i{const e={};for(let s in t){const i=t[s];q(s)?s===r&&(e.to=i):e[s]=i}return e});s[r]=n}}else{const i=xt(e.duration,A.defaults.duration),r=Object.keys(t).map(e=>({o:parseFloat(e)/100,p:t[e]})).sort((t,e)=>t.o-e.o);r.forEach(t=>{const e=t.o,r=t.p;for(let t in r)if(q(t)){let n=s[t];n||(n=s[t]=[]);const o=e*i;let a=n.length,l=n[a-1];const h={to:r[t]};let d=0;for(let t=0;t=u?r.none:z(_)?x.composition:_,R=this._offset+(s?s._offset:0);C&&(w.parent=this);let L=NaN,B=NaN,X=0,Y=0;for(let t=0;t2&&e?(Ae=[],l.forEach((t,e)=>{e?1===e?(Ne[1]=t,Ae.push(Ne)):Ae.push(t):Ne[0]=t})):Ae=l}else ke[0]=l,Ae=ke;let f=null,g=null,y=NaN,v=0,b=0;for(let t=Ae.length;b1?wt(k,e,i,h,null,p)/t:k),e,i,h,null,p),L=wt(xt(Ie.delay,b?0:N),e,i,h,null,p),B=Ie.modifier||D,W=!z(T),U=!z(_),q=P(_),j=q||W&&U,G=g?v+L:L,Z=dt(R+G,12);Y||!W&&!q||(Y=1);let Q=g;if(c!==r.none){let t=f._head;for(;t&&!t._isOverridden&&t._absoluteStartTime<=Z;)if(Q=t,t=t._nextRep,t&&t._absoluteStartTime>=Z)for(;t;)Zt(t),t=t._nextRep}if(j){kt(q?wt(_[0],e,i,h,Ee,p):T,we),kt(q?wt(_[1],e,i,h,Ce,p):_,Se);const t=Ct(e,a,o,$e);0===we.t&&(Q?1===Q._valueType&&(we.t=1,we.u=Q._unit):(kt(t,Dt),1===Dt.t&&(we.t=1,we.u=Dt.u)))}else U?kt(_,Se):g?Nt(g,Se):kt(s&&Q&&Q.parent.parent===s?Q._value:Ct(e,a,o,$e),Se),W?kt(T,we):g?Nt(g,we):kt(s&&Q&&Q.parent.parent===s?Q._value:Ct(e,a,o,$e),we);if(we.o&&(we.n=Et(Q?Q._toNumber:kt(Ct(e,a,o,$e),Dt).n,we.n,we.o)),Se.o&&(Se.n=Et(we.n,Se.n,Se.o)),we.t!==Se.t)if(3===we.t||3===Se.t){const t=3===we.t?we:Se,e=3===we.t?Se:we;e.t=3,e.s=ft(t.s),e.d=t.d.map(()=>e.n)}else if(1===we.t||1===Se.t){const t=1===we.t?we:Se,e=1===we.t?Se:we;e.t=1,e.u=t.u}else if(2===we.t||2===Se.t){const t=2===we.t?we:Se,e=2===we.t?Se:we;e.t=2,e.s=t.s,e.d=[0,0,0,1]}if(we.u!==Se.u){let t=Se.u?we:Se;t=ce(e,t,Se.u?Se.u:we.u,!1)}if(Se.d&&we.d&&Se.d.length!==we.d.length){const t=we.d.length>Se.d.length?we:Se,e=t===we?Se:we;e.d=t.d.map((t,s)=>z(e.d[s])?0:e.d[s]),e.s=ft(t.s)}const J=dt(+A||d,12);let K=$e[a];H(K)||($e[a]=null);const tt={parent:this,id:Re++,property:a,target:e,_value:null,_toFunc:Ce.func,_fromFunc:Ee.func,_ease:xe(C),_fromNumbers:ft(we.d),_toNumbers:ft(Se.d),_strings:ft(Se.s),_fromNumber:we.n,_toNumber:Se.n,_numbers:ft(we.d),_number:we.n,_unit:Se.u,_modifier:B,_currentTime:0,_startTime:G,_delay:+L,_updateDuration:J,_changeDuration:J,_absoluteStartTime:Z,_tweenType:o,_valueType:Se.t,_composition:c,_isOverlapped:0,_isOverridden:0,_renderTransforms:0,_inlineValue:K,_prevRep:null,_nextRep:null,_prevAdd:null,_nextAdd:null,_prev:null,_next:null};c!==r.none&&Qt(tt,f);const et=tt._valueType;tt._value=3===et?It(tt,1,-1):2===et?At(tt,1,-1):1===et?`${B(tt._toNumber)}${tt._unit}`:B(tt._toNumber),isNaN(y)&&(y=tt._startTime),v=dt(G+J,12),g=tt,X++,bt(this,tt)}(isNaN(B)||yL)&&(L=v),3===o&&(c=X-b,u=X)}if(!isNaN(c)){let t=0;yt(this,e=>{t>=c&&t{t.id===e.id&&(t._renderTransforms=1)})),t++})}}h||console.warn("No target found. Make sure the element you're trying to animate is accessible before creating your animation."),B?(yt(this,t=>{t._startTime-t._delay||(t._delay-=B),t._startTime-=B}),L-=B):B=0,L||(L=d,this.iterationCount=0),this.targets=l,this.id=z(m)?Le:m,this.duration=L===d?d:pt((L+this._loopDelay)*this.iterationCount-this._loopDelay)||d,this.onRender=T||x.onRender,this._ease=$,this._delay=B,this.iterationDuration=L,!this._autoplay&&Y&&this.onRender(this)}stretch(t){const e=this.duration;if(e===mt(t))return this;const s=t/e;return yt(this,t=>{t._updateDuration=mt(t._updateDuration*s),t._changeDuration=mt(t._changeDuration*s),t._currentTime*=s,t._startTime*=s,t._absoluteStartTime*=s}),super.stretch(t)}refresh(){return yt(this,t=>{const e=t._toFunc,s=t._fromFunc;(e||s)&&(s?(kt(s(),we),we.u!==t._unit&&t.target[o]&&ce(t.target,we,t._unit,!0),t._fromNumbers=ft(we.d),t._fromNumber=we.n):e&&(kt(Ct(t.target,t.property,t._tweenType),Dt),t._fromNumbers=ft(Dt.d),t._fromNumber=Dt.n),e&&(kt(e(),Se),t._toNumbers=ft(Se.d),t._strings=ft(Se.s),t._toNumber=Se.o?Et(t._fromNumber,Se.n,Se.o):Se.n))}),this.duration===d&&this.restart(),this}revert(){return super.revert(),Ft(this)}then(t){return super.then(t)}}const Pe=(t,e)=>{let s=t.iterationDuration;if(s===d&&(s=0),z(e))return s;if(M(+e))return+e;const i=e,r=t?t.labels:null,n=!H(r),o=((t,e)=>{if(L(e,"<")){const s="<"===e[1],i=t._tail,r=i?i._offset+i._delay:0;return s?r:r+i.duration}})(t,i),a=!z(o),l=E.exec(i);if(l){const t=l[0],e=i.split(t),h=n&&e[0]?r[e[0]]:s,d=a?o:n?h:s,c=+e[1];return Et(d,c,t[0])}return a?o:n?z(r[i])?s:r[i]:s};function Fe(t,e,s,i,r,n){const o=M(t.duration)&&t.duration<=d?s-d:s;e.composition&&Lt(e,o,1,1,1);const a=i?new Be(i,t,e,o,!1,r,n):new ne(t,e,o);return e.composition&&a.init(!0),bt(e,a),yt(e,t=>{const s=t._offset+t._delay+t.duration;s>e.iterationDuration&&(e.iterationDuration=s)}),e.duration=function(t){return pt((t.iterationDuration+t._loopDelay)*t.iterationCount-t._loopDelay)||d}(e),e}let Me=0;class Ve extends ne{constructor(t={}){super(t,null,0),++Me,this.id=z(t.id)?Me:t.id,this.duration=0,this.labels={};const e=t.defaults,s=A.defaults;this.defaults=e?gt(e,s):s,this.composition=xt(t.composition,!0),this.onRender=t.onRender||s.onRender;const i=xt(t.playbackEase,s.playbackEase);this._ease=i?xe(i):null,this.iterationDuration=0}add(t,e,s){const i=F(e),r=F(t);if(i||r){if(this._hasChildren=!0,i){const i=e,r=A.editor&&A.editor.addTimelineChild,n=s&&"Stagger"===s.type&&A.editor,o=O(s)?s:null;if(o||n){const e=ae(t),n=this.duration,a=this.iterationDuration,l=i.id;let h=0;const d=e.length,c=r?r(t,i,this.id,s,d):null,u=o||A.editor.resolveStagger(s.defaultValue);e.forEach(t=>{const s={...c||i};this.duration=n,this.iterationDuration=a,z(l)||(s.id=l+"-"+h),Fe(s,this,Pe(this,u(t,h,e,null,this)),t,h,e),h++})}else{const e=r?r(t,i,this.id,s):i,n=s&&s.type?s.defaultValue:s;Fe(e,this,Pe(this,n),t)}}else Fe(t,this,Pe(this,e));return this.composition&&this.init(!0),this}}sync(t,e){if(z(t)||t&&z(t.pause))return this;t.pause();const s=+(t.effect?t.effect.getTiming().duration:t.duration);return z(t)||z(t.persist)||(t.persist=!0),this.add(t,{currentTime:[0,s],duration:s,delay:0,ease:"linear",playbackEase:"linear"},e)}set(t,e,s){return z(e)?this:(e.duration=d,e.composition=r.replace,this.add(t,e,s))}call(t,e){return z(t)||t&&!O(t)?this:this.add({duration:0,delay:0,onComplete:()=>t(this)},e)}label(t,e){return z(t)||t&&!V(t)||(this.labels[t]=Pe(this,e)),this}remove(t,e){return te(ae(t),this,e),this}stretch(t){const e=this.duration;if(e===mt(t))return this;const s=t/e,i=this.labels;yt(this,t=>t.stretch(t.duration*s));for(let t in i)i[t]*=s;return super.stretch(t)}refresh(){return yt(this,t=>{t.refresh&&t.refresh()}),this}revert(){return super.revert(),yt(this,t=>t.revert,!0),Ft(this)}then(t){return super.then(t)}}const Oe=t=>A.editor?A.editor.addTimeline(t):new Ve(t).init();class ze{constructor(t,e){D.current&&D.current.register(this);const s=()=>{if(this.callbacks.completed)return;let t=!0;for(let e in this.animations)if(!this.animations[e].paused&&t){t=!1;break}t&&this.callbacks.complete()},i={onBegin:()=>{this.callbacks.completed&&this.callbacks.reset(),this.callbacks.play()},onComplete:s,onPause:s},n={v:1,autoplay:!1},o={};if(this.targets=[],this.animations={},this.callbacks=null,!z(t)&&!z(e)){for(let t in e){const s=e[t];q(t)?o[t]=s:L(t,"on")?n[t]=s:i[t]=s}this.callbacks=new Be({v:0},n);for(let e in o){const s=o[e],n=F(s);let a={},l="+=0";if(n){const t=s.unit;V(t)&&(l+=t)}else a.duration=s;a[e]=n?gt({to:l},s):l;const h=gt(i,a);h.composition=r.replace,h.autoplay=!1;const d=this.animations[e]=new Be(t,h,null,0,!1).init();this.targets.length||this.targets.push(...d.targets),this[e]=(t,e,s)=>{const i=d._head;if(z(t)&&i){const t=i._numbers;return t&&t.length?t:i._modifier(i._number)}return yt(d,e=>{if(P(t))for(let s=0,i=t.length;si+(t-e)/(s-e)*(r-i);var Xe=Object.freeze({__proto__:null,clamp:ht,damp:(t,e,s,i)=>i?1===i?e:ut(t,e,1-Math.exp(-i*s*.1)):t,degToRad:t=>t*Math.PI/180,lerp:ut,mapRange:He,padEnd:(t,e,s)=>`${t}`.padEnd(e,s),padStart:(t,e,s)=>`${t}`.padStart(e,s),radToDeg:t=>180*t/Math.PI,round:dt,roundPad:(t,e)=>(+t).toFixed(e),snap:ct,wrap:(t,e,s)=>((t-e)%(s-e)+(s-e))%(s-e)+e});const Ye=10*u;class We{constructor(t={}){const e=!z(t.bounce)||!z(t.duration);this.timeStep=.02,this.restThreshold=5e-4,this.restDuration=200,this.maxDuration=6e4,this.maxRestSteps=this.restDuration/this.timeStep/u,this.maxIterations=this.maxDuration/this.timeStep/u,this.bn=ht(xt(t.bounce,.5),-1,1),this.pd=ht(xt(t.duration,628),10*A.timeScale,Ye*A.timeScale),this.m=ht(xt(t.mass,1),1,Ye),this.s=ht(xt(t.stiffness,100),d,Ye),this.d=ht(xt(t.damping,10),d,Ye),this.v=ht(xt(t.velocity,0),-1e4,Ye),this.w0=0,this.zeta=0,this.wd=0,this.b=0,this.completed=!1,this.solverDuration=0,this.settlingDuration=0,this.parent=null,this.onComplete=t.onComplete||y,e&&this.calculateSDFromBD(),this.compute(),this.ease=t=>{const e=t*this.settlingDuration,s=this.completed,i=this.pd;return e>=i&&!s&&(this.completed=!0,this.onComplete(this.parent)),e=0?this.d=4*(1-this.bn)*at/t:this.d=4*at/(t*(1+this.bn)),this.s=dt(ht(this.s,d,Ye),3),this.d=dt(ht(this.d,d,300),3)}calculateBDFromSD(){const t=2*at/Q(this.s);this.pd=t*(1===A.timeScale?u:1);const e=this.d/(2*Q(this.s));this.bn=e<=1?1-this.d*t/(4*at):4*at/(this.d*t)-1,this.bn=dt(ht(this.bn,-1,1),3),this.pd=dt(ht(this.pd,10*A.timeScale,Ye*A.timeScale),3)}compute(){const{maxRestSteps:t,maxIterations:e,restThreshold:s,timeStep:i,m:r,d:n,s:o,v:a}=this,l=this.w0=ht(Q(o/r),d,u),h=this.zeta=n/(2*Q(o*r));h<1?(this.wd=l*Q(1-h*h),this.b=(h*l-a)/this.wd):1===h?(this.wd=0,this.b=-a+l):(this.wd=l*Q(h*h-1),this.b=(h*l-a)/this.wd);let c=0,p=0,m=0;for(;p<=t&&m<=e;)tt(1-this.solve(c))new We(t),qe=t=>(console.warn("createSpring() is deprecated use spring() instead"),new We(t)),je={_head:null,_tail:null},Ge=(t,e,s)=>{let i,r=je._head;for(;r;){const n=r._next,o=r.$el===t,a=!e||r.property===e,l=!s||r.parent===s;if(o&&a&&l){i=r.animation;try{i.commitStyles()}catch{}i.cancel(),vt(je,r);const t=r.parent;t&&(t._completed++,t.animations.length===t._completed&&(t.completed=!0,t.paused=!0,t.muteCallbacks||(t.onComplete(t),t._resolve(t))))}r=n}return i},Ze=(t,e,s,i,r)=>{const n=e.animate(i,r),o=r.delay+ +r.duration*r.iterations;n.playbackRate=t._speed,t.paused&&n.pause(),t.durationGe(e,s,t);return n.oncancel=a,n.onremove=a,t.persist||(n.onfinish=a),n};function Qe(t,e,s){const i=le(t);if(!i.length)return;const[r]=i,n=St(r,e),o=Pt(e,r,n);let a=Ct(r,o);if(z(s))return a;if(kt(a,Dt),0===Dt.t||1===Dt.t){if(!1===s)return Dt.n;{const t=ce(r,Dt,s,!1);return`${dt(t.n,A.precision)}${t.u}`}}}const Je=(t,e)=>{if(!z(e))return e.duration=d,e.composition=xt(e.composition,r.none),new Be(t,e,null,0,!0).resume()},Ke=(t,e,s)=>{const i=ae(t);for(let t=0,r=i.length;t{t.cancelable&&t.preventDefault()};class es{constructor(t){this.el=t,this.zIndex=0,this.parentElement=null,this.classList={add:y,remove:y}}get x(){return this.el.x||0}set x(t){this.el.x=t}get y(){return this.el.y||0}set y(t){this.el.y=t}get width(){return this.el.width||0}set width(t){this.el.width=t}get height(){return this.el.height||0}set height(t){this.el.height=t}getBoundingClientRect(){return{top:this.y,right:this.x,bottom:this.y+this.height,left:this.x+this.width}}}class ss{constructor(t){this.$el=t,this.inlineTransforms=[],this.point=new DOMPoint,this.inversedMatrix=this.getMatrix().inverse()}normalizePoint(t,e){return this.point.x=t,this.point.y=e,this.point.matrixTransform(this.inversedMatrix)}traverseUp(t){let e=this.$el.parentElement,s=0;for(;e&&e!==i;)t(e,s),e=e.parentElement,s++}getMatrix(){const t=new DOMMatrix;return this.traverseUp(e=>{const s=getComputedStyle(e).transform;if(s){const e=new DOMMatrix(s);t.preMultiplySelf(e)}}),t}remove(){this.traverseUp((t,e)=>{this.inlineTransforms[e]=t.style.transform,t.style.transform="none"})}revert(){this.traverseUp((t,e)=>{const s=this.inlineTransforms[e];""===s?t.style.removeProperty("transform"):t.style.transform=s})}}const is=(t,e)=>t&&O(t)?t(e):t;let rs=0;class ns{constructor(t,e={}){if(!t)return;D.current&&D.current.register(this);const r=e.x,n=e.y,o=e.trigger,a=e.modifier,l=e.releaseEase,h=l&&xe(l),d=!z(l)&&!z(l.ease),u=F(r)&&!z(r.mapTo)?r.mapTo:"translateX",p=F(n)&&!z(n.mapTo)?n.mapTo:"translateY",m=is(e.container,this);this.containerArray=P(m)?m:null,this.$container=m&&!this.containerArray?ae(m)[0]:i.body,this.useWin=this.$container===i.body,this.$scrollContainer=this.useWin?s:this.$container,this.$target=F(t)?new es(t):ae(t)[0],this.$trigger=ae(o||t)[0],this.fixed="fixed"===Qe(this.$target,"position"),this.isFinePointer=!0,this.containerPadding=[0,0,0,0],this.containerFriction=0,this.releaseContainerFriction=0,this.snapX=0,this.snapY=0,this.scrollSpeed=0,this.scrollThreshold=0,this.dragSpeed=0,this.dragThreshold=3,this.maxVelocity=0,this.minVelocity=0,this.velocityMultiplier=0,this.cursor=!1,this.releaseXSpring=d?l:Ue({mass:xt(e.releaseMass,1),stiffness:xt(e.releaseStiffness,80),damping:xt(e.releaseDamping,20)}),this.releaseYSpring=d?l:Ue({mass:xt(e.releaseMass,1),stiffness:xt(e.releaseStiffness,80),damping:xt(e.releaseDamping,20)}),this.releaseEase=h||ve.outQuint,this.hasReleaseSpring=d,this.onGrab=e.onGrab||y,this.onDrag=e.onDrag||y,this.onRelease=e.onRelease||y,this.onUpdate=e.onUpdate||y,this.onSettle=e.onSettle||y,this.onSnap=e.onSnap||y,this.onResize=e.onResize||y,this.onAfterResize=e.onAfterResize||y,this.disabled=[0,0];const f={};if(a&&(f.modifier=a),z(r)||!0===r)f[u]=0;else if(F(r)){const t=r,e={};t.modifier&&(e.modifier=t.modifier),t.composition&&(e.composition=t.composition),f[u]=e}else!1===r&&(f[u]=0,this.disabled[0]=1);if(z(n)||!0===n)f[p]=0;else if(F(n)){const t=n,e={};t.modifier&&(e.modifier=t.modifier),t.composition&&(e.composition=t.composition),f[p]=e}else!1===n&&(f[p]=0,this.disabled[1]=1);this.animate=new ze(this.$target,f),this.xProp=u,this.yProp=p,this.destX=0,this.destY=0,this.deltaX=0,this.deltaY=0,this.scroll={x:0,y:0},this.coords=[this.x,this.y,0,0],this.snapped=[0,0],this.pointer=[0,0,0,0,0,0,0,0],this.scrollView=[0,0],this.dragArea=[0,0,0,0],this.containerBounds=[-c,c,c,-c],this.scrollBounds=[0,0,0,0],this.targetBounds=[0,0,0,0],this.window=[0,0],this.velocityStack=[0,0,0],this.velocityStackIndex=0,this.velocityTime=B(),this.velocity=0,this.angle=0,this.cursorStyles=null,this.triggerStyles=null,this.bodyStyles=null,this.targetStyles=null,this.touchActionStyles=null,this.transforms=new ss(this.$target),this.overshootCoords={x:0,y:0},this.overshootTicker=new ne({autoplay:!1,onUpdate:()=>{this.updated=!0,this.manual=!0,this.disabled[0]||this.animate[this.xProp](this.overshootCoords.x,1),this.disabled[1]||this.animate[this.yProp](this.overshootCoords.y,1)},onComplete:()=>{this.manual=!1,this.disabled[0]||this.animate[this.xProp](this.overshootCoords.x,0),this.disabled[1]||this.animate[this.yProp](this.overshootCoords.y,0)}},null,0).init(),this.updateTicker=new ne({autoplay:!1,onUpdate:()=>this.update()},null,0).init(),this.contained=!z(m),this.manual=!1,this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.enabled=!1,this.initialized=!1,this.activeProp=this.disabled[1]?u:p,this.animate.callbacks.onRender=()=>{const t=this.updated,e=!(this.grabbed&&t)&&this.released,s=this.x,i=this.y,r=s-this.coords[2],n=i-this.coords[3];this.deltaX=r,this.deltaY=n,this.coords[2]=s,this.coords[3]=i,t&&(r||n)&&this.onUpdate(this),e?(this.computeVelocity(r,n),this.angle=ot(n,r)):this.updated=!1},this.animate.callbacks.onComplete=()=>{!this.grabbed&&this.released&&(this.released=!1),this.manual||(this.deltaX=0,this.deltaY=0,this.velocity=0,this.velocityStack[0]=0,this.velocityStack[1]=0,this.velocityStack[2]=0,this.velocityStackIndex=0,this.onSettle(this))},this.resizeTicker=new ne({autoplay:!1,duration:150*A.timeScale,onComplete:()=>{this.onResize(this),this.refresh(),this.onAfterResize(this)}}).init(),this.parameters=e,this.resizeObserver=new ResizeObserver(()=>{this.initialized?this.resizeTicker.restart():this.initialized=!0}),this.enable(),this.refresh(),this.resizeObserver.observe(this.$container),F(t)||this.resizeObserver.observe(this.$target)}computeVelocity(t,e){const s=this.velocityTime,i=B(),r=i-s;if(r<17)return this.velocity;this.velocityTime=i;const n=this.velocityStack,o=this.velocityMultiplier,a=this.minVelocity,l=this.maxVelocity,h=this.velocityStackIndex;n[h]=dt(ht(Q(t*t+e*e)/r*o,a,l),5);const d=nt(n[0],n[1],n[2]);return this.velocity=d,this.velocityStackIndex=(h+1)%3,d}setX(t,e=!1){if(this.disabled[0])return;const s=dt(t,5);return this.overshootTicker.pause(),this.manual=!0,this.updated=!e,this.destX=s,this.snapped[0]=ct(s,this.snapX),this.animate[this.xProp](s,0),this.manual=!1,this}setY(t,e=!1){if(this.disabled[1])return;const s=dt(t,5);return this.overshootTicker.pause(),this.manual=!0,this.updated=!e,this.destY=s,this.snapped[1]=ct(s,this.snapY),this.animate[this.yProp](s,0),this.manual=!1,this}get x(){return dt(this.animate[this.xProp](),A.precision)}set x(t){this.setX(t,!1)}get y(){return dt(this.animate[this.yProp](),A.precision)}set y(t){this.setY(t,!1)}get progressX(){return He(this.x,this.containerBounds[3],this.containerBounds[1],0,1)}set progressX(t){this.setX(He(t,0,1,this.containerBounds[3],this.containerBounds[1]),!1)}get progressY(){return He(this.y,this.containerBounds[0],this.containerBounds[2],0,1)}set progressY(t){this.setY(He(t,0,1,this.containerBounds[0],this.containerBounds[2]),!1)}updateScrollCoords(){const t=dt(this.useWin?s.scrollX:this.$container.scrollLeft,0),e=dt(this.useWin?s.scrollY:this.$container.scrollTop,0),[i,r,n,o]=this.containerPadding,a=this.scrollThreshold;this.scroll.x=t,this.scroll.y=e,this.scrollBounds[0]=e-this.targetBounds[0]+i-a,this.scrollBounds[1]=t-this.targetBounds[1]-r+a,this.scrollBounds[2]=e-this.targetBounds[2]-n+a,this.scrollBounds[3]=t-this.targetBounds[3]+o-a}updateBoundingValues(){const t=this.$container;if(!t)return;const e=this.x,r=this.y,n=this.coords[2],o=this.coords[3];this.coords[2]=0,this.coords[3]=0,this.setX(0,!0),this.setY(0,!0),this.transforms.remove();const a=this.window[0]=s.innerWidth,l=this.window[1]=s.innerHeight,h=this.useWin,d=t.scrollWidth,c=t.scrollHeight,u=this.fixed,p=t.getBoundingClientRect(),[m,f,g,y]=this.containerPadding;this.dragArea[0]=h?0:p.left,this.dragArea[1]=h?0:p.top,this.scrollView[0]=h?ht(d,a,d):d,this.scrollView[1]=h?ht(c,l,c):c,this.updateScrollCoords();const{width:v,height:b,left:_,top:T,right:x,bottom:w}=t.getBoundingClientRect();this.dragArea[2]=dt(h?ht(v,a,a):v,0),this.dragArea[3]=dt(h?ht(b,l,l):b,0);const S=Qe(t,"overflow"),$="visible"===S,C="hidden"===S;if(this.canScroll=!u&&this.contained&&(t===i.body&&$||!C&&!$)&&(d>this.dragArea[2]+y-f||c>this.dragArea[3]+m-g)&&(!this.containerArray||this.containerArray&&!P(this.containerArray)),this.contained){const e=this.scroll.x,s=this.scroll.y,i=this.canScroll,r=this.$target.getBoundingClientRect(),n=i?h?0:t.scrollLeft:0,o=i?h?0:t.scrollTop:0,d=i?this.scrollView[0]-n-v:0,c=i?this.scrollView[1]-o-b:0;this.targetBounds[0]=dt(r.top+s-(h?0:T),0),this.targetBounds[1]=dt(r.right+e-(h?a:x),0),this.targetBounds[2]=dt(r.bottom+s-(h?l:w),0),this.targetBounds[3]=dt(r.left+e-(h?0:_),0),this.containerArray?(this.containerBounds[0]=this.containerArray[0]+m,this.containerBounds[1]=this.containerArray[1]-f,this.containerBounds[2]=this.containerArray[2]-g,this.containerBounds[3]=this.containerArray[3]+y):(this.containerBounds[0]=-dt(r.top-(u?ht(T,0,l):T)+o-m,0),this.containerBounds[1]=-dt(r.right-(u?ht(x,0,a):x)-d+f,0),this.containerBounds[2]=-dt(r.bottom-(u?ht(w,0,l):w)-c+g,0),this.containerBounds[3]=-dt(r.left-(u?ht(_,0,a):_)+n-y,0))}this.transforms.revert(),this.coords[2]=n,this.coords[3]=o,this.setX(e,!0),this.setY(r,!0)}isOutOfBounds(t,e,s){if(!this.contained)return 0;const[i,r,n,o]=t,[a,l]=this.disabled,h=!a&&er,d=!l&&sn;return h&&!d?1:!h&&d?2:h&&d?3:0}refresh(){const t=this.parameters,e=t.x,r=t.y,n=is(t.container,this),o=is(t.containerPadding,this)||0,a=P(o)?o:[o,o,o,o],l=this.x,h=this.y,d=is(t.cursor,this),c={onHover:"grab",onGrab:"grabbing"};if(d){const{onHover:t,onGrab:e}=d;t&&(c.onHover=t),e&&(c.onGrab=e)}const u=is(t.dragThreshold,this),p={mouse:3,touch:7};if(M(u))p.mouse=u,p.touch=u;else if(u){const{mouse:t,touch:e}=u;z(t)||(p.mouse=t),z(e)||(p.touch=e)}this.containerArray=P(n)?n:null,this.$container=n&&!this.containerArray?ae(n)[0]:i.body,this.useWin=this.$container===i.body,this.$scrollContainer=this.useWin?s:this.$container,this.isFinePointer=matchMedia("(pointer:fine)").matches,this.containerPadding=xt(a,[0,0,0,0]),this.containerFriction=ht(xt(is(t.containerFriction,this),.8),0,1),this.releaseContainerFriction=ht(xt(is(t.releaseContainerFriction,this),this.containerFriction),0,1),this.snapX=is(F(e)&&!z(e.snap)?e.snap:t.snap,this),this.snapY=is(F(r)&&!z(r.snap)?r.snap:t.snap,this),this.scrollSpeed=xt(is(t.scrollSpeed,this),1.5),this.scrollThreshold=xt(is(t.scrollThreshold,this),20),this.dragSpeed=xt(is(t.dragSpeed,this),1),this.dragThreshold=this.isFinePointer?p.mouse:p.touch,this.minVelocity=xt(is(t.minVelocity,this),0),this.maxVelocity=xt(is(t.maxVelocity,this),50),this.velocityMultiplier=xt(is(t.velocityMultiplier,this),1),this.cursor=!1!==d&&c,this.updateBoundingValues();const[m,f,g,y]=this.containerBounds;this.setX(ht(l,y,f),!0),this.setY(ht(h,m,g),!0)}update(){if(this.updateScrollCoords(),this.canScroll){const[t,e,s,i]=this.containerPadding,[r,n]=this.scrollView,o=this.dragArea[2],a=this.dragArea[3],l=this.scroll.x,h=this.scroll.y,d=this.$container.scrollWidth,c=this.$container.scrollHeight,u=this.useWin?ht(d,this.window[0],d):d,p=this.useWin?ht(c,this.window[1],c):c,m=r-u,f=n-p;this.dragged&&m>0&&(this.coords[0]-=m,this.scrollView[0]=u),this.dragged&&f>0&&(this.coords[1]-=f,this.scrollView[1]=p);const g=10*this.scrollSpeed,y=this.scrollThreshold,[v,b]=this.coords,[_,T,x,w]=this.scrollBounds,S=dt(ht((b-_+t)/y,-1,0)*g,0),$=dt(ht((v-T-e)/y,0,1)*g,0),C=dt(ht((b-x-s)/y,0,1)*g,0),E=dt(ht((v-w+i)/y,-1,0)*g,0);if(S||C||E||$){const[t,e]=this.disabled;let s=l,i=h;t||(s=dt(ht(l+(E||$),0,r-o),0),this.coords[0]-=l-s),e||(i=dt(ht(h+(S||C),0,n-a),0),this.coords[1]-=h-i),this.useWin?this.$scrollContainer.scrollBy(-(l-s),-(h-i)):this.$scrollContainer.scrollTo(s,i)}}const[t,e,s,i]=this.containerBounds,[r,n,o,a,l,h]=this.pointer;this.coords[0]+=(r-l)*this.dragSpeed,this.coords[1]+=(n-h)*this.dragSpeed,this.pointer[4]=r,this.pointer[5]=n;const[d,c]=this.coords,[u,p]=this.snapped,m=(1-this.containerFriction)*this.dragSpeed;this.setX(d>e?e+(d-e)*m:ds?s+(c-s)*m:c{this.canScroll=!1,this.$scrollContainer.scrollTo(n.x,n.y)}}).init().then(()=>{this.canScroll=a})}return this}handleHover(){this.isFinePointer&&this.cursor&&!this.cursorStyles&&(this.cursorStyles=Je(this.$trigger,{cursor:this.cursor.onHover}))}animateInView(t,e=0,s=ve.inOutQuad){this.stop(),this.updateBoundingValues();const i=this.x,r=this.y,[n,o,a,l]=this.containerPadding,h=this.scroll.y-this.targetBounds[0]+n+e,d=this.scroll.x-this.targetBounds[1]-o-e,c=this.scroll.y-this.targetBounds[2]-a-e,u=this.scroll.x-this.targetBounds[3]+l+e,p=this.isOutOfBounds([h,d,c,u],i,r);if(p){const[e,n]=this.disabled,o=ht(ct(i,this.snapX),u,d),a=ht(ct(r,this.snapY),h,c),l=z(t)?350*A.timeScale:t;e||1!==p&&3!==p||this.animate[this.xProp](o,l,s),n||2!==p&&3!==p||this.animate[this.yProp](a,l,s)}return this}handleDown(t){const e=t.target;if(this.grabbed||"range"===e.type)return;t.stopPropagation(),this.grabbed=!0,this.released=!1,this.stop(),this.updateBoundingValues();const s=t.changedTouches,r=s?s[0].clientX:t.clientX,n=s?s[0].clientY:t.clientY,{x:o,y:a}=this.transforms.normalizePoint(r,n),[l,h,d,c]=this.containerBounds,u=(1-this.containerFriction)*this.dragSpeed,p=this.x,m=this.y;this.coords[0]=this.coords[2]=u?p>h?h+(p-h)/u:pd?d+(m-d)/u:mrs?f:rs)+1,this.targetStyles=Je(this.$target,{zIndex:rs}),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.cursorStyles&&(this.cursorStyles.revert(),this.cursorStyles=null),this.isFinePointer&&this.cursor&&(this.bodyStyles=Je(i.body,{cursor:this.cursor.onGrab})),this.scrollInView(100,0,ve.out(3)),this.onGrab(this),i.addEventListener("touchmove",this),i.addEventListener("touchend",this),i.addEventListener("touchcancel",this),i.addEventListener("mousemove",this),i.addEventListener("mouseup",this),i.addEventListener("selectstart",this)}handleMove(t){if(!this.grabbed)return;const e=t.changedTouches,s=e?e[0].clientX:t.clientX,i=e?e[0].clientY:t.clientY,{x:r,y:n}=this.transforms.normalizePoint(s,i),o=r-this.pointer[6],a=n-this.pointer[7];let l=t.target,h=!1,d=!1,c=!1;for(;e&&l&&l!==this.$trigger;){const t=Qe(l,"overflow-y");if("hidden"!==t&&"visible"!==t){const{scrollTop:t,scrollHeight:e,clientHeight:s}=l;if(e>s){c=!0,h=t<=3,d=t>=e-s-3;break}}l=l.parentElement}c&&(!h&&!d||h&&a<0||d&&a>0)?(this.pointer[0]=r,this.pointer[1]=n,this.pointer[2]=r,this.pointer[3]=n,this.pointer[4]=r,this.pointer[5]=n,this.pointer[6]=r,this.pointer[7]=n):(ts(t),this.triggerStyles||(this.triggerStyles=Je(this.$trigger,{pointerEvents:"none"})),this.$trigger.addEventListener("touchstart",ts,{passive:!1}),this.$trigger.addEventListener("touchmove",ts,{passive:!1}),this.$trigger.addEventListener("touchend",ts),(this.dragged||!this.disabled[0]&&tt(o)>this.dragThreshold||!this.disabled[1]&&tt(a)>this.dragThreshold)&&(this.updateTicker.resume(),this.pointer[2]=this.pointer[0],this.pointer[3]=this.pointer[1],this.pointer[0]=r,this.pointer[1]=n,this.dragged=!0,this.released=!1,this.onDrag(this)))}handleUp(){if(!this.grabbed)return;this.updateTicker.pause(),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.bodyStyles&&(this.bodyStyles.revert(),this.bodyStyles=null);const[t,e]=this.disabled,[s,n,o,a,l,h]=this.pointer,[d,c,u,p]=this.containerBounds,[m,f]=this.snapped,g=this.releaseXSpring,y=this.releaseYSpring,v=this.releaseEase,b=this.hasReleaseSpring,_=this.overshootCoords,T=this.x,x=this.y,w=this.computeVelocity(s-l,n-h),S=this.angle=ot(n-a,s-o),$=150*w,C=(1-this.releaseContainerFriction)*this.dragSpeed,E=T+K(S)*$,k=x+J(S)*$,N=E>c?c+(E-c)*C:Eu?u+(k-u)*C:kc?-1:1:TV&&(V=B)}if(!e){const e=R===u?x>u?-1:1:xV&&(V=P)}if(!b&&L&&C&&(B||P)){const t=r.blend;new Be(_,{x:{to:N,duration:.65*B},y:{to:D,duration:.65*P},ease:v,composition:t}).init(),new Be(_,{x:{to:I,duration:B},y:{to:R,duration:P},ease:v,composition:t}).init(),this.overshootTicker.stretch(nt(B,P)).restart()}else t||this.animate[this.xProp](I,B,F),e||this.animate[this.yProp](R,P,M);this.scrollInView(V,this.scrollThreshold,v);let O=!1;I!==m&&(this.snapped[0]=I,this.snapX&&(O=!0)),R!==f&&this.snapY&&(this.snapped[1]=R,this.snapY&&(O=!0)),O&&this.onSnap(this),this.grabbed=!1,this.dragged=!1,this.updated=!0,this.released=!0,this.onRelease(this),this.$trigger.removeEventListener("touchstart",ts),this.$trigger.removeEventListener("touchmove",ts),this.$trigger.removeEventListener("touchend",ts),i.removeEventListener("touchmove",this),i.removeEventListener("touchend",this),i.removeEventListener("touchcancel",this),i.removeEventListener("mousemove",this),i.removeEventListener("mouseup",this),i.removeEventListener("selectstart",this)}reset(){return this.stop(),this.resizeTicker.pause(),this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.setX(0,!0),this.setY(0,!0),this.coords[0]=0,this.coords[1]=0,this.pointer[0]=0,this.pointer[1]=0,this.pointer[2]=0,this.pointer[3]=0,this.pointer[4]=0,this.pointer[5]=0,this.pointer[6]=0,this.pointer[7]=0,this.velocity=0,this.velocityStack[0]=0,this.velocityStack[1]=0,this.velocityStack[2]=0,this.velocityStackIndex=0,this.angle=0,this}enable(){return this.enabled||(this.enabled=!0,this.$target.classList.remove("is-disabled"),this.touchActionStyles=Je(this.$trigger,{touchAction:this.disabled[0]?"pan-x":this.disabled[1]?"pan-y":"none"}),this.$trigger.addEventListener("touchstart",this,{passive:!0}),this.$trigger.addEventListener("mousedown",this,{passive:!0}),this.$trigger.addEventListener("mouseenter",this)),this}disable(){return this.enabled=!1,this.grabbed=!1,this.dragged=!1,this.updated=!1,this.released=!1,this.canScroll=!1,this.touchActionStyles.revert(),this.cursorStyles&&(this.cursorStyles.revert(),this.cursorStyles=null),this.triggerStyles&&(this.triggerStyles.revert(),this.triggerStyles=null),this.bodyStyles&&(this.bodyStyles.revert(),this.bodyStyles=null),this.targetStyles&&(this.targetStyles.revert(),this.targetStyles=null),this.$target.classList.add("is-disabled"),this.$trigger.removeEventListener("touchstart",this),this.$trigger.removeEventListener("mousedown",this),this.$trigger.removeEventListener("mouseenter",this),i.removeEventListener("touchmove",this),i.removeEventListener("touchend",this),i.removeEventListener("touchcancel",this),i.removeEventListener("mousemove",this),i.removeEventListener("mouseup",this),i.removeEventListener("selectstart",this),this}revert(){return this.reset(),this.disable(),this.$target.classList.remove("is-disabled"),this.updateTicker.revert(),this.overshootTicker.revert(),this.resizeTicker.revert(),this.animate.revert(),this.resizeObserver.disconnect(),this}handleEvent(t){switch(t.type){case"mousedown":case"touchstart":this.handleDown(t);break;case"mousemove":case"touchmove":this.handleMove(t);break;case"mouseup":case"touchend":case"touchcancel":this.handleUp();break;case"mouseenter":this.handleHover();break;case"selectstart":ts(t)}}}const os=(t=y)=>new ne({duration:1*A.timeScale,onComplete:t},null,0).resume(),as=t=>{let e;return(...s)=>{let i,r,n,o,a;e&&(i=e.currentIteration,r=e.iterationProgress,n=e.reversed,o=e._alternate,a=e._startTime,e.revert());const l=t(...s);return l&&!O(l)&&l.revert&&(e=l),z(r)||(e.currentIteration=i,e.iterationProgress=(o&&i%2?!n:n)?1-r:r,e._startTime=a),l||y}};class ls{constructor(t={}){D.current&&D.current.register(this);const e=t.root;let r=i;e&&(r=e.current||e.nativeElement||ae(e)[0]||i);const n=t.defaults,o=A.defaults,a=t.mediaQueries;if(this.defaults=n?gt(n,o):o,this.root=r,this.constructors=[],this.revertConstructors=[],this.revertibles=[],this.constructorsOnce=[],this.revertConstructorsOnce=[],this.revertiblesOnce=[],this.once=!1,this.onceIndex=0,this.methods={},this.matches={},this.mediaQueryLists={},this.data={},a)for(let t in a){const e=s.matchMedia(a[t]);this.mediaQueryLists[t]=e,e.addEventListener("change",this)}}register(t){(this.once?this.revertiblesOnce:this.revertibles).push(t)}execute(t){let e=D.current,s=D.root,i=A.defaults;D.current=this,D.root=this.root,A.defaults=this.defaults;const r=this.mediaQueryLists;for(let t in r)this.matches[t]=r[t].matches;const n=t(this);return D.current=e,D.root=s,A.defaults=i,n}refresh(){return this.onceIndex=0,this.execute(()=>{let t=this.revertibles.length,e=this.revertConstructors.length;for(;t--;)this.revertibles[t].revert();for(;e--;)this.revertConstructors[e](this);this.revertibles.length=0,this.revertConstructors.length=0,this.constructors.forEach(t=>{const e=t(this);O(e)&&this.revertConstructors.push(e)})}),this}add(t,e){if(this.once=!1,O(t)){const e=t;this.constructors.push(e),this.execute(()=>{const t=e(this);O(t)&&this.revertConstructors.push(t)})}else this.methods[t]=(...t)=>this.execute(()=>e(...t));return this}addOnce(t){if(this.once=!0,O(t)){const e=this.onceIndex++;if(this.constructorsOnce[e])return this;const s=t;this.constructorsOnce[e]=s,this.execute(()=>{const t=s(this);O(t)&&this.revertConstructorsOnce.push(t)})}return this}keepTime(t){this.once=!0;const e=this.onceIndex++,s=this.constructorsOnce[e];if(O(s))return s(this);const i=as(t);let r;return this.constructorsOnce[e]=i,this.execute(()=>{r=i(this)}),r}handleEvent(t){"change"===t.type&&this.refresh()}revert(){const t=this.revertibles,e=this.revertConstructors,s=this.revertiblesOnce,i=this.revertConstructorsOnce,r=this.mediaQueryLists;let n=t.length,o=e.length,a=s.length,l=i.length;for(;n--;)t[n].revert();for(;o--;)e[o](this);for(;a--;)s[a].revert();for(;l--;)i[l](this);for(let t in r)r[t].removeEventListener("change",this);t.length=0,e.length=0,this.constructors.length=0,s.length=0,i.length=0,this.constructorsOnce.length=0,this.onceIndex=0,this.matches={},this.methods={},this.mediaQueryLists={},this.data={}}}const hs=(t,e)=>t&&O(t)?t(e):t,ds=new Map;class cs{constructor(t){this.element=t,this.useWin=this.element===i.body,this.winWidth=0,this.winHeight=0,this.width=0,this.height=0,this.left=0,this.top=0,this.scale=1,this.zIndex=0,this.scrollX=0,this.scrollY=0,this.prevScrollX=0,this.prevScrollY=0,this.scrollWidth=0,this.scrollHeight=0,this.velocity=0,this.backwardX=!1,this.backwardY=!1,this.scrollTicker=new ne({autoplay:!1,onBegin:()=>this.dataTimer.resume(),onUpdate:()=>{const t=this.backwardX||this.backwardY;yt(this,t=>t.handleScroll(),t)},onComplete:()=>this.dataTimer.pause()}).init(),this.dataTimer=new ne({autoplay:!1,frameRate:30,onUpdate:t=>{const e=t.deltaTime,s=this.prevScrollX,i=this.prevScrollY,r=this.scrollX,n=this.scrollY,o=s-r,a=i-n;this.prevScrollX=r,this.prevScrollY=n,o&&(this.backwardX=s>r),a&&(this.backwardY=i>n),this.velocity=dt(e>0?Math.sqrt(o*o+a*a)/e:0,5)}}).init(),this.resizeTicker=new ne({autoplay:!1,duration:250*A.timeScale,onComplete:()=>{this.updateWindowBounds(),this.refreshScrollObservers(),this.handleScroll()}}).init(),this.wakeTicker=new ne({autoplay:!1,duration:500*A.timeScale,onBegin:()=>{this.scrollTicker.resume()},onComplete:()=>{this.scrollTicker.pause()}}).init(),this._head=null,this._tail=null,this.updateScrollCoords(),this.updateWindowBounds(),this.updateBounds(),this.refreshScrollObservers(),this.handleScroll(),this.resizeObserver=new ResizeObserver(()=>this.resizeTicker.restart()),this.resizeObserver.observe(this.element),(this.useWin?s:this.element).addEventListener("scroll",this,!1)}updateScrollCoords(){const t=this.useWin,e=this.element;this.scrollX=dt(t?s.scrollX:e.scrollLeft,0),this.scrollY=dt(t?s.scrollY:e.scrollTop,0)}updateWindowBounds(){this.winWidth=s.innerWidth,this.winHeight=(()=>{const t=i.createElement("div");i.body.appendChild(t),t.style.height="100lvh";const e=t.offsetHeight;return i.body.removeChild(t),e})()}updateBounds(){const t=getComputedStyle(this.element),e=this.element;let s,i;if(this.scrollWidth=e.scrollWidth+parseFloat(t.marginLeft)+parseFloat(t.marginRight),this.scrollHeight=e.scrollHeight+parseFloat(t.marginTop)+parseFloat(t.marginBottom),this.updateWindowBounds(),this.useWin)s=this.winWidth,i=this.winHeight;else{const t=e.getBoundingClientRect();s=e.clientWidth,i=e.clientHeight,this.top=t.top,this.left=t.left,this.scale=t.width?s/t.width:t.height?i/t.height:1}this.width=s,this.height=i}refreshScrollObservers(){yt(this,t=>{t._debug&&t.removeDebug()}),this.updateBounds(),yt(this,t=>{t.refresh(),t.onResize(t),t._debug&&t.debug()})}refresh(){this.updateWindowBounds(),this.updateBounds(),this.refreshScrollObservers(),this.handleScroll()}handleScroll(){this.updateScrollCoords(),this.wakeTicker.restart()}handleEvent(t){"scroll"===t.type&&this.handleScroll()}revert(){this.scrollTicker.cancel(),this.dataTimer.cancel(),this.resizeTicker.cancel(),this.wakeTicker.cancel(),this.resizeObserver.disconnect(),(this.useWin?s:this.element).removeEventListener("scroll",this),ds.delete(this.element)}}const us=(t,e,s,i,r)=>{const n="min"===e,o="max"===e,a="top"===e||"left"===e||"start"===e||n?0:"bottom"===e||"right"===e||"end"===e||o?"100%":"center"===e?"50%":e,{n:l,u:h}=kt(a,Dt);let d=l;return"%"===h?d=l/100*s:h&&(d=ce(t,Dt,"px",!0).n),o&&i<0&&(d+=i),n&&r>0&&(d+=r),d},ps=(t,e,s,i,r)=>{let n;if(V(e)){const o=E.exec(e);if(o){const a=o[0],l=a[0],h=e.split(a),d="min"===h[0],c="max"===h[0],u=us(t,h[0],s,i,r),p=us(t,h[1],s,i,r);if(d){const e=Et(us(t,"min",s),p,l);n=eu?u:e}else n=Et(u,p,l)}else n=us(t,e,s,i,r)}else n=e;return dt(n,0)},ms=t=>{let e;const s=t.targets;for(let t=0,i=s.length;t()=>{const e=this.linked;return e&&e[t]?e[t]():null}):null,h=a&&l.length>2;this.index=fs++,this.id=z(t.id)?this.index:t.id,this.container=(t=>{const e=t&&ae(t)[0]||i.body;let s=ds.get(e);return s||(s=new cs(e),ds.set(e,s)),s})(t.container),this.target=null,this.linked=null,this.repeat=null,this.horizontal=null,this.enter=null,this.leave=null,this.sync=n||o||!!l,this.syncEase=n?s:null,this.syncSmooth=o?!0===e||r?1:e:null,this.onSyncEnter=l&&!h&&l[0]?l[0]:y,this.onSyncLeave=l&&!h&&l[1]?l[1]:y,this.onSyncEnterForward=l&&h&&l[0]?l[0]:y,this.onSyncLeaveForward=l&&h&&l[1]?l[1]:y,this.onSyncEnterBackward=l&&h&&l[2]?l[2]:y,this.onSyncLeaveBackward=l&&h&&l[3]?l[3]:y,this.onEnter=t.onEnter||y,this.onLeave=t.onLeave||y,this.onEnterForward=t.onEnterForward||y,this.onLeaveForward=t.onLeaveForward||y,this.onEnterBackward=t.onEnterBackward||y,this.onLeaveBackward=t.onLeaveBackward||y,this.onUpdate=t.onUpdate||y,this.onResize=t.onResize||y,this.onSyncComplete=t.onSyncComplete||y,this.reverted=!1,this.ready=!1,this.completed=!1,this.began=!1,this.isInView=!1,this.forceEnter=!1,this.hasEntered=!1,this.offset=0,this.offsetStart=0,this.offsetEnd=0,this.distance=0,this.prevProgress=0,this.thresholds=["start","end","end","start"],this.coords=[0,0,0,0],this.debugStyles=null,this.$debug=null,this._params=t,this._debug=xt(t.debug,!1),this._next=null,this._prev=null,bt(this.container,this),os(()=>{if(!this.reverted){if(!this.target){const e=ae(t.target)[0];this.target=e||i.body,this.refresh()}this._debug&&this.debug()}})}link(t){if(t&&(t.pause(),this.linked=t,z(t)||z(t.persist)||(t.persist=!0),!this._params.target)){let e;z(t.targets)?yt(t,t=>{t.targets&&!e&&(e=ms(t))}):e=ms(t),this.target=e||i.body,this.refresh()}return this}get velocity(){return this.container.velocity}get backward(){return this.horizontal?this.container.backwardX:this.container.backwardY}get scroll(){return this.horizontal?this.container.scrollX:this.container.scrollY}get progress(){const t=(this.scroll-this.offsetStart)/this.distance;return t===1/0||isNaN(t)?0:dt(ht(t,0,1),6)}refresh(){this.ready=!0,this.reverted=!1;const t=this._params;return this.repeat=xt(hs(t.repeat,this),!0),this.horizontal="x"===xt(hs(t.axis,this),"y"),this.enter=xt(hs(t.enter,this),"end start"),this.leave=xt(hs(t.leave,this),"start end"),this.updateBounds(),this.handleScroll(),this}removeDebug(){return this.$debug&&(this.$debug.parentNode.removeChild(this.$debug),this.$debug=null),this.debugStyles&&(this.debugStyles.revert(),this.$debug=null),this}debug(){this.removeDebug();const t=this.container,e=this.horizontal,s=t.element.querySelector(":scope > .animejs-onscroll-debug"),r=i.createElement("div"),n=i.createElement("div"),o=i.createElement("div"),a=gs[this.index%gs.length],l=t.useWin,h=l?t.winWidth:t.width,d=l?t.winHeight:t.height,c=t.scrollWidth,u=t.scrollHeight,p=this.container.width>360?320:260,m=e?0:10,f=e?10:0,g=e?24:p/2,y=e?g:15,v=e?60:g,b=e?v:y,_=e?"repeat-x":"repeat-y",T=t=>e?"0px "+t+"px":t+"px 2px",x=t=>`linear-gradient(${e?90:0}deg, ${t} 2px, transparent 1px)`,w=(t,e,s,i,r)=>`position:${t};left:${e}px;top:${s}px;width:${i}px;height:${r}px;`;r.style.cssText=`${w("absolute",m,f,e?c:p,e?p:u)}\n pointer-events: none;\n z-index: ${this.container.zIndex++};\n display: flex;\n flex-direction: ${e?"column":"row"};\n filter: drop-shadow(0px 1px 0px rgba(0,0,0,.75));\n `,n.style.cssText=`${w("sticky",0,0,e?h:g,e?g:d)}`,s||(n.style.cssText+=`background:\n ${x("#FFFF")}${T(g-10)} / 100px 100px ${_},\n ${x("#FFF8")}${T(g-10)} / 10px 10px ${_};\n `),o.style.cssText=`${w("relative",0,0,e?c:g,e?g:u)}`,s||(o.style.cssText+=`background:\n ${x("#FFFF")}${T(0)} / ${e?"100px 10px":"10px 100px"} ${_},\n ${x("#FFF8")}${T(0)} / ${e?"10px 0px":"0px 10px"} ${_};\n `);const S=[" enter: "," leave: "];this.coords.forEach((t,s)=>{const r=s>1,l=(r?0:this.offset)+t,m=s%2,f=l(r?e?h:d:e?c:u)-b,_=(r?m&&!f:!m&&!f)||g,T=i.createElement("div"),x=i.createElement("div"),$=e?_?"right":"left":_?"bottom":"top",C=_?(e?v:y)+(r?e?-1:g?0:-2:e?-1:-2):e?1:0;x.innerHTML=`${this.id}${S[m]}${this.thresholds[s]}`,T.style.cssText=`${w("absolute",0,0,v,y)}\n display: flex;\n flex-direction: ${e?"column":"row"};\n justify-content: flex-${r?"start":"end"};\n align-items: flex-${_?"end":"start"};\n border-${$}: 2px solid ${a};\n `,x.style.cssText=`\n overflow: hidden;\n max-width: ${p/2-10}px;\n height: ${y};\n margin-${e?_?"right":"left":_?"bottom":"top"}: -2px;\n padding: 1px;\n font-family: ui-monospace, monospace;\n font-size: 10px;\n letter-spacing: -.025em;\n line-height: 9px;\n font-weight: 600;\n text-align: ${e&&_||!e&&!r?"right":"left"};\n white-space: pre;\n text-overflow: ellipsis;\n color: ${m?a:"rgba(0,0,0,.75)"};\n background-color: ${m?"rgba(0,0,0,.65)":a};\n border: 2px solid ${m?a:"transparent"};\n border-${e?_?"top-left":"top-right":_?"top-left":"bottom-left"}-radius: 5px;\n border-${e?_?"bottom-left":"bottom-right":_?"top-right":"bottom-right"}-radius: 5px;\n `,T.appendChild(x);let E=l-C+(e?1:0);T.style[e?"left":"top"]=`${E}px`,(r?n:o).appendChild(T)}),r.appendChild(n),r.appendChild(o),t.element.appendChild(r),s||r.classList.add("animejs-onscroll-debug"),this.$debug=r,"static"===Qe(t.element,"position")&&(this.debugStyles=Je(t.element,{position:"relative "}))}updateBounds(){let t;this._debug&&this.removeDebug();const e=this.target,s=this.container,r=this.horizontal,n=this.linked;let o,a=e;for(n&&(o=n.currentTime,n.seek(0,!0));a&&a!==s.element&&a!==i.body;){const e="sticky"===Qe(a,"position")&&Je(a,{position:"static"});a=a.parentElement,e&&(t||(t=[]),t.push(e))}const l=e.getBoundingClientRect(),h=s.scale,d=(r?l.left+s.scrollX-s.left:l.top+s.scrollY-s.top)*h,c=(r?l.width:l.height)*h,u=r?s.width:s.height,p=(r?s.scrollWidth:s.scrollHeight)-u,m=this.enter,f=this.leave;let g="start",y="end",v="end",b="start";if(V(m)){const t=m.split(" ");v=t[0],g=t.length>1?t[1]:g}else if(F(m)){const t=m;z(t.container)||(v=t.container),z(t.target)||(g=t.target)}else M(m)&&(v=m);if(V(f)){const t=f.split(" ");b=t[0],y=t.length>1?t[1]:y}else if(F(f)){const t=f;z(t.container)||(b=t.container),z(t.target)||(y=t.target)}else M(f)&&(b=f);const _=ps(e,g,c),T=ps(e,y,c),x=_+d-u,w=T+d-p,S=ps(e,v,u,x,w),$=ps(e,b,u,x,w),C=_+d-S,E=T+d-$,k=E-C;this.offset=d,this.offsetStart=C,this.offsetEnd=E,this.distance=k<=0?0:k,this.thresholds=[g,y,v,b],this.coords=[_,T,S,$],t&&t.forEach(t=>t.revert()),n&&n.seek(o,!0),this._debug&&this.debug()}handleScroll(){if(!this.ready)return;const t=this.linked,e=this.sync,s=this.syncEase,i=this.syncSmooth,r=t&&(s||i),n=this.horizontal,o=this.container,a=this.scroll,l=a<=this.offsetStart,h=a>=this.offsetEnd,d=!l&&!h,c=a===this.offsetStart||a===this.offsetEnd,u=!this.hasEntered&&c,p=this._debug&&this.$debug;let m=!1,f=!1,g=this.progress;if(l&&this.began&&(this.began=!1),g>0&&!this.began&&(this.began=!0),r){const e=t.progress;if(i&&M(i)){if(i<1){const t=1e-4,s=eg&&!g?-t:0;g=dt(ut(e,g,ut(.01,.2,i))+s,6)}}else s&&(g=s(g));m=g!==this.prevProgress,f=1===e,m&&!f&&i&&e&&o.wakeTicker.restart()}if(p){const t=n?o.scrollY:o.scrollX;p.style[n?"top":"left"]=t+10+"px"}(d&&!this.isInView||u&&!this.forceEnter&&!this.hasEntered)&&(d&&(this.isInView=!0),this.forceEnter&&this.hasEntered?d&&(this.forceEnter=!1):(p&&d&&(p.style.zIndex=""+this.container.zIndex++),this.onSyncEnter(this),this.onEnter(this),this.backward?(this.onSyncEnterBackward(this),this.onEnterBackward(this)):(this.onSyncEnterForward(this),this.onEnterForward(this)),this.hasEntered=!0,u&&(this.forceEnter=!0))),(d||!d&&this.isInView)&&(m=!0),m&&(r&&t.seek(t.duration*g),this.onUpdate(this)),!d&&this.isInView&&(this.isInView=!1,this.onSyncLeave(this),this.onLeave(this),this.backward?(this.onSyncLeaveBackward(this),this.onLeaveBackward(this)):(this.onSyncLeaveForward(this),this.onLeaveForward(this)),e&&!i&&(f=!0)),g>=1&&this.began&&!this.completed&&(e&&f||!e)&&(e&&this.onSyncComplete(this),this.completed=!0,(!this.repeat&&!t||!this.repeat&&t&&t.completed)&&this.revert()),g<1&&this.completed&&(this.completed=!1),this.prevProgress=g}revert(){if(this.reverted)return;const t=this.container;return vt(t,this),t._head||t.revert(),this._debug&&this.removeDebug(),this.reverted=!0,this.ready=!1,this}}const vs=(t,e,s)=>(((1-3*s+3*e)*t+(3*s-6*e))*t+3*e)*t,bs=(t=.5,e=0,s=.5,i=1)=>t===e&&s===i?ue:r=>0===r||1===r?r:vs(((t,e,s)=>{let i,r,n=0,o=1,a=0;do{r=n+(o-n)/2,i=vs(r,e,s)-t,i>0?o=r:n=r}while(tt(i)>1e-7&&++a<100);return r})(r,t,s),e,i),_s=(t=10,e)=>{const s=e?st:it;return e=>s(ht(e,0,1)*t)*(1/t)},Ts=(...t)=>{const e=t.length;if(!e)return ue;const s=e-1,i=t[0],r=t[s],n=[0],o=[G(i)];for(let e=1;e{const s=[0],i=t-1;for(let t=1;t{const s=[];for(let i=0;i<=e;i++)s.push(dt(t(i/e),4));return`linear(${s.join(", ")})`},$s={},Cs=t=>{let e=$s[t];if(e)return e;if(e="linear",V(t)){if(L(t,"linear")||L(t,"cubic-")||L(t,"steps")||L(t,"ease"))e=t;else if(L(t,"cubicB"))e=R(t);else{const s=_e(t);O(s)&&(e=s===ue?"linear":Ss(s))}$s[t]=e}else if(O(t)){const s=Ss(t);s&&(e=s)}else t.ease&&(e=Ss(t.ease));return e},Es=["x","y","z"],ks=["perspective","width","height","margin","padding","top","right","bottom","left","borderWidth","fontSize","borderRadius",...Es],Ns=(()=>[...Es,...f.filter(t=>["X","Y","Z"].some(e=>t.endsWith(e)))])();let Ds=null;const As=(t,e,s,i,r)=>{let n=V(e)?e:wt(e,s,i,r,null,null);return M(n)?ks.includes(t)||L(t,"translate")?`${n}px`:L(t,"rotate")||L(t,"skew")?`${n}deg`:`${n}`:n},Is=(t,e,s,i,r,n)=>{let o="0";const a=z(i)?getComputedStyle(t)[e]:As(e,i,t,r,n);return o=z(s)?P(i)?i.map(s=>As(e,s,t,r,n)):a:[As(e,s,t,r,n),a],o};class Rs{constructor(t,s){D.current&&D.current.register(this),H(Ds)&&(!e||!z(CSS)&&Object.hasOwnProperty.call(CSS,"registerProperty")?(f.forEach(t=>{const e=L(t,"skew"),s=L(t,"scale"),i=L(t,"rotate"),r=L(t,"translate"),n=i||e,o=n?"":s?"":r?"":"*";try{CSS.registerProperty({name:"--"+t,syntax:o,inherits:!1,initialValue:r?"0px":n?"0deg":s?"1":"0"})}catch{}}),Ds=!0):Ds=!1);const i=le(t);i.length||console.warn("No target found. Make sure the element you're trying to animate is accessible before creating your animation.");const r=xt(s.autoplay,A.defaults.autoplay),n=!(!r||!r.link)&&r,o=s.alternate&&!0===s.alternate,a=s.reversed&&!0===s.reversed,h=xt(s.loop,A.defaults.loop),d=!0===h||h===1/0?1/0:M(h)?h+1:1,c=o?a?"alternate-reverse":"alternate":a?"reverse":"normal",v=1===A.timeScale?1:u;this.targets=i,this.animations=[],this.controlAnimation=null,this.onComplete=s.onComplete||A.defaults.onComplete,this.duration=0,this.muteCallbacks=!1,this.completed=!1,this.paused=!r||!1!==n,this.reversed=a,this.persist=xt(s.persist,A.defaults.persist),this.autoplay=r,this._speed=xt(s.playbackRate,A.defaults.playbackRate),this._resolve=y,this._completed=0,this._inlineStyles=[],i.forEach((t,e)=>{const r=t[l],n=Ns.some(t=>s.hasOwnProperty(t)),o=t.style,a=this._inlineStyles[e]={},h=xt(s.ease,A.defaults.ease),u=wt(h,t,e,i,null,null),y=O(u)||V(u)?u:h,b=h.ease&&h,_=Cs(y),T=(b?b.settlingDuration:wt(xt(s.duration,A.defaults.duration),t,e,i,null,null))*v,x=wt(xt(s.delay,A.defaults.delay),t,e,i,null,null)*v,w=xt(s.composition,"replace");for(let l in s){if(!q(l))continue;const h={},u={iterations:d,direction:c,fill:"both",easing:_,duration:T,delay:x,composite:w},p=s[l],g=!!n&&(f.includes(l)?l:m.get(l)),y=g?"transform":l;let b;if(a[y]||(a[y]=o[y]),F(p)){const s=p,n=xt(s.ease,_),a=n.ease&&n,d=s.to,c=s.from;if(u.duration=(a?a.settlingDuration:wt(xt(s.duration,T),t,e,i,null,null))*v,u.delay=wt(xt(s.delay,x),t,e,i,null,null)*v,u.composite=xt(s.composition,w),u.easing=Cs(n),b=Is(t,l,c,d,e,i),g?(h[`--${g}`]=b,r[g]=b):h[l]=Is(t,l,c,d,e,i),Ze(this,t,l,h,u),!z(c))if(g){const t=`--${g}`;o.setProperty(t,h[t][0])}else o[l]=h[l][0]}else b=P(p)?p.map(s=>As(l,s,t,e,i)):As(l,p,t,e,i),g?(h[`--${g}`]=b,r[g]=b):h[l]=b,Ze(this,t,l,h,u)}if(n){let t=p;for(let e in r)t+=`${g[e]}var(--${e})) `;o.transform=t}}),n&&this.autoplay.link(this)}forEach(t){try{const e=V(t)?e=>e[t]():t;this.animations.forEach(e)}catch{}return this}get speed(){return this._speed}set speed(t){this._speed=+t,this.forEach(e=>e.playbackRate=t)}get currentTime(){const t=this.controlAnimation,e=A.timeScale;return this.completed?this.duration:t?+t.currentTime*(1===e?1:e):0}set currentTime(t){const e=t*(1===A.timeScale?1:u);this.forEach(t=>{!this.persist&&e>=this.duration&&t.play(),t.currentTime=e})}get progress(){return this.currentTime/this.duration}set progress(t){this.forEach(e=>e.currentTime=t*this.duration||0)}resume(){return this.paused?(this.paused=!1,this.forEach("play")):this}pause(){return this.paused?this:(this.paused=!0,this.forEach("pause"))}alternate(){return this.reversed=!this.reversed,this.forEach("reverse"),this.paused&&this.forEach("pause"),this}play(){return this.reversed&&this.alternate(),this.resume()}reverse(){return this.reversed||this.alternate(),this.resume()}seek(t,e=!1){return e&&(this.muteCallbacks=!0),t{this.targets.forEach(t=>{"none"===t.style.transform&&t.style.removeProperty("transform")})}),this}revert(){return this.cancel().targets.forEach((t,e)=>{const s=t.style,i=this._inlineStyles[e];for(let e in i){const r=i[e];z(r)||r===p?s.removeProperty(R(e)):t.style[e]=r}t.getAttribute("style")===p&&t.removeAttribute("style")}),this}then(t=y){const e=this.then,s=()=>{this.then=null,t(this),this.then=e,this._resolve=y};return new Promise(t=>(this._resolve=()=>t(s()),this.completed&&this._resolve(),this))}}const Ls={animate:(t,e)=>new Rs(t,e),convertEase:Ss};let Bs=0,Ps=0;const Fs=(t,e)=>!(!t||!e)&&(t===e||t.contains(e)),Ms=t=>{if(!t)return null;const e=t.style,s=e.transition||"";return e.setProperty("transition","none","important"),s},Vs=(t,e)=>{if(!t)return;const s=t.style;e?s.transition=e:s.removeProperty("transition")},Os=t=>{const e=t.layout.transitionMuteStore,s=t.$el,i=t.$measure;s&&!e.has(s)&&e.set(s,Ms(s)),i&&!e.has(i)&&e.set(i,Ms(i))},zs=t=>{t.forEach((t,e)=>Vs(e,t)),t.clear()},Hs={display:"none",visibility:"hidden",opacity:"0",transform:"none",position:"static"},Xs=t=>{if(!t)return;const e=t.parentNode;e&&(e._head===t&&(e._head=t._next),e._tail===t&&(e._tail=t._prev),t._prev&&(t._prev._next=t._next),t._next&&(t._next._prev=t._prev),t._prev=null,t._next=null,t.parentNode=null)},Ys=(t,e,s,i)=>{let r=t.dataset.layoutId;r||(r=t.dataset.layoutId="node-"+Ps++);const n=i||{};return n.$el=t,n.$measure=t,n.id=r,n.index=0,n.targets=null,n.delay=0,n.duration=0,n.ease=null,n.state=s,n.layout=s.layout,n.parentNode=e||null,n.isTarget=!1,n.isEntering=!1,n.isLeaving=!1,n.isInlined=!1,n.hasTransform=!1,n.inlineStyles=[],n.inlineTransforms=null,n.inlineTransition=null,n.branchAdded=!1,n.branchRemoved=!1,n.branchNotRendered=!1,n.sizeChanged=!1,n.hasVisibilitySwap=!1,n.hasDisplayNone=!1,n.hasVisibilityHidden=!1,n.measuredInlineTransform=null,n.measuredInlineTransition=null,n.measuredDisplay=null,n.measuredVisibility=null,n.measuredPosition=null,n.measuredHasDisplayNone=!1,n.measuredHasVisibilityHidden=!1,n.measuredIsVisible=!1,n.measuredIsRemoved=!1,n.measuredIsInsideRoot=!1,n.properties={transform:"none",x:0,y:0,left:0,top:0,clientLeft:0,clientTop:0,width:0,height:0},n.layout.properties.forEach(t=>n.properties[t]=0),n._head=null,n._tail=null,n._prev=null,n._next=null,n},Ws=(t,e,s,i)=>{const r=t.$el,n=t.layout.root,o=n===r,a=t.properties,l=t.state.rootNode,h=t.parentNode,d=s.transform,c=r.style.transform,u=!!h&&h.measuredIsRemoved,p=s.position;o&&(t.layout.absoluteCoords="fixed"===p||"absolute"===p),t.$measure=e,t.inlineTransforms=c,t.hasTransform=d&&"none"!==d,t.measuredIsInsideRoot=Fs(n,e),t.measuredInlineTransform=null,t.measuredDisplay=s.display,t.measuredVisibility=s.visibility,t.measuredPosition=p,t.measuredHasDisplayNone="none"===s.display,t.measuredHasVisibilityHidden="hidden"===s.visibility,t.measuredIsVisible=!(t.measuredHasDisplayNone||t.measuredHasVisibilityHidden),t.measuredIsRemoved=t.measuredHasDisplayNone||t.measuredHasVisibilityHidden||u;let m=!1,f=r.previousSibling;for(;f&&(f.nodeType===Node.COMMENT_NODE||f.nodeType===Node.TEXT_NODE&&!f.textContent.trim());)f=f.previousSibling;if(f&&f.nodeType===Node.TEXT_NODE)m=!0;else{for(f=r.nextSibling;f&&(f.nodeType===Node.COMMENT_NODE||f.nodeType===Node.TEXT_NODE&&!f.textContent.trim());)f=f.nextSibling;m=null!==f&&f.nodeType===Node.TEXT_NODE}if(t.isInlined=m,t.hasTransform&&!i){const s=t.layout.transitionMuteStore;s.get(r)||(t.inlineTransition=Ms(r)),e===r?r.style.transform="none":(s.get(e)||(t.measuredInlineTransition=Ms(e)),t.measuredInlineTransform=e.style.transform,e.style.transform="none")}let g,y,v=0,b=0,_=0,T=0;if(!i){const t=e.getBoundingClientRect();v=t.left,b=t.top,_=t.width,T=t.height}for(let t in a){const e="transform"===t?d:s[t]||s.getPropertyValue&&s.getPropertyValue(t);z(e)||(a[t]=e)}if(a.left=v,a.top=b,a.clientLeft=i?0:e.clientLeft,a.clientTop=i?0:e.clientTop,o)t.layout.absoluteCoords?(g=v,y=b):(g=0,y=0);else{const e=h||l,s=e.properties.left,i=e.properties.top,r=e.properties.clientLeft,n=e.properties.clientTop;if(t.layout.absoluteCoords)g=v-s-r,y=b-i-n;else if(e===l){const t=l.properties.left,e=l.properties.top;g=v-t-l.properties.clientLeft,y=b-e-l.properties.clientTop}else g=v-s-r,y=b-i-n}return a.x=g,a.y=y,a.width=_,a.height=T,t},Us=(t,e)=>{if(e)for(let s in e)t.properties[s]=e[s]},qs=(t,e)=>{const s=wt(e.ease,t.$el,t.index,t.targets,null,null),i=O(s)?s:e.ease,r=!z(i)&&!z(i.ease);t.ease=r?i.ease:i,t.duration=r?i.settlingDuration:wt(e.duration,t.$el,t.index,t.targets,null,null),t.delay=wt(e.delay,t.$el,t.index,t.targets,null,null)},js=t=>{const e=t.$el.style,s=t.inlineStyles;s.length=0,t.layout.recordedProperties.forEach(t=>{s.push(t,e[t]||"")})},Gs=t=>{const e=t.$el.style,s=t.inlineStyles;for(let t=0,i=s.length;t{const e=t.inlineTransforms,s=t.$el.style;!t.hasTransform||!e||t.hasTransform&&"none"===s.transform||e&&"none"===e?s.removeProperty("transform"):e&&(s.transform=e);const i=t.$measure;if(t.hasTransform&&i!==t.$el){const e=i.style,s=t.measuredInlineTransform;s&&""!==s?e.transform=s:e.removeProperty("transform")}t.measuredInlineTransform=null,null!==t.inlineTransition&&(Vs(t.$el,t.inlineTransition),t.inlineTransition=null),i!==t.$el&&null!==t.measuredInlineTransition&&(Vs(i,t.measuredInlineTransition),t.measuredInlineTransition=null)},Qs=t=>{(t.measuredIsRemoved||t.hasVisibilitySwap)&&(t.$el.style.removeProperty("display"),t.$el.style.removeProperty("visibility"),t.hasVisibilitySwap&&(t.$measure.style.removeProperty("display"),t.$measure.style.removeProperty("visibility"))),t.layout.pendingRemoval.delete(t.$el)},Js=(t,e,s)=>(e.properties={...t.properties},e.state=s,e.isTarget=t.isTarget,e.hasTransform=t.hasTransform,e.inlineTransforms=t.inlineTransforms,e.measuredIsVisible=t.measuredIsVisible,e.measuredDisplay=t.measuredDisplay,e.measuredIsRemoved=t.measuredIsRemoved,e.measuredHasDisplayNone=t.measuredHasDisplayNone,e.measuredHasVisibilityHidden=t.measuredHasVisibilityHidden,e.hasDisplayNone=t.hasDisplayNone,e.isInlined=t.isInlined,e.hasVisibilityHidden=t.hasVisibilityHidden,e);class Ks{constructor(t){this.layout=t,this.rootNode=null,this.rootNodes=new Set,this.nodes=new Map,this.scrollX=0,this.scrollY=0}revert(){return this.forEachNode(t=>{this.layout.pendingRemoval.delete(t.$el),t.$el.removeAttribute("data-layout-id"),t.$measure.removeAttribute("data-layout-id")}),this.rootNode=null,this.rootNodes.clear(),this.nodes.clear(),this}getNode(t){if(t&&t.dataset)return this.nodes.get(t.dataset.layoutId)}getComputedValue(t,e){const s=this.getNode(t);if(s)return s.properties[e]}forEach(t,e){let s=t,i=0;for(;s;)if(e(s,i++),s._head)s=s._head;else if(s._next)s=s._next;else{for(;s&&!s._next;)s=s.parentNode;s&&(s=s._next)}}forEachRootNode(t){this.forEach(this.rootNode,t)}forEachNode(t){for(const e of this.rootNodes)this.forEach(e,t)}registerElement(t,e){if(!t||1!==t.nodeType)return null;this.layout.transitionMuteStore.has(t)||this.layout.transitionMuteStore.set(t,Ms(t));const s=[t,e],i=this.layout.root;let r=null;for(;s.length;){const t=s.pop(),e=s.pop();if(!e||1!==e.nodeType||X(e))continue;const n=!!t&&t.measuredIsRemoved,o=n?Hs:getComputedStyle(e),a=!!n||"none"===o.display,l=!!n||"hidden"===o.visibility,h=!a&&!l,d=e.dataset.layoutId,c=Fs(i,e);let u=d?this.nodes.get(d):null;if(u&&u.$el!==e){const a=Fs(i,u.$el),l=u.measuredIsVisible;if(a||!c&&(c||l||!h)){if(a&&!l&&h){Ws(u,e,o,n);let t=e.lastElementChild;for(;t;)s.push(t,u),t=t.previousElementSibling;r||(r=u);continue}{let i=e.lastElementChild;for(;i;)s.push(i,t),i=i.previousElementSibling;r||(r=u);continue}}Xs(u),u=Ys(e,t,this,u)}else u=Ys(e,t,this,u);u.branchAdded=!1,u.branchRemoved=!1,u.branchNotRendered=!1,u.isTarget=!1,u.sizeChanged=!1,u.hasVisibilityHidden=l,u.hasDisplayNone=a,u.hasVisibilitySwap=l&&!u.measuredHasVisibilityHidden||a&&!u.measuredHasDisplayNone,this.nodes.set(u.id,u),u.parentNode=t||null,u._prev=null,u._next=null,t?(this.rootNodes.delete(u),t._head?(t._tail._next=u,u._prev=t._tail,t._tail=u):(t._head=u,t._tail=u)):this.rootNodes.add(u),Ws(u,u.$el,o,n);let p=e.lastElementChild;for(;p;)s.push(p,u),p=p.previousElementSibling;r||(r=u)}return r}ensureDetachedNode(t,e){if(!t||t===this.layout.root)return null;const s=t.dataset.layoutId,i=s?this.nodes.get(s):null;if(i&&i.$el===t)return i;let r=null,n=t.parentElement;for(;n&&n!==this.layout.root;){if(e.has(n)){r=this.ensureDetachedNode(n,e);break}n=n.parentElement}return this.registerElement(t,r)}record(){const t=this.layout,e=t.children,s=t.root,i=P(e)?e:[e],r=[],n="*"===e?s:D.root,o=[];let a=s.parentElement;for(;a&&1===a.nodeType;){const t=getComputedStyle(a);if(t.transform&&"none"!==t.transform){const t=a.style.transform||"",e=Ms(a);o.push(a,t,e),a.style.transform="none"}a=a.parentElement}for(let t=0,e=i.length;t{u.push(t.$el)}),this.nodes.forEach((t,e)=>{t.index=c++,t.targets=u,t&&t.measuredIsInsideRoot&&d.add(e)});const p=new Set,m=[];for(let t=0,e=l.length;tthis.recordedProperties.add(t)),this.pendingRemoval=new WeakSet,this.transitionMuteStore=new Map,this.oldState=new Ks(this),this.newState=new Ks(this),this.timeline=null,this.transformAnimation=null,this.animating=[],this.swapping=[],this.leaving=[],this.entering=[],this.oldState.record(),zs(this.transitionMuteStore)}revert(){return this.root.classList.remove("is-animated"),this.timeline&&(this.timeline.complete(),this.timeline=null),this.transformAnimation&&(this.transformAnimation.complete(),this.transformAnimation=null),this.animating.length=this.swapping.length=this.leaving.length=this.entering.length=0,this.oldState.revert(),this.newState.revert(),requestAnimationFrame(()=>zs(this.transitionMuteStore)),this}record(){return this.transformAnimation&&(this.transformAnimation.cancel(),this.transformAnimation=null),this.oldState.record(),this.timeline&&(this.timeline.cancel(),this.timeline=null),this.newState.forEachRootNode(Gs),this}animate(t={}){const e={ease:xt(t.ease,this.params.ease),delay:xt(t.delay,this.params.delay),duration:xt(t.duration,this.params.duration)},s={id:this.id},i=xt(t.onComplete,this.params.onComplete),r=xt(t.onPause,this.params.onPause);for(let e in N)"ease"!==e&&"duration"!==e&&"delay"!==e&&(z(t[e])?z(this.params[e])||(s[e]=this.params[e]):s[e]=t[e]);s.onComplete=()=>{const e=t.autoplay,s=A.editor;if(e&&e.linked||s&&s.showPanel)i&&i(this.timeline);else{this.transformAnimation&&this.transformAnimation.cancel(),f.forEachRootNode(t=>{Qs(t),Gs(t)});for(let t=0,e=w.length;t{this.root.classList.contains("is-animated")||zs(this.transitionMuteStore)})}},s.onPause=()=>{const e=t.autoplay;if(e&&e.linked)return i&&i(this.timeline),void(r&&r(this.timeline));this.root.classList.contains("is-animated")&&(this.transformAnimation&&this.transformAnimation.cancel(),f.forEachRootNode(Qs),this.root.classList.remove("is-animated"),i&&i(this.timeline),r&&r(this.timeline))},s.composition=!1;const n=gt(gt(t.swapAt||{},this.swapAtParams),e),o=gt(gt(t.enterFrom||{},this.enterFromParams),e),a=gt(gt(t.leaveTo||{},this.leaveToParams),e),[l,h]=ti(n),[d,c]=ti(o),[u,p]=ti(a),m=this.oldState,f=this.newState,g=this.animating,y=this.swapping,v=this.entering,b=this.leaving,_=this.pendingRemoval;g.length=y.length=v.length=b.length=0,m.forEachRootNode(Os),f.record(),f.forEachRootNode(js);const T=[],x=[],w=[],S=[],$=f.rootNode,C=$.$el;f.forEachRootNode(t=>{const e=t.$el,s=t.id,i=t.parentNode,r=!!i&&i.branchAdded,n=!!i&&i.branchRemoved,o=!!i&&i.branchNotRendered;let a=m.nodes.get(s);const l=!a;l?(a=Js(t,{},m),m.nodes.set(s,a),a.measuredIsRemoved=!0):a.measuredIsRemoved&&!t.measuredIsRemoved&&(Js(t,a,m),a.measuredIsRemoved=!0);const h=a.parentNode,c=(h?h.id:null)!==(i?i.id:null),p=a.$el!==t.$el,y=a.measuredIsRemoved,x=t.measuredIsRemoved;if(!a.measuredIsRemoved&&!x&&!l&&(c||p)){const t=a.properties.left,e=a.properties.top,s=i||f.rootNode,r=s.id?m.nodes.get(s.id):null,n=r?r.properties.left:s.properties.left,o=r?r.properties.top:s.properties.top,l=r?r.properties.clientLeft:s.properties.clientLeft,h=r?r.properties.clientTop:s.properties.clientTop;a.properties.x=t-n-l,a.properties.y=e-o-h}t.hasVisibilitySwap&&(t.hasVisibilityHidden&&(t.$el.style.visibility="visible",t.$measure.style.visibility="hidden"),t.hasDisplayNone&&(t.$el.style.display=a.measuredDisplay||t.measuredDisplay||"",t.$measure.style.visibility="hidden"));const w=_.has(e),S=a.measuredIsVisible,C=t.measuredIsVisible,E=!S&&C&&!o,k=!x&&(y||w)&&!r,N=x&&!y&&!n,D=N||x&&w&&!n;t.branchAdded=r||k,t.branchRemoved=n||D,t.branchNotRendered=o||x,x&&S&&(t.$el.style.display=a.measuredDisplay,t.$el.style.visibility="visible",Js(a,t,f)),N?(t.isTarget&&(b.push(e),t.isLeaving=!0),_.add(e)):!x&&w&&_.delete(e),k&&!o||E?(Us(a,d),t.isTarget&&(v.push(e),t.isEntering=!0)):D&&!o&&Us(t,u),t===$||!t.isTarget||t.isEntering||t.isLeaving||g.push(e),T.push(e)});let E=0,k=0,D=0;f.forEachRootNode(t=>{const s=t.$el,i=t.parentNode,r=m.nodes.get(t.id),n=t.properties,o=r.properties;let a=i!==$&&i;for(;a&&!a.isTarget&&a!==$;)a=a.parentNode;t===$?(t.index=0,t.targets=g,qs(t,e)):t.isEntering?(t.index=a?a.index:E,t.targets=a?g:v,qs(t,c),E++):t.isLeaving?(t.index=a?a.index:k,t.targets=a?g:b,k++,qs(t,p)):t.isTarget?(t.index=D++,t.targets=g,qs(t,e)):(t.index=a?a.index:0,t.targets=g,qs(t,h)),r.index=t.index,r.targets=t.targets;for(let e in n)n[e]=wt(n[e],s,t.index,t.targets,null,null),o[e]=wt(o[e],s,r.index,r.targets,null,null);const d=Math.abs(n.width-o.width)>1,u=Math.abs(n.height-o.height)>1;if(t.sizeChanged=d||u,t.isTarget&&(!t.measuredIsRemoved&&r.measuredIsVisible||t.measuredIsRemoved&&t.measuredIsVisible)){"none"===n.transform&&"none"===o.transform||(t.hasTransform=!0,w.push(s));for(let t in n)if("transform"!==t&&n[t]!==o[t]){x.push(s);break}}t.isTarget||(y.push(s),t.sizeChanged&&i&&i.isTarget&&i.sizeChanged&&(l.transform&&(t.hasTransform=!0,w.push(s)),S.push(s)))});const I={delay:t=>f.getNode(t).delay,duration:t=>f.getNode(t).duration,ease:t=>f.getNode(t).ease};if(s.defaults=I,this.timeline=Oe(s),!x.length&&!w.length&&!y.length)return zs(this.transitionMuteStore),this.timeline.complete();if(T.length){this.root.classList.add("is-animated");for(let t=0,e=T.length;twindow.scrollTo(m.scrollX,m.scrollY));for(let t=0,e=x.length;t{const e=n[t],s=o[t];"transform"!==t&&e!==s&&(l[t]=[e,s],a=!0)}),a&&this.timeline.add(e,l,0)}}if(y.length){for(let t=0,e=y.length;t{"transform"!==t&&(e.style[t]=`${m.getComputedValue(e,t)}`)})}for(let t=0,e=y.length;t{e.style.width=`${i.width}px`,e.style.height=`${i.height}px`,e.style.minWidth="auto",e.style.minHeight="auto",e.style.maxWidth="none",e.style.maxHeight="none",s.isInlined||(e.style.translate=`${i.x}px ${i.y}px`),this.properties.forEach(t=>{"transform"!==t&&(e.style[t]=`${f.getComputedValue(e,t)}`)})},s.delay+s.duration/2)}if(S.length){const t=xe(f.nodes.get(S[0].dataset.layoutId).ease),e=e=>1-t(1-e),s={};if(l)for(let t in l)"transform"!==t&&(s[t]=[{from:e=>m.getComputedValue(e,t),to:l[t]},{from:l[t],to:e=>f.getComputedValue(e,t),ease:e}]);this.timeline.add(S,s,0)}}const R=w.length;if(R){for(let t=0;tf.getNode(t).isInlined?"0px 0px":`${f.getComputedValue(t,"x")}px ${f.getComputedValue(t,"y")}px`,transform:t=>{const e=f.getComputedValue(t,"transform");if(!S.includes(t))return e;const s=m.getComputedValue(t,"transform"),i=f.getNode(t);return[s,wt(l.transform,t,i.index,i.targets,null,null),e]},autoplay:!1,...I}),this.timeline.sync(this.transformAnimation,0)}return this.timeline.init()}update(t,e={}){return this.record(),t(this),this.animate(e)}}const si=Xe,ii={},ri=t=>(...e)=>{const s=t(...e);return new Proxy(y,{apply:(t,e,[i])=>s(i),get:(t,e)=>{if(ii[e])return ri((...t)=>{const i=ii[e](...t);return t=>i(s(t))})}})},ni=(t,e,s=0)=>{const i=(...t)=>(t.length(...s)=>e?e=>t(...s,e):e=>t(e,...s))(e,s)):e)(...t);return ii[t]||(ii[t]=i),i},oi=ni("roundPad",si.roundPad),ai=ni("padStart",si.padStart),li=ni("padEnd",si.padEnd),hi=ni("wrap",si.wrap),di=ni("mapRange",si.mapRange),ci=ni("degToRad",si.degToRad),ui=ni("radToDeg",si.radToDeg),pi=ni("snap",si.snap),mi=ni("clamp",si.clamp),fi=ni("round",si.round),gi=ni("lerp",si.lerp,1),yi=ni("damp",si.damp,1),vi=(t=0,e=1,s=0)=>{const i=10**s;return Math.floor((Math.random()*(e-t+1/i)+t)*i)/i};let bi=0;const _i=(t,e=0,s=1,i=0)=>{let r=void 0===t?bi++:t;return(t=e,n=s,o=i)=>{r+=1831565813,r=Math.imul(r^r>>>15,1|r),r^=r+Math.imul(r^r>>>7,61|r);const a=10**o;return Math.floor((((r^r>>>14)>>>0)/4294967296*(n-t+1/a)+t)*a)/a}},Ti=t=>t[vi(0,t.length-1)],xi=t=>{let e,s,i=t.length;for(;i;)s=vi(0,--i),e=t[i],t[i]=t[s],t[s]=e;return t},wi=(t,e={})=>{let s,i=[],r=0;const n=e.from,o=e.reversed,a=e.ease,l=!z(a),h=l&&!z(a.ease)?a.ease:l?xe(a):null,d=e.grid,c=!0===d,u=e.axis,m=e.total,f=z(n)||0===n||"first"===n,g="center"===n,y="last"===n,v="random"===n,b=P(n),_=P(t),T=e.use,x=G(_?t[0]:t),w=_?G(t[1]):0,S=$.exec((_?t[1]:t)+p),C=e.start||0+(_?x:0);let E=f?0:M(n)?n:0;return(t,a,l,p,f)=>{const[$]=le(t),k=z(m)?l.length:m,N=!z(T)&&(O(T)?T($,a,k):Ct($,T)),D=M(N)||V(N)&&M(+N)?+N:a;if(g&&(E=(k-1)/2),y&&(E=k-1),!i.length){if(c){let t=!0,e=1/0,s=1/0,r=-1/0,o=-1/0;const a=[],h=[];for(let i=0;ir&&(r=d),c>o&&(o=c)}if(t){let t=a[0],l=h[0];b?(t=e+n[0]*(r-e),l=s+n[1]*(o-s)):g?(t=(e+r)/2,l=(s+o)/2):y?(t=a[k-1],l=h[k-1]):M(n)&&(t=a[n],l=h[n]);for(let e=0;e0&&e0&&d<1/0)for(let t=0,e=i.length;th(t/r)*r)),o&&(i=i.map(t=>u?t<0?-1*t:-t:tt(r-t))),v&&(i=xi(i))}const A=_?(w-x)/r:x;z(s)&&(s=f?Pe(f,z(e.start)?f.iterationDuration:C):C);let I=s+(A*dt(i[D],2)||0);return e.modifier&&(I=e.modifier(I)),S&&(I=`${I}${S[2]}`),I}};var Si=Object.freeze({__proto__:null,$:le,addChild:bt,clamp:mi,cleanInlineStyles:Mt,createSeededRandom:_i,damp:yi,degToRad:ci,forEachChildren:yt,get:Qe,keepTime:as,lerp:gi,mapRange:di,padEnd:li,padStart:ai,radToDeg:ui,random:vi,randomPick:Ti,remove:Ke,removeChild:vt,round:fi,roundPad:oi,set:Je,shuffle:xi,snap:pi,stagger:wi,sync:os,wrap:hi});const $i=t=>{const e=ae(t)[0];return e&&X(e)?e:console.warn(`${t} is not a valid SVGGeometryElement`)},Ci=(t,e,s,i,r)=>{const n=s+i,o=r?Math.max(0,Math.min(n,e)):(n%e+e)%e;return t.getPointAtLength(o)},Ei=(t,e,s=0)=>i=>{const r=+t.getTotalLength(),n=i[a],o=t.getCTM(),l=0===s;return{from:0,to:r,modifier:i=>{const a=i+s*r;if("a"===e){const e=Ci(t,r,a,-1,l),s=Ci(t,r,a,1,l);return 180*ot(s.y-e.y,s.x-e.x)/at}{const s=Ci(t,r,a,0,l);return"x"===e?n||!o?s.x:s.x*o.a+s.y*o.c+o.e:n||!o?s.y:s.x*o.b+s.y*o.d+o.f}}}},ki=(t,e=0)=>{const s=$i(t);if(s)return{translateX:Ei(s,"x",e),translateY:Ei(s,"y",e),rotate:Ei(s,"a",e)}},Ni=(t,e=0,s=0)=>ae(t).map(t=>((t,e,s)=>{const i=u,r=getComputedStyle(t),n=r.strokeLinecap,o="non-scaling-stroke"===r.vectorEffect?t:null;let a=n;const l=new Proxy(t,{get(t,e){const s=t[e];return e===h?t:"setAttribute"===e?(...e)=>{if("draw"===e[0]){const s=e[1].split(" "),r=+s[0],l=+s[1],h=(t=>{let e=1;if(t&&t.getCTM){const s=t.getCTM();s&&(e=(Q(s.a*s.a+s.b*s.b)+Q(s.c*s.c+s.d*s.d))/2)}return e})(o),d=-1e3*r*h,c=l*i*h+d,u=i*h+(0===r&&1===l||1===r&&0===l?0:10*h)-c;if("butt"!==n){const e=r===l?"butt":n;a!==e&&(t.style.strokeLinecap=`${e}`,a=e)}t.setAttribute("stroke-dashoffset",`${d}`),t.setAttribute("stroke-dasharray",`${c} ${u}`)}return Reflect.apply(s,t,e)}:O(s)?(...e)=>Reflect.apply(s,t,e):s}});return"1000"!==t.getAttribute("pathLength")&&(t.setAttribute("pathLength","1000"),l.setAttribute("draw",`${e} ${s}`)),l})(t,e,s)),Di=(t,e=.33)=>(s,i,r,n)=>{if(!(s.tagName||"").toLowerCase().match(/^(path|polygon|polyline)$/))throw new Error(`Can't morph a <${s.tagName}> SVG element. Use , or .`);const o=$i(t);if(!o)throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing , or SVG element.");if(!(o.tagName||"").toLowerCase().match(/^(path|polygon|polyline)$/))throw new Error(`Can't morph a <${o.tagName}> SVG element. Use , or .`);const a="path"===s.tagName,l=a?" ":",",h=n?n._value:null;h&&s.setAttribute(a?"d":"points",h);let d="",c="";if(e){const t=s.getTotalLength(),i=o.getTotalLength(),r=Math.max(Math.ceil(t*e),Math.ceil(i*e));for(let e=0;et.isWordLike||" "===t.segment||M(+t.segment),Wi=t=>t.setAttribute("aria-hidden","true"),Ui=(t,e)=>[...t.querySelectorAll(`[data-${e}]:not([data-${e}] [data-${e}])`)],qi={line:"#00D672",word:"#FF4B4B",char:"#5A87FF"},ji=t=>{if(!t.childElementCount&&!t.textContent.trim()){const e=t.parentElement;t.remove(),e&&ji(e)}},Gi=(t,e,s)=>{const i=t.getAttribute(Oi);if(null!==i&&+i!==e||"BR"===t.tagName){s.add(t);const e=t.previousSibling,i=t.nextSibling;e&&3===e.nodeType&&Pi.test(e.textContent)&&s.add(e),i&&3===i.nodeType&&Pi.test(i.textContent)&&s.add(i)}let r=t.childElementCount;for(;r--;)Gi(t.children[r],e,s);return s},Zi=(t,e={})=>{let s="";const i=V(e.class)?` class="${e.class}"`:"",r=xt(e.clone,!1),n=xt(e.wrap,!1),o=n?!0===n?"clip":n:!!r&&"clip";return n&&(s+=``),s+=``,r?(s+="{value}",s+=`{value}`):s+="{value}",s+="",n&&(s+=""),s},Qi=(t,e,s,i,r,n,o,a,l)=>{const h=r===Fi,d=r===Vi,c=`_${r}_`,u=O(t)?t(s):t,p=h?"block":"inline-block";Xi.innerHTML=u.replace(Ri,``).replace(Li,`${d?l:h?o:a}`);const m=Xi.content,f=m.firstElementChild,g=m.querySelector(`[data-${r}]`)||f,y=m.querySelectorAll(`i.${c}`),v=y.length;if(v){f.style.display=p,g.style.display=p,g.setAttribute(Oi,`${o}`),h||(g.setAttribute("data-word",`${a}`),d&&g.setAttribute("data-char",`${l}`));let t=v;for(;t--;){const e=y[t],i=e.parentElement;i.style.display=p,h?i.innerHTML=s.innerHTML:i.replaceChild(s.cloneNode(!0),e)}e.push(g),i.appendChild(m)}else console.warn('The expression "{value}" is missing from the provided template.');return n&&(f.style.outline=`1px dotted ${qi[r]}`),f};class Ji{constructor(t,s={}){zi||(zi=Ii?new Ii([],{granularity:Mi}):{segment:t=>{const e=[],s=t.split(Bi);for(let t=0,i=s.length;t[...t].map(t=>({segment:t}))}),!Xi&&e&&(Xi=i.createElement("template")),D.current&&D.current.register(this);const{words:r,chars:n,lines:o,accessible:a,includeSpaces:l,debug:h}=s,d=(t=P(t)?t[0]:t)&&t.nodeType?t:(oe(t)||[])[0],c=!0===o?{}:o,u=!0===r||z(r)?{}:r,p=!0===n?{}:n;this.debug=xt(h,!1),this.includeSpaces=xt(l,!1),this.accessible=xt(a,!0),this.linesOnly=c&&!u&&!p,this.lineTemplate=F(c)?Zi(Fi,c):c,this.wordTemplate=F(u)||this.linesOnly?Zi(Mi,u):u,this.charTemplate=F(p)?Zi(Vi,p):p,this.$target=d,this.html=d&&d.innerHTML,this.lines=[],this.words=[],this.chars=[],this.effects=[],this.effectsCleanups=[],this.cache=null,this.ready=!1,this.width=0,this.resizeTimeout=null;const m=()=>this.html&&(c||u||p)&&this.split();this.resizeObserver=new ResizeObserver(()=>{clearTimeout(this.resizeTimeout),this.resizeTimeout=setTimeout(()=>{const t=d.offsetWidth;t!==this.width&&(this.width=t,m())},150)}),this.lineTemplate&&!this.ready?i.fonts.ready.then(m):m(),d?this.resizeObserver.observe(d):console.warn("No Text Splitter target found.")}addEffect(t){if(!O(t))return console.warn("Effect must return a function."),this;const e=as(t);return this.effects.push(e),this.ready&&(this.effectsCleanups[this.effects.length-1]=e(this)),this}revert(){return clearTimeout(this.resizeTimeout),this.lines.length=this.words.length=this.chars.length=0,this.resizeObserver.disconnect(),this.effectsCleanups.forEach(t=>O(t)?t(this):t.revert&&t.revert()),this.$target.innerHTML=this.html,this}splitNode(t){const e=this.wordTemplate,s=this.charTemplate,r=this.includeSpaces,n=this.debug,o=t.nodeType;if(3===o){const o=t.nodeValue;if(o.trim()){const a=[],l=this.words,h=this.chars,d=zi.segment(o),c=i.createDocumentFragment();let u=null;for(const t of d){const e=t.segment,s=Yi(t);if(!u||s&&u&&Yi(u))a.push(e);else{const t=a.length-1,s=a[t];Bi.test(s)||Bi.test(e)?a.push(e):a[t]+=e}u=t}for(let t=0,o=a.length;tO(t)&&t(this)),s||(t&&(e.innerHTML=this.html,this.words.length=this.chars.length=0),this.splitNode(e),this.cache=e.innerHTML),l&&(s&&(e.innerHTML=this.cache),this.lines.length=0,n&&(this.words=Ui(e,Mi))),o&&(l||n)&&(this.chars=Ui(e,Vi));const h=this.words.length?this.words:this.chars;let d,c=0;for(let t=0,e=h.length;t.5*i&&c++,e.setAttribute(Oi,`${c}`);const r=e.querySelectorAll(`[${Oi}]`);let n=r.length;for(;n--;)r[n].setAttribute(Oi,`${c}`);d=s}if(l){const t=i.createDocumentFragment(),s=new Set,a=[];for(let t=0;t{const e=t.parentNode;e&&(1===t.nodeType&&s.add(e),e.removeChild(t))}),a.push(i)}s.forEach(ji);for(let e=0,s=a.length;ethis.effectsCleanups[e]=t(this)),this}refresh(){this.split(!0)}}const Ki=(t,e)=>new Ji(t,e),tr=(t,e)=>(console.warn("text.split() is deprecated, import splitText() directly, or text.splitText()"),new Ji(t,e)),er=t=>{let e="";for(let s=0,i=t.length;s{t||(t={});const e=t.chars,s=xe(t.ease||"linear"),i=t.text,r=t.from,n=t.reversed||!1,o=t.perturbation||0,a=t.cursor,l=!0===a?"_":"number"==typeof a?String.fromCharCode(a):"string"==typeof a?a:"",h=l.length,d=t.seed||0,c=void 0===t.override||t.override,u=t.revealRate||60,p=1e3*A.timeScale/u,m=t.settleDuration||300*A.timeScale,f=t.settleRate||30,g=t.duration,v=t.revealDelay,b=t.delay,_=t.onChange||y;return(t,a,u,y)=>{const T="function"==typeof e?e(t,a,u):e||"a-zA-Z0-9!%#_",x=er(sr[T]||T),w=x.length-1,S="function"==typeof g?g(t,a,u):g,$="function"==typeof v?v(t,a,u):v||0,C="function"==typeof b?b(t,a,u):b||0,E=d?_i(d):_i();ir.has(t)||ir.set(t,t.textContent);const k=y?y._value:t.textContent,N=void 0!==i?"function"==typeof i?i(t,a,u):i:y?y._value:ir.get(t),D=" "===N||" "===N?" ":N,I=" "===k?0:k.length,R=D.length,L=!0===c?x:"string"==typeof c&&c.length>0?er(sr[c]||c):null,B=L?L.length-1:0,P=" "===c?" ":null,F=""===c?R:Math.max(I,R),M=S>0?S:(F-1)*p+m,V=dt((M+$)/A.timeScale,0)*A.timeScale,O=$>0?dt($/V,12):0,z=void 0===r||"auto"===r?R0;t--){const e=E(0,t),s=H[t];H[t]=H[e],H[e]=s}}else{const t="right"===z?(""!==c&&I?I:F)-1:"center"===z?((""!==c&&I?I:F)-1)/2:"number"==typeof z?z:0,e=Math.abs,s=new Array(F);for(let t=0;te(s-t)-e(i-t));for(let t=0;t0?o*X:0;for(let t=0;t0?(E(0,2e3)-1e3)/1e3*G:0,s=G>0?(E(0,2e3)-1e3)/1e3*G:0;q[t]=H[t]*Y+e,j[t]=Math.ceil((q[t]+X+s)/U)*U}if(Rt&&(t=j[e]);const e=new Array(R);for(let t=0;tH[t]-H[e]);const s=(1-t)/R;for(let i=0;ij[e[i]]&&(j[e[i]]=r)}}const Z=new Array(F);for(let t=0;t0;return{from:0,to:1,duration:V,delay:C,ease:"linear",modifier:t=>{if(t===K)return et;if(K=t,C>0&&t<=0)return et=k,k;if(t<=0)return et=J,J;if(t>=1)return et=D,D;et="";const e=t/U|0,i=e!==tt;i&&(tt=e);const r=O>0?(t-O)/(1-O):t,n=r>0?s(r):0;for(let t=0;t=j[t]?tA.editor?A.editor.addAnimation(t,e):new Be(t,e,null,0,!1).init(),t.clamp=mi,t.cleanInlineStyles=Mt,t.createAnimatable=(t,e)=>new ze(t,e),t.createDraggable=(t,e)=>new ns(t,e),t.createDrawable=Ni,t.createLayout=(t,e)=>new ei(t,e),t.createMotionPath=ki,t.createScope=t=>new ls(t),t.createSeededRandom=_i,t.createSpring=qe,t.createTimeline=Oe,t.createTimer=t=>new ne(t,null,0).init(),t.cubicBezier=bs,t.damp=yi,t.degToRad=ci,t.eases=ve,t.easings=ws,t.engine=Yt,t.forEachChildren=yt,t.get=Qe,t.globals=A,t.irregular=xs,t.keepTime=as,t.lerp=gi,t.linear=Ts,t.mapRange=di,t.morphTo=Di,t.onScroll=(t={})=>new ys(t),t.padEnd=li,t.padStart=ai,t.radToDeg=ui,t.random=vi,t.randomPick=Ti,t.remove=Ke,t.removeChild=vt,t.round=fi,t.roundPad=oi,t.scrambleText=rr,t.scrollContainers=ds,t.set=Je,t.shuffle=xi,t.snap=pi,t.split=tr,t.splitText=Ki,t.spring=Ue,t.stagger=wi,t.steps=_s,t.svg=Ai,t.sync=os,t.text=nr,t.utils=Si,t.waapi=Ls,t.wrap=hi}); diff --git a/dist/modules/animatable/animatable.cjs b/dist/modules/animatable/animatable.cjs index 17000d7d8..969ec4f6b 100644 --- a/dist/modules/animatable/animatable.cjs +++ b/dist/modules/animatable/animatable.cjs @@ -1,8 +1,8 @@ /** * Anime.js - animatable - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/animatable/animatable.js b/dist/modules/animatable/animatable.js index be27f8bfd..2966f0897 100644 --- a/dist/modules/animatable/animatable.js +++ b/dist/modules/animatable/animatable.js @@ -1,13 +1,13 @@ /** * Anime.js - animatable - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { compositionTypes, noop } from '../core/consts.js'; import { scope } from '../core/globals.js'; -import { isUnd, isKey, stringStartsWith, isObj, mergeObjects, forEachChildren, isStr, isArr } from '../core/helpers.js'; +import { isUnd, isKey, stringStartsWith, isObj, mergeObjects, forEachChildren, isArr, isStr } from '../core/helpers.js'; import { JSAnimation } from '../animation/animation.js'; import { parseEase } from '../easings/eases/parser.js'; diff --git a/dist/modules/animatable/index.cjs b/dist/modules/animatable/index.cjs index 3eb5c82a6..45cff6a3a 100644 --- a/dist/modules/animatable/index.cjs +++ b/dist/modules/animatable/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - animatable - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/animatable/index.js b/dist/modules/animatable/index.js index b52f7bef7..581dc999e 100644 --- a/dist/modules/animatable/index.js +++ b/dist/modules/animatable/index.js @@ -1,8 +1,8 @@ /** * Anime.js - animatable - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { Animatable, createAnimatable } from './animatable.js'; diff --git a/dist/modules/animation/additive.cjs b/dist/modules/animation/additive.cjs index 7f2e0c7da..1c798554a 100644 --- a/dist/modules/animation/additive.cjs +++ b/dist/modules/animation/additive.cjs @@ -1,8 +1,8 @@ /** * Anime.js - animation - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/animation/additive.js b/dist/modules/animation/additive.js index 206c86cfe..1161c3a0f 100644 --- a/dist/modules/animation/additive.js +++ b/dist/modules/animation/additive.js @@ -1,8 +1,8 @@ /** * Anime.js - animation - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { noop, minValue, valueTypes, tickModes } from '../core/consts.js'; diff --git a/dist/modules/animation/animation.cjs b/dist/modules/animation/animation.cjs index e1ef0c3e4..a8ba15265 100644 --- a/dist/modules/animation/animation.cjs +++ b/dist/modules/animation/animation.cjs @@ -1,8 +1,8 @@ /** * Anime.js - animation - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -54,12 +54,14 @@ const fromTargetObject = values.createDecomposedValueTargetObject(); const toTargetObject = values.createDecomposedValueTargetObject(); const inlineStylesStore = {}; const toFunctionStore = { func: null }; +const fromFunctionStore = { func: null }; const keyframesTargetArray = [null]; const fastSetValuesArray = [null, null]; /** @type {TweenKeyValue} */ const keyObjectTarget = { to: null }; let tweenId = 0; +let JSAnimationId = 0; let keyframes; /** @type {TweenParamsOptions & TweenValues} */ let key; @@ -160,7 +162,7 @@ class JSAnimation extends timer.Timer { * @param {Number} [parentPosition] * @param {Boolean} [fastSet=false] * @param {Number} [index=0] - * @param {Number} [length=0] + * @param {TargetsArray} [allTargets] */ constructor( targets$1, @@ -169,11 +171,13 @@ class JSAnimation extends timer.Timer { parentPosition, fastSet = false, index = 0, - length = 0 + allTargets ) { super(/** @type {TimerParams & AnimationParams} */(parameters), parent, parentPosition); + ++JSAnimationId; + const parsedTargets = targets.registerTargets(targets$1); const targetsLength = parsedTargets.length; @@ -183,6 +187,7 @@ class JSAnimation extends timer.Timer { const params = /** @type {AnimationParams} */(kfParams ? helpers.mergeObjects(generateKeyframes(/** @type {DurationKeyframes} */(kfParams), parameters), parameters) : parameters); const { + id, delay, duration, ease, @@ -193,11 +198,12 @@ class JSAnimation extends timer.Timer { } = params; const animDefaults = parent ? parent.defaults : globals.globals.defaults; - const animaPlaybackEase = values.setValue(playbackEase, animDefaults.playbackEase); - const animEase = animaPlaybackEase ? parser.parseEase(animaPlaybackEase) : null; - const hasSpring = !helpers.isUnd(ease) && !helpers.isUnd(/** @type {Spring} */(ease).ease); - const tEasing = hasSpring ? /** @type {Spring} */(ease).ease : values.setValue(ease, animEase ? 'linear' : animDefaults.ease); - const tDuration = hasSpring ? /** @type {Spring} */(ease).settlingDuration : values.setValue(duration, animDefaults.duration); + const animEase = values.setValue(ease, animDefaults.ease); + const animPlaybackEase = values.setValue(playbackEase, animDefaults.playbackEase); + const parsedAnimPlaybackEase = animPlaybackEase ? parser.parseEase(animPlaybackEase) : null; + const hasSpring = !helpers.isUnd(/** @type {Spring} */(animEase).ease); + const tEasing = hasSpring ? /** @type {Spring} */(animEase).ease : values.setValue(ease, parsedAnimPlaybackEase ? 'linear' : animDefaults.ease); + const tDuration = hasSpring ? /** @type {Spring} */(animEase).settlingDuration : values.setValue(duration, animDefaults.duration); const tDelay = values.setValue(delay, animDefaults.delay); const tModifier = modifier || animDefaults.modifier; // If no composition is defined and the targets length is high (>= 1000) set the composition to 'none' (0) for faster tween creation @@ -205,7 +211,7 @@ class JSAnimation extends timer.Timer { // const absoluteOffsetTime = this._offset; const absoluteOffsetTime = this._offset + (parent ? parent._offset : 0); // This allows targeting the current animation in the spring onComplete callback - if (hasSpring) /** @type {Spring} */(ease).parent = this; + if (hasSpring) /** @type {Spring} */(animEase).parent = this; let iterationDuration = NaN; let iterationDelay = NaN; @@ -216,7 +222,7 @@ class JSAnimation extends timer.Timer { const target = parsedTargets[targetIndex]; const ti = index || targetIndex; - const tl = length || targetsLength; + const tl = allTargets || parsedTargets; let lastTransformGroupIndex = NaN; let lastTransformGroupLength = NaN; @@ -290,8 +296,16 @@ class JSAnimation extends timer.Timer { } toFunctionStore.func = null; + fromFunctionStore.func = null; - const computedToValue = values.getFunctionValue(key.to, target, ti, tl, toFunctionStore); + const computedComposition = values.getFunctionValue(values.setValue(key.composition, tComposition), target, ti, tl, null, null); + const tweenComposition = helpers.isNum(computedComposition) ? computedComposition : consts.compositionTypes[computedComposition]; + if (!siblings && tweenComposition !== consts.compositionTypes.none) siblings = composition.getTweenSiblings(target, propName); + // Timelines pass the last sibling tween if it belongs to the same timeline + // Standalone animations only pass prevTween when the property has multiple keyframes + const tailTween = siblings ? siblings._tail : null; + const prevSiblingTween = parent && tailTween && tailTween.parent.parent === parent ? tailTween : prevTween; + const computedToValue = values.getFunctionValue(key.to, target, ti, tl, toFunctionStore, prevSiblingTween); let tweenToValue; // Allows function based values to return an object syntax value ({to: v}) @@ -301,17 +315,18 @@ class JSAnimation extends timer.Timer { } else { tweenToValue = computedToValue; } - const tweenFromValue = values.getFunctionValue(key.from, target, ti, tl); - const keyEasing = key.ease; + const tweenFromValue = values.getFunctionValue(key.from, target, ti, tl, null, prevSiblingTween); + const easeToParse = key.ease || tEasing; + + const easeFunctionResult = values.getFunctionValue(easeToParse, target, ti, tl, null, prevSiblingTween); + const keyEasing = helpers.isFnc(easeFunctionResult) || helpers.isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; + const hasSpring = !helpers.isUnd(keyEasing) && !helpers.isUnd(/** @type {Spring} */(keyEasing).ease); - // Easing are treated differently and don't accept function based value to prevent having to pass a function wrapper that returns an other function all the time - const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing || tEasing; + const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; // Calculate default individual keyframe duration by dividing the tl of keyframes - const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : values.getFunctionValue(values.setValue(key.duration, (l > 1 ? values.getFunctionValue(tDuration, target, ti, tl) / l : tDuration)), target, ti, tl); + const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : values.getFunctionValue(values.setValue(key.duration, (l > 1 ? values.getFunctionValue(tDuration, target, ti, tl, null, prevSiblingTween) / l : tDuration)), target, ti, tl, null, prevSiblingTween); // Default delay value should only be applied to the first tween - const tweenDelay = values.getFunctionValue(values.setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl); - const computedComposition = values.getFunctionValue(values.setValue(key.composition, tComposition), target, ti, tl); - const tweenComposition = helpers.isNum(computedComposition) ? computedComposition : consts.compositionTypes[computedComposition]; + const tweenDelay = values.getFunctionValue(values.setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl, null, prevSiblingTween); // Modifiers are treated differently and don't accept function based value to prevent having to pass a function wrapper const tweenModifier = key.modifier || tModifier; const hasFromvalue = !helpers.isUnd(tweenFromValue); @@ -328,7 +343,6 @@ class JSAnimation extends timer.Timer { let prevSibling = prevTween; if (tweenComposition !== consts.compositionTypes.none) { - if (!siblings) siblings = composition.getTweenSiblings(target, propName); let nextSibling = siblings._head; // Iterate trough all the next siblings until we find a sibling with an equal or inferior start time while (nextSibling && !nextSibling._isOverridden && nextSibling._absoluteStartTime <= absoluteStartTime) { @@ -347,8 +361,10 @@ class JSAnimation extends timer.Timer { // Decompose values if (isFromToValue) { - values.decomposeRawValue(isFromToArray ? values.getFunctionValue(tweenToValue[0], target, ti, tl) : tweenFromValue, fromTargetObject); - values.decomposeRawValue(isFromToArray ? values.getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore) : tweenToValue, toTargetObject); + values.decomposeRawValue(isFromToArray ? values.getFunctionValue(tweenToValue[0], target, ti, tl, fromFunctionStore, prevSiblingTween) : tweenFromValue, fromTargetObject); + values.decomposeRawValue(isFromToArray ? values.getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore, prevSiblingTween) : tweenToValue, toTargetObject); + // Needed to force an inline style registration + const originalValue = values.getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore); if (fromTargetObject.t === consts.valueTypes.NUMBER) { if (prevSibling) { if (prevSibling._valueType === consts.valueTypes.UNIT) { @@ -357,7 +373,7 @@ class JSAnimation extends timer.Timer { } } else { values.decomposeRawValue( - values.getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore), + originalValue, values.decomposedOriginalValue ); if (values.decomposedOriginalValue.t === consts.valueTypes.UNIT) { @@ -462,7 +478,8 @@ class JSAnimation extends timer.Timer { property: propName, target: target, _value: null, - _func: toFunctionStore.func, + _toFunc: toFunctionStore.func, + _fromFunc: fromFunctionStore.func, _ease: parser.parseEase(tweenEasing), _fromNumbers: helpers.cloneArray(fromTargetObject.d), _toNumbers: helpers.cloneArray(toTargetObject.d), @@ -499,6 +516,18 @@ class JSAnimation extends timer.Timer { composition.composeTween(tween, siblings); } + // Pre-compute the tween end value for function-based value chaining (ie morphTo / scrambleText in keyframe arrays and timelines) + const vt = tween._valueType; + if (vt === consts.valueTypes.COMPLEX) { + tween._value = values.composeComplexValue(tween, 1, -1); + } else if (vt === consts.valueTypes.COLOR) { + tween._value = values.composeColorValue(tween, 1, -1); + } else if (vt === consts.valueTypes.UNIT) { + tween._value = `${tweenModifier(tween._toNumber)}${tween._unit}`; + } else { + tween._value = tweenModifier(tween._toNumber); + } + if (isNaN(firstTweenChangeStartTime)) { firstTweenChangeStartTime = tween._startTime; } @@ -576,12 +605,14 @@ class JSAnimation extends timer.Timer { } /** @type {TargetsArray} */ this.targets = parsedTargets; + /** @type {String|Number} */ + this.id = !helpers.isUnd(id) ? id : JSAnimationId; /** @type {Number} */ this.duration = iterationDuration === consts.minValue ? consts.minValue : helpers.clampInfinity(((iterationDuration + this._loopDelay) * this.iterationCount) - this._loopDelay) || consts.minValue; /** @type {Callback} */ this.onRender = onRender || animDefaults.onRender; /** @type {EasingFunction} */ - this._ease = animEase; + this._ease = parsedAnimPlaybackEase; /** @type {Number} */ this._delay = iterationDelay; // NOTE: I'm keeping delay values separated from offsets in timelines because delays can override previous tweens and it could be confusing to debug a timeline with overridden tweens and no associated visible delays. @@ -618,18 +649,29 @@ class JSAnimation extends timer.Timer { */ refresh() { helpers.forEachChildren(this, (/** @type {Tween} */tween) => { - const tweenFunc = tween._func; - if (tweenFunc) { - const ogValue = values.getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType); - values.decomposeRawValue(ogValue, values.decomposedOriginalValue); - // TODO: Check for from / to Array based values here, - values.decomposeRawValue(tweenFunc(), toTargetObject); - tween._fromNumbers = helpers.cloneArray(values.decomposedOriginalValue.d); - tween._fromNumber = values.decomposedOriginalValue.n; - tween._toNumbers = helpers.cloneArray(toTargetObject.d); - tween._strings = helpers.cloneArray(toTargetObject.s); - // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 - tween._toNumber = toTargetObject.o ? values.getRelativeValue(values.decomposedOriginalValue.n, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + const toFunc = tween._toFunc; + const fromFunc = tween._fromFunc; + if (toFunc || fromFunc) { + if (fromFunc) { + values.decomposeRawValue(fromFunc(), fromTargetObject); + if (fromTargetObject.u !== tween._unit && tween.target[consts.isDomSymbol]) { + units.convertValueUnit(/** @type {DOMTarget} */(tween.target), fromTargetObject, tween._unit, true); + } + tween._fromNumbers = helpers.cloneArray(fromTargetObject.d); + tween._fromNumber = fromTargetObject.n; + } else if (toFunc) { + // When only toFunc exists, get from value from target + values.decomposeRawValue(values.getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType), values.decomposedOriginalValue); + tween._fromNumbers = helpers.cloneArray(values.decomposedOriginalValue.d); + tween._fromNumber = values.decomposedOriginalValue.n; + } + if (toFunc) { + values.decomposeRawValue(toFunc(), toTargetObject); + tween._toNumbers = helpers.cloneArray(toTargetObject.d); + tween._strings = helpers.cloneArray(toTargetObject.s); + // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 + tween._toNumber = toTargetObject.o ? values.getRelativeValue(tween._fromNumber, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + } } }); // This forces setter animations to render once @@ -643,7 +685,7 @@ class JSAnimation extends timer.Timer { */ revert() { super.revert(); - return styles.cleanInlineStyles(this); + return styles.revertValues(this); } /** @@ -665,7 +707,13 @@ class JSAnimation extends timer.Timer { * @param {AnimationParams} parameters * @return {JSAnimation} */ -const animate = (targets, parameters) => new JSAnimation(targets, parameters, null, 0, false).init(); +const animate = (targets, parameters) => { + if (globals.globals.editor) { + return globals.globals.editor.addAnimation(targets, parameters); + } else { + return new JSAnimation(targets, parameters, null, 0, false).init(); + } +}; exports.JSAnimation = JSAnimation; exports.animate = animate; diff --git a/dist/modules/animation/animation.d.ts b/dist/modules/animation/animation.d.ts index 9d560e414..9434ca72c 100644 --- a/dist/modules/animation/animation.d.ts +++ b/dist/modules/animation/animation.d.ts @@ -6,9 +6,9 @@ export class JSAnimation extends Timer { * @param {Number} [parentPosition] * @param {Boolean} [fastSet=false] * @param {Number} [index=0] - * @param {Number} [length=0] + * @param {TargetsArray} [allTargets] */ - constructor(targets: TargetsParam, parameters: AnimationParams, parent?: Timeline, parentPosition?: number, fastSet?: boolean, index?: number, length?: number); + constructor(targets: TargetsParam, parameters: AnimationParams, parent?: Timeline, parentPosition?: number, fastSet?: boolean, index?: number, allTargets?: TargetsArray); /** @type {TargetsArray} */ targets: TargetsArray; /** @type {Callback} */ diff --git a/dist/modules/animation/animation.js b/dist/modules/animation/animation.js index e20109c0c..58e083857 100644 --- a/dist/modules/animation/animation.js +++ b/dist/modules/animation/animation.js @@ -1,16 +1,16 @@ /** * Anime.js - animation - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -import { K, compositionTypes, valueTypes, minValue, tweenTypes } from '../core/consts.js'; -import { mergeObjects, isUnd, isKey, isObj, round, cloneArray, isNil, addChild, forEachChildren, clampInfinity, normalizeTime, isArr, isNum } from '../core/helpers.js'; +import { K, compositionTypes, valueTypes, minValue, isDomSymbol, tweenTypes } from '../core/consts.js'; +import { mergeObjects, isUnd, isKey, isObj, cloneArray, isNil, round, addChild, forEachChildren, clampInfinity, normalizeTime, isArr, isNum, isFnc, isStr } from '../core/helpers.js'; import { globals } from '../core/globals.js'; import { registerTargets } from '../core/targets.js'; -import { setValue, getTweenType, getFunctionValue, decomposeRawValue, createDecomposedValueTargetObject, getOriginalAnimatableValue, decomposedOriginalValue, getRelativeValue, decomposeTweenValue } from '../core/values.js'; -import { sanitizePropertyName, cleanInlineStyles } from '../core/styles.js'; +import { setValue, getTweenType, getFunctionValue, decomposeRawValue, getOriginalAnimatableValue, createDecomposedValueTargetObject, decomposedOriginalValue, getRelativeValue, composeComplexValue, composeColorValue, decomposeTweenValue } from '../core/values.js'; +import { sanitizePropertyName, revertValues } from '../core/styles.js'; import { convertValueUnit } from '../core/units.js'; import { parseEase } from '../easings/eases/parser.js'; import { Timer } from '../timer/timer.js'; @@ -52,12 +52,14 @@ const fromTargetObject = createDecomposedValueTargetObject(); const toTargetObject = createDecomposedValueTargetObject(); const inlineStylesStore = {}; const toFunctionStore = { func: null }; +const fromFunctionStore = { func: null }; const keyframesTargetArray = [null]; const fastSetValuesArray = [null, null]; /** @type {TweenKeyValue} */ const keyObjectTarget = { to: null }; let tweenId = 0; +let JSAnimationId = 0; let keyframes; /** @type {TweenParamsOptions & TweenValues} */ let key; @@ -158,7 +160,7 @@ class JSAnimation extends Timer { * @param {Number} [parentPosition] * @param {Boolean} [fastSet=false] * @param {Number} [index=0] - * @param {Number} [length=0] + * @param {TargetsArray} [allTargets] */ constructor( targets, @@ -167,11 +169,13 @@ class JSAnimation extends Timer { parentPosition, fastSet = false, index = 0, - length = 0 + allTargets ) { super(/** @type {TimerParams & AnimationParams} */(parameters), parent, parentPosition); + ++JSAnimationId; + const parsedTargets = registerTargets(targets); const targetsLength = parsedTargets.length; @@ -181,6 +185,7 @@ class JSAnimation extends Timer { const params = /** @type {AnimationParams} */(kfParams ? mergeObjects(generateKeyframes(/** @type {DurationKeyframes} */(kfParams), parameters), parameters) : parameters); const { + id, delay, duration, ease, @@ -191,11 +196,12 @@ class JSAnimation extends Timer { } = params; const animDefaults = parent ? parent.defaults : globals.defaults; - const animaPlaybackEase = setValue(playbackEase, animDefaults.playbackEase); - const animEase = animaPlaybackEase ? parseEase(animaPlaybackEase) : null; - const hasSpring = !isUnd(ease) && !isUnd(/** @type {Spring} */(ease).ease); - const tEasing = hasSpring ? /** @type {Spring} */(ease).ease : setValue(ease, animEase ? 'linear' : animDefaults.ease); - const tDuration = hasSpring ? /** @type {Spring} */(ease).settlingDuration : setValue(duration, animDefaults.duration); + const animEase = setValue(ease, animDefaults.ease); + const animPlaybackEase = setValue(playbackEase, animDefaults.playbackEase); + const parsedAnimPlaybackEase = animPlaybackEase ? parseEase(animPlaybackEase) : null; + const hasSpring = !isUnd(/** @type {Spring} */(animEase).ease); + const tEasing = hasSpring ? /** @type {Spring} */(animEase).ease : setValue(ease, parsedAnimPlaybackEase ? 'linear' : animDefaults.ease); + const tDuration = hasSpring ? /** @type {Spring} */(animEase).settlingDuration : setValue(duration, animDefaults.duration); const tDelay = setValue(delay, animDefaults.delay); const tModifier = modifier || animDefaults.modifier; // If no composition is defined and the targets length is high (>= 1000) set the composition to 'none' (0) for faster tween creation @@ -203,7 +209,7 @@ class JSAnimation extends Timer { // const absoluteOffsetTime = this._offset; const absoluteOffsetTime = this._offset + (parent ? parent._offset : 0); // This allows targeting the current animation in the spring onComplete callback - if (hasSpring) /** @type {Spring} */(ease).parent = this; + if (hasSpring) /** @type {Spring} */(animEase).parent = this; let iterationDuration = NaN; let iterationDelay = NaN; @@ -214,7 +220,7 @@ class JSAnimation extends Timer { const target = parsedTargets[targetIndex]; const ti = index || targetIndex; - const tl = length || targetsLength; + const tl = allTargets || parsedTargets; let lastTransformGroupIndex = NaN; let lastTransformGroupLength = NaN; @@ -288,8 +294,16 @@ class JSAnimation extends Timer { } toFunctionStore.func = null; + fromFunctionStore.func = null; - const computedToValue = getFunctionValue(key.to, target, ti, tl, toFunctionStore); + const computedComposition = getFunctionValue(setValue(key.composition, tComposition), target, ti, tl, null, null); + const tweenComposition = isNum(computedComposition) ? computedComposition : compositionTypes[computedComposition]; + if (!siblings && tweenComposition !== compositionTypes.none) siblings = getTweenSiblings(target, propName); + // Timelines pass the last sibling tween if it belongs to the same timeline + // Standalone animations only pass prevTween when the property has multiple keyframes + const tailTween = siblings ? siblings._tail : null; + const prevSiblingTween = parent && tailTween && tailTween.parent.parent === parent ? tailTween : prevTween; + const computedToValue = getFunctionValue(key.to, target, ti, tl, toFunctionStore, prevSiblingTween); let tweenToValue; // Allows function based values to return an object syntax value ({to: v}) @@ -299,17 +313,18 @@ class JSAnimation extends Timer { } else { tweenToValue = computedToValue; } - const tweenFromValue = getFunctionValue(key.from, target, ti, tl); - const keyEasing = key.ease; + const tweenFromValue = getFunctionValue(key.from, target, ti, tl, null, prevSiblingTween); + const easeToParse = key.ease || tEasing; + + const easeFunctionResult = getFunctionValue(easeToParse, target, ti, tl, null, prevSiblingTween); + const keyEasing = isFnc(easeFunctionResult) || isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; + const hasSpring = !isUnd(keyEasing) && !isUnd(/** @type {Spring} */(keyEasing).ease); - // Easing are treated differently and don't accept function based value to prevent having to pass a function wrapper that returns an other function all the time - const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing || tEasing; + const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; // Calculate default individual keyframe duration by dividing the tl of keyframes - const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(setValue(key.duration, (l > 1 ? getFunctionValue(tDuration, target, ti, tl) / l : tDuration)), target, ti, tl); + const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(setValue(key.duration, (l > 1 ? getFunctionValue(tDuration, target, ti, tl, null, prevSiblingTween) / l : tDuration)), target, ti, tl, null, prevSiblingTween); // Default delay value should only be applied to the first tween - const tweenDelay = getFunctionValue(setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl); - const computedComposition = getFunctionValue(setValue(key.composition, tComposition), target, ti, tl); - const tweenComposition = isNum(computedComposition) ? computedComposition : compositionTypes[computedComposition]; + const tweenDelay = getFunctionValue(setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl, null, prevSiblingTween); // Modifiers are treated differently and don't accept function based value to prevent having to pass a function wrapper const tweenModifier = key.modifier || tModifier; const hasFromvalue = !isUnd(tweenFromValue); @@ -326,7 +341,6 @@ class JSAnimation extends Timer { let prevSibling = prevTween; if (tweenComposition !== compositionTypes.none) { - if (!siblings) siblings = getTweenSiblings(target, propName); let nextSibling = siblings._head; // Iterate trough all the next siblings until we find a sibling with an equal or inferior start time while (nextSibling && !nextSibling._isOverridden && nextSibling._absoluteStartTime <= absoluteStartTime) { @@ -345,8 +359,10 @@ class JSAnimation extends Timer { // Decompose values if (isFromToValue) { - decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[0], target, ti, tl) : tweenFromValue, fromTargetObject); - decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore) : tweenToValue, toTargetObject); + decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[0], target, ti, tl, fromFunctionStore, prevSiblingTween) : tweenFromValue, fromTargetObject); + decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore, prevSiblingTween) : tweenToValue, toTargetObject); + // Needed to force an inline style registration + const originalValue = getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore); if (fromTargetObject.t === valueTypes.NUMBER) { if (prevSibling) { if (prevSibling._valueType === valueTypes.UNIT) { @@ -355,7 +371,7 @@ class JSAnimation extends Timer { } } else { decomposeRawValue( - getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore), + originalValue, decomposedOriginalValue ); if (decomposedOriginalValue.t === valueTypes.UNIT) { @@ -460,7 +476,8 @@ class JSAnimation extends Timer { property: propName, target: target, _value: null, - _func: toFunctionStore.func, + _toFunc: toFunctionStore.func, + _fromFunc: fromFunctionStore.func, _ease: parseEase(tweenEasing), _fromNumbers: cloneArray(fromTargetObject.d), _toNumbers: cloneArray(toTargetObject.d), @@ -497,6 +514,18 @@ class JSAnimation extends Timer { composeTween(tween, siblings); } + // Pre-compute the tween end value for function-based value chaining (ie morphTo / scrambleText in keyframe arrays and timelines) + const vt = tween._valueType; + if (vt === valueTypes.COMPLEX) { + tween._value = composeComplexValue(tween, 1, -1); + } else if (vt === valueTypes.COLOR) { + tween._value = composeColorValue(tween, 1, -1); + } else if (vt === valueTypes.UNIT) { + tween._value = `${tweenModifier(tween._toNumber)}${tween._unit}`; + } else { + tween._value = tweenModifier(tween._toNumber); + } + if (isNaN(firstTweenChangeStartTime)) { firstTweenChangeStartTime = tween._startTime; } @@ -574,12 +603,14 @@ class JSAnimation extends Timer { } /** @type {TargetsArray} */ this.targets = parsedTargets; + /** @type {String|Number} */ + this.id = !isUnd(id) ? id : JSAnimationId; /** @type {Number} */ this.duration = iterationDuration === minValue ? minValue : clampInfinity(((iterationDuration + this._loopDelay) * this.iterationCount) - this._loopDelay) || minValue; /** @type {Callback} */ this.onRender = onRender || animDefaults.onRender; /** @type {EasingFunction} */ - this._ease = animEase; + this._ease = parsedAnimPlaybackEase; /** @type {Number} */ this._delay = iterationDelay; // NOTE: I'm keeping delay values separated from offsets in timelines because delays can override previous tweens and it could be confusing to debug a timeline with overridden tweens and no associated visible delays. @@ -616,18 +647,29 @@ class JSAnimation extends Timer { */ refresh() { forEachChildren(this, (/** @type {Tween} */tween) => { - const tweenFunc = tween._func; - if (tweenFunc) { - const ogValue = getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType); - decomposeRawValue(ogValue, decomposedOriginalValue); - // TODO: Check for from / to Array based values here, - decomposeRawValue(tweenFunc(), toTargetObject); - tween._fromNumbers = cloneArray(decomposedOriginalValue.d); - tween._fromNumber = decomposedOriginalValue.n; - tween._toNumbers = cloneArray(toTargetObject.d); - tween._strings = cloneArray(toTargetObject.s); - // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 - tween._toNumber = toTargetObject.o ? getRelativeValue(decomposedOriginalValue.n, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + const toFunc = tween._toFunc; + const fromFunc = tween._fromFunc; + if (toFunc || fromFunc) { + if (fromFunc) { + decomposeRawValue(fromFunc(), fromTargetObject); + if (fromTargetObject.u !== tween._unit && tween.target[isDomSymbol]) { + convertValueUnit(/** @type {DOMTarget} */(tween.target), fromTargetObject, tween._unit, true); + } + tween._fromNumbers = cloneArray(fromTargetObject.d); + tween._fromNumber = fromTargetObject.n; + } else if (toFunc) { + // When only toFunc exists, get from value from target + decomposeRawValue(getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType), decomposedOriginalValue); + tween._fromNumbers = cloneArray(decomposedOriginalValue.d); + tween._fromNumber = decomposedOriginalValue.n; + } + if (toFunc) { + decomposeRawValue(toFunc(), toTargetObject); + tween._toNumbers = cloneArray(toTargetObject.d); + tween._strings = cloneArray(toTargetObject.s); + // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 + tween._toNumber = toTargetObject.o ? getRelativeValue(tween._fromNumber, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + } } }); // This forces setter animations to render once @@ -641,7 +683,7 @@ class JSAnimation extends Timer { */ revert() { super.revert(); - return cleanInlineStyles(this); + return revertValues(this); } /** @@ -663,6 +705,12 @@ class JSAnimation extends Timer { * @param {AnimationParams} parameters * @return {JSAnimation} */ -const animate = (targets, parameters) => new JSAnimation(targets, parameters, null, 0, false).init(); +const animate = (targets, parameters) => { + if (globals.editor) { + return globals.editor.addAnimation(targets, parameters); + } else { + return new JSAnimation(targets, parameters, null, 0, false).init(); + } +}; export { JSAnimation, animate }; diff --git a/dist/modules/animation/composition.cjs b/dist/modules/animation/composition.cjs index 9db81bf9f..ffd139647 100644 --- a/dist/modules/animation/composition.cjs +++ b/dist/modules/animation/composition.cjs @@ -1,8 +1,8 @@ /** * Anime.js - animation - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/animation/composition.js b/dist/modules/animation/composition.js index cf31a1b6b..5670afd11 100644 --- a/dist/modules/animation/composition.js +++ b/dist/modules/animation/composition.js @@ -1,15 +1,15 @@ /** * Anime.js - animation - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -import { minValue, compositionTypes, tweenTypes } from '../core/consts.js'; +import { compositionTypes, minValue, tweenTypes } from '../core/consts.js'; import { forEachChildren, removeChild, isUnd, addChild, round, cloneArray } from '../core/helpers.js'; import { sanitizePropertyName } from '../core/styles.js'; import { engine } from '../engine/engine.js'; -import { addAdditiveAnimation, additive } from './additive.js'; +import { additive, addAdditiveAnimation } from './additive.js'; /** * @import { diff --git a/dist/modules/animation/index.cjs b/dist/modules/animation/index.cjs index 1b49bf47d..5b8a20d52 100644 --- a/dist/modules/animation/index.cjs +++ b/dist/modules/animation/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - animation - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/animation/index.js b/dist/modules/animation/index.js index 851425052..f66ce1efd 100644 --- a/dist/modules/animation/index.js +++ b/dist/modules/animation/index.js @@ -1,8 +1,8 @@ /** * Anime.js - animation - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { JSAnimation, animate } from './animation.js'; diff --git a/dist/modules/core/clock.cjs b/dist/modules/core/clock.cjs index 01de20884..3b40498ad 100644 --- a/dist/modules/core/clock.cjs +++ b/dist/modules/core/clock.cjs @@ -1,14 +1,14 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; var consts = require('./consts.cjs'); -var helpers = require('./helpers.cjs'); +var globals = require('./globals.cjs'); /** * @import { @@ -30,7 +30,7 @@ class Clock { /** @type {Number} */ this._currentTime = initTime; /** @type {Number} */ - this._elapsedTime = initTime; + this._lastTickTime = initTime; /** @type {Number} */ this._startTime = initTime; /** @type {Number} */ @@ -38,7 +38,7 @@ class Clock { /** @type {Number} */ this._scheduledTime = 0; /** @type {Number} */ - this._frameDuration = helpers.round(consts.K / consts.maxFps, 0); + this._frameDuration = consts.K / consts.maxFps; /** @type {Number} */ this._fps = consts.maxFps; /** @type {Number} */ @@ -59,7 +59,8 @@ class Clock { const previousFrameDuration = this._frameDuration; const fr = +frameRate; const fps = fr < consts.minValue ? consts.minValue : fr; - const frameDuration = helpers.round(consts.K / fps, 0); + const frameDuration = consts.K / fps; + if (fps > globals.defaults.frameRate) globals.defaults.frameRate = fps; this._fps = fps; this._frameDuration = frameDuration; this._scheduledTime += frameDuration - previousFrameDuration; @@ -80,14 +81,13 @@ class Clock { */ requestTick(time) { const scheduledTime = this._scheduledTime; - const elapsedTime = this._elapsedTime; - this._elapsedTime += (time - elapsedTime); - // If the elapsed time is lower than the scheduled time + this._lastTickTime = time; + // If the current time is lower than the scheduled time // this means not enough time has passed to hit one frameDuration // so skip that frame - if (elapsedTime < scheduledTime) return consts.tickModes.NONE; + if (time < scheduledTime) return consts.tickModes.NONE; const frameDuration = this._frameDuration; - const frameDelta = elapsedTime - scheduledTime; + const frameDelta = time - scheduledTime; // Ensures that _scheduledTime progresses in steps of at least 1 frameDuration. // Skips ahead if the actual elapsed time is higher. this._scheduledTime += frameDelta < frameDuration ? frameDuration : frameDelta; diff --git a/dist/modules/core/clock.d.ts b/dist/modules/core/clock.d.ts index d757d6af2..7669560c7 100644 --- a/dist/modules/core/clock.d.ts +++ b/dist/modules/core/clock.d.ts @@ -12,7 +12,7 @@ export class Clock { /** @type {Number} */ _currentTime: number; /** @type {Number} */ - _elapsedTime: number; + _lastTickTime: number; /** @type {Number} */ _startTime: number; /** @type {Number} */ diff --git a/dist/modules/core/clock.js b/dist/modules/core/clock.js index f25ca29c2..cf28d2ba4 100644 --- a/dist/modules/core/clock.js +++ b/dist/modules/core/clock.js @@ -1,12 +1,12 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { K, maxFps, minValue, tickModes } from './consts.js'; -import { round } from './helpers.js'; +import { defaults } from './globals.js'; /** * @import { @@ -28,7 +28,7 @@ class Clock { /** @type {Number} */ this._currentTime = initTime; /** @type {Number} */ - this._elapsedTime = initTime; + this._lastTickTime = initTime; /** @type {Number} */ this._startTime = initTime; /** @type {Number} */ @@ -36,7 +36,7 @@ class Clock { /** @type {Number} */ this._scheduledTime = 0; /** @type {Number} */ - this._frameDuration = round(K / maxFps, 0); + this._frameDuration = K / maxFps; /** @type {Number} */ this._fps = maxFps; /** @type {Number} */ @@ -57,7 +57,8 @@ class Clock { const previousFrameDuration = this._frameDuration; const fr = +frameRate; const fps = fr < minValue ? minValue : fr; - const frameDuration = round(K / fps, 0); + const frameDuration = K / fps; + if (fps > defaults.frameRate) defaults.frameRate = fps; this._fps = fps; this._frameDuration = frameDuration; this._scheduledTime += frameDuration - previousFrameDuration; @@ -78,14 +79,13 @@ class Clock { */ requestTick(time) { const scheduledTime = this._scheduledTime; - const elapsedTime = this._elapsedTime; - this._elapsedTime += (time - elapsedTime); - // If the elapsed time is lower than the scheduled time + this._lastTickTime = time; + // If the current time is lower than the scheduled time // this means not enough time has passed to hit one frameDuration // so skip that frame - if (elapsedTime < scheduledTime) return tickModes.NONE; + if (time < scheduledTime) return tickModes.NONE; const frameDuration = this._frameDuration; - const frameDelta = elapsedTime - scheduledTime; + const frameDelta = time - scheduledTime; // Ensures that _scheduledTime progresses in steps of at least 1 frameDuration. // Skips ahead if the actual elapsed time is higher. this._scheduledTime += frameDelta < frameDuration ? frameDuration : frameDelta; diff --git a/dist/modules/core/colors.cjs b/dist/modules/core/colors.cjs index 508f86ebf..a4a1ccce0 100644 --- a/dist/modules/core/colors.cjs +++ b/dist/modules/core/colors.cjs @@ -1,8 +1,8 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/core/colors.js b/dist/modules/core/colors.js index abffa8546..ee392ccb4 100644 --- a/dist/modules/core/colors.js +++ b/dist/modules/core/colors.js @@ -1,8 +1,8 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { rgbExecRgx, rgbaExecRgx, hslExecRgx, hslaExecRgx } from './consts.js'; diff --git a/dist/modules/core/consts.cjs b/dist/modules/core/consts.cjs index 3f1840153..777a249ee 100644 --- a/dist/modules/core/consts.cjs +++ b/dist/modules/core/consts.cjs @@ -1,8 +1,8 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -12,8 +12,10 @@ // TODO: Do we need to check if we're running inside a worker ? const isBrowser = typeof window !== 'undefined'; -/** @type {Window & {AnimeJS: Array}|null} */ -const win = isBrowser ? /** @type {Window & {AnimeJS: Array}} */(/** @type {unknown} */(window)) : null; +/** @typedef {Window & {AnimeJS: Array}|null} AnimeJSWindow + +/** @type {AnimeJSWindow} */ +const win = isBrowser ? /** @type {AnimeJSWindow} */(/** @type {unknown} */(window)) : null; /** @type {Document|null} */ const doc = isBrowser ? document : null; @@ -57,7 +59,6 @@ const isRegisteredTargetSymbol = Symbol(); const isDomSymbol = Symbol(); const isSvgSymbol = Symbol(); const transformsSymbol = Symbol(); -const morphPointsSymbol = Symbol(); const proxyTargetSymbol = Symbol(); // Numbers @@ -65,7 +66,7 @@ const proxyTargetSymbol = Symbol(); const minValue = 1e-11; const maxValue = 1e12; const K = 1e3; -const maxFps = 120; +const maxFps = 240; // Strings @@ -81,6 +82,7 @@ const shortTransforms = /*#__PURE__*/ (() => { })(); const validTransforms = [ + 'perspective', 'translateX', 'translateY', 'translateZ', @@ -95,9 +97,6 @@ const validTransforms = [ 'skew', 'skewX', 'skewY', - 'matrix', - 'matrix3d', - 'perspective', ]; const transformsFragmentStrings = /*#__PURE__*/ validTransforms.reduce((a, v) => ({...a, [v]: v + '('}), {}); @@ -109,6 +108,7 @@ const noop = () => {}; // Regex +const validRgbHslRgx = /\)\s*[-.\d]/; const hexTestRgx = /(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i; const rgbExecRgx = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i; const rgbaExecRgx = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i; @@ -119,7 +119,6 @@ const digitWithExponentRgx = /[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi; // export const unitsExecRgx = /^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)+([a-z]+|%)$/i; const unitsExecRgx = /^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i; const lowerCaseRgx = /([a-z])([A-Z])/g; -const transformsExecRgx = /(\w+)(\([^)]+\)+)/g; // Match inline transforms with cacl() values, returns the value wrapped in () const relativeValuesExecRgx = /(\*=|\+=|-=)/; const cssVariableMatchRgx = /var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/; @@ -141,7 +140,6 @@ exports.lowerCaseRgx = lowerCaseRgx; exports.maxFps = maxFps; exports.maxValue = maxValue; exports.minValue = minValue; -exports.morphPointsSymbol = morphPointsSymbol; exports.noop = noop; exports.proxyTargetSymbol = proxyTargetSymbol; exports.relativeValuesExecRgx = relativeValuesExecRgx; @@ -149,11 +147,11 @@ exports.rgbExecRgx = rgbExecRgx; exports.rgbaExecRgx = rgbaExecRgx; exports.shortTransforms = shortTransforms; exports.tickModes = tickModes; -exports.transformsExecRgx = transformsExecRgx; exports.transformsFragmentStrings = transformsFragmentStrings; exports.transformsSymbol = transformsSymbol; exports.tweenTypes = tweenTypes; exports.unitsExecRgx = unitsExecRgx; +exports.validRgbHslRgx = validRgbHslRgx; exports.validTransforms = validTransforms; exports.valueTypes = valueTypes; exports.win = win; diff --git a/dist/modules/core/consts.d.ts b/dist/modules/core/consts.d.ts index aa4347f78..e4916e492 100644 --- a/dist/modules/core/consts.d.ts +++ b/dist/modules/core/consts.d.ts @@ -1,8 +1,8 @@ export const isBrowser: boolean; -/** @type {Window & {AnimeJS: Array}|null} */ -export const win: (Window & { - AnimeJS: any[]; -}) | null; +/** @typedef {Window & {AnimeJS: Array}|null} AnimeJSWindow + +/** @type {AnimeJSWindow} */ +export const win: AnimeJSWindow; /** @type {Document|null} */ export const doc: Document | null; export type tweenTypes = number; @@ -36,18 +36,18 @@ export const isRegisteredTargetSymbol: unique symbol; export const isDomSymbol: unique symbol; export const isSvgSymbol: unique symbol; export const transformsSymbol: unique symbol; -export const morphPointsSymbol: unique symbol; export const proxyTargetSymbol: unique symbol; export const minValue: 1e-11; export const maxValue: 1000000000000; export const K: 1000; -export const maxFps: 120; +export const maxFps: 240; export const emptyString: ""; export const cssVarPrefix: "var("; export const shortTransforms: Map; export const validTransforms: string[]; export const transformsFragmentStrings: {}; export function noop(): void; +export const validRgbHslRgx: RegExp; export const hexTestRgx: RegExp; export const rgbExecRgx: RegExp; export const rgbaExecRgx: RegExp; @@ -56,6 +56,11 @@ export const hslaExecRgx: RegExp; export const digitWithExponentRgx: RegExp; export const unitsExecRgx: RegExp; export const lowerCaseRgx: RegExp; -export const transformsExecRgx: RegExp; export const relativeValuesExecRgx: RegExp; export const cssVariableMatchRgx: RegExp; +/** + * /** + */ +export type AnimeJSWindow = (Window & { + AnimeJS: any[]; +}) | null; diff --git a/dist/modules/core/consts.js b/dist/modules/core/consts.js index e889247c9..2ef9810ae 100644 --- a/dist/modules/core/consts.js +++ b/dist/modules/core/consts.js @@ -1,8 +1,8 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ // Environments @@ -10,8 +10,10 @@ // TODO: Do we need to check if we're running inside a worker ? const isBrowser = typeof window !== 'undefined'; -/** @type {Window & {AnimeJS: Array}|null} */ -const win = isBrowser ? /** @type {Window & {AnimeJS: Array}} */(/** @type {unknown} */(window)) : null; +/** @typedef {Window & {AnimeJS: Array}|null} AnimeJSWindow + +/** @type {AnimeJSWindow} */ +const win = isBrowser ? /** @type {AnimeJSWindow} */(/** @type {unknown} */(window)) : null; /** @type {Document|null} */ const doc = isBrowser ? document : null; @@ -55,7 +57,6 @@ const isRegisteredTargetSymbol = Symbol(); const isDomSymbol = Symbol(); const isSvgSymbol = Symbol(); const transformsSymbol = Symbol(); -const morphPointsSymbol = Symbol(); const proxyTargetSymbol = Symbol(); // Numbers @@ -63,7 +64,7 @@ const proxyTargetSymbol = Symbol(); const minValue = 1e-11; const maxValue = 1e12; const K = 1e3; -const maxFps = 120; +const maxFps = 240; // Strings @@ -79,6 +80,7 @@ const shortTransforms = /*#__PURE__*/ (() => { })(); const validTransforms = [ + 'perspective', 'translateX', 'translateY', 'translateZ', @@ -93,9 +95,6 @@ const validTransforms = [ 'skew', 'skewX', 'skewY', - 'matrix', - 'matrix3d', - 'perspective', ]; const transformsFragmentStrings = /*#__PURE__*/ validTransforms.reduce((a, v) => ({...a, [v]: v + '('}), {}); @@ -107,6 +106,7 @@ const noop = () => {}; // Regex +const validRgbHslRgx = /\)\s*[-.\d]/; const hexTestRgx = /(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i; const rgbExecRgx = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i; const rgbaExecRgx = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i; @@ -117,8 +117,7 @@ const digitWithExponentRgx = /[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi; // export const unitsExecRgx = /^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)+([a-z]+|%)$/i; const unitsExecRgx = /^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i; const lowerCaseRgx = /([a-z])([A-Z])/g; -const transformsExecRgx = /(\w+)(\([^)]+\)+)/g; // Match inline transforms with cacl() values, returns the value wrapped in () const relativeValuesExecRgx = /(\*=|\+=|-=)/; const cssVariableMatchRgx = /var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/; -export { K, compositionTypes, cssVarPrefix, cssVariableMatchRgx, digitWithExponentRgx, doc, emptyString, hexTestRgx, hslExecRgx, hslaExecRgx, isBrowser, isDomSymbol, isRegisteredTargetSymbol, isSvgSymbol, lowerCaseRgx, maxFps, maxValue, minValue, morphPointsSymbol, noop, proxyTargetSymbol, relativeValuesExecRgx, rgbExecRgx, rgbaExecRgx, shortTransforms, tickModes, transformsExecRgx, transformsFragmentStrings, transformsSymbol, tweenTypes, unitsExecRgx, validTransforms, valueTypes, win }; +export { K, compositionTypes, cssVarPrefix, cssVariableMatchRgx, digitWithExponentRgx, doc, emptyString, hexTestRgx, hslExecRgx, hslaExecRgx, isBrowser, isDomSymbol, isRegisteredTargetSymbol, isSvgSymbol, lowerCaseRgx, maxFps, maxValue, minValue, noop, proxyTargetSymbol, relativeValuesExecRgx, rgbExecRgx, rgbaExecRgx, shortTransforms, tickModes, transformsFragmentStrings, transformsSymbol, tweenTypes, unitsExecRgx, validRgbHslRgx, validTransforms, valueTypes, win }; diff --git a/dist/modules/core/globals.cjs b/dist/modules/core/globals.cjs index 74b836906..1481252d4 100644 --- a/dist/modules/core/globals.cjs +++ b/dist/modules/core/globals.cjs @@ -1,8 +1,8 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -20,6 +20,18 @@ var consts = require('./consts.cjs'); * } from '../scope/index.js' */ +/** + * @typedef {Object} EditorGlobals + * @property {boolean} showPanel + * @property {boolean} synced + * @property {Function} addAnimation + * @property {Function} addTimeline + * @property {Function} addTimelineChild + * @property {Function} resolveStagger + * @property {Object|null} _head + * @property {Object|null} _tail + */ + /** @type {DefaultsParams} */ const defaults = { id: null, @@ -63,9 +75,11 @@ const globals = { timeScale: 1, /** @type {Number} */ tickThreshold: 200, + /** @type {EditorGlobals|null} */ + editor: null, }; -const globalVersions = { version: '4.2.2', engine: null }; +const globalVersions = { version: '4.4.1', engine: null }; if (consts.isBrowser) { if (!consts.win.AnimeJS) consts.win.AnimeJS = []; diff --git a/dist/modules/core/globals.d.ts b/dist/modules/core/globals.d.ts index 88e1227f4..144aeef97 100644 --- a/dist/modules/core/globals.d.ts +++ b/dist/modules/core/globals.d.ts @@ -8,6 +8,17 @@ * Scope, * } from '../scope/index.js' */ +/** + * @typedef {Object} EditorGlobals + * @property {boolean} showPanel + * @property {boolean} synced + * @property {Function} addAnimation + * @property {Function} addTimeline + * @property {Function} addTimelineChild + * @property {Function} resolveStagger + * @property {Object|null} _head + * @property {Object|null} _tail + */ /** @type {DefaultsParams} */ export const defaults: DefaultsParams; export namespace scope { @@ -19,11 +30,22 @@ export namespace globals { export let precision: number; export let timeScale: number; export let tickThreshold: number; + export let editor: EditorGlobals | null; } export namespace globalVersions { let version: string; let engine: any; } +export type EditorGlobals = { + showPanel: boolean; + synced: boolean; + addAnimation: Function; + addTimeline: Function; + addTimelineChild: Function; + resolveStagger: Function; + _head: any | null; + _tail: any | null; +}; import type { DefaultsParams } from '../types/index.js'; import type { Scope } from '../scope/index.js'; import { doc } from './consts.js'; diff --git a/dist/modules/core/globals.js b/dist/modules/core/globals.js index cc9b876cd..ef2fcebd1 100644 --- a/dist/modules/core/globals.js +++ b/dist/modules/core/globals.js @@ -1,11 +1,11 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -import { isBrowser, win, noop, maxFps, K, compositionTypes, doc } from './consts.js'; +import { isBrowser, win, noop, compositionTypes, K, maxFps, doc } from './consts.js'; /** * @import { @@ -18,6 +18,18 @@ import { isBrowser, win, noop, maxFps, K, compositionTypes, doc } from './consts * } from '../scope/index.js' */ +/** + * @typedef {Object} EditorGlobals + * @property {boolean} showPanel + * @property {boolean} synced + * @property {Function} addAnimation + * @property {Function} addTimeline + * @property {Function} addTimelineChild + * @property {Function} resolveStagger + * @property {Object|null} _head + * @property {Object|null} _tail + */ + /** @type {DefaultsParams} */ const defaults = { id: null, @@ -61,9 +73,11 @@ const globals = { timeScale: 1, /** @type {Number} */ tickThreshold: 200, + /** @type {EditorGlobals|null} */ + editor: null, }; -const globalVersions = { version: '4.2.2', engine: null }; +const globalVersions = { version: '4.4.1', engine: null }; if (isBrowser) { if (!win.AnimeJS) win.AnimeJS = []; diff --git a/dist/modules/core/helpers.cjs b/dist/modules/core/helpers.cjs index 7541a9f7c..127970de0 100644 --- a/dist/modules/core/helpers.cjs +++ b/dist/modules/core/helpers.cjs @@ -1,8 +1,8 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -59,8 +59,8 @@ const isHex = a => consts.hexTestRgx.test(a); const isRgb = a => stringStartsWith(a, 'rgb'); /**@param {any} a @return {Boolean} */ const isHsl = a => stringStartsWith(a, 'hsl'); -/**@param {any} a @return {Boolean} */ -const isCol = a => isHex(a) || isRgb(a) || isHsl(a); +/**@param {any} a @return {Boolean} */ // Make sure boxShadow syntax like 'rgb(255, 0, 0) 0px 0px 6px 0px' is not a valid color type +const isCol = a => isHex(a) || ((isRgb(a) || isHsl(a)) && (a[a.length - 1] === ')' || !consts.validRgbHslRgx.test(a))); /**@param {any} a @return {Boolean} */ const isKey = a => !globals.globals.defaults.hasOwnProperty(a); @@ -124,8 +124,6 @@ const _round = Math.round; */ const clamp = (v, min, max) => v < min ? min : v > max ? max : v; -const powCache = {}; - /** * Rounds a number to specified decimal places * @@ -133,13 +131,12 @@ const powCache = {}; * @param {Number} decimalLength - Number of decimal places * @return {Number} */ -const round = (v, decimalLength) => { - if (decimalLength < 0) return v; - if (!decimalLength) return _round(v); - let p = powCache[decimalLength]; - if (!p) p = powCache[decimalLength] = 10 ** decimalLength; - return _round(v * p) / p; -}; + const round = (v, decimalLength) => { + if (decimalLength < 0) return v; + if (!decimalLength) return _round(v); + const p = 10 ** decimalLength; + return _round(v * p) / p; + }; /** * Snaps a value to nearest increment or array value diff --git a/dist/modules/core/helpers.js b/dist/modules/core/helpers.js index caf78c3d6..1e5ecb82f 100644 --- a/dist/modules/core/helpers.js +++ b/dist/modules/core/helpers.js @@ -1,11 +1,11 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -import { isBrowser, maxValue, minValue, hexTestRgx, lowerCaseRgx } from './consts.js'; +import { isBrowser, maxValue, minValue, lowerCaseRgx, hexTestRgx, validRgbHslRgx } from './consts.js'; import { globals } from './globals.js'; /** @@ -57,8 +57,8 @@ const isHex = a => hexTestRgx.test(a); const isRgb = a => stringStartsWith(a, 'rgb'); /**@param {any} a @return {Boolean} */ const isHsl = a => stringStartsWith(a, 'hsl'); -/**@param {any} a @return {Boolean} */ -const isCol = a => isHex(a) || isRgb(a) || isHsl(a); +/**@param {any} a @return {Boolean} */ // Make sure boxShadow syntax like 'rgb(255, 0, 0) 0px 0px 6px 0px' is not a valid color type +const isCol = a => isHex(a) || ((isRgb(a) || isHsl(a)) && (a[a.length - 1] === ')' || !validRgbHslRgx.test(a))); /**@param {any} a @return {Boolean} */ const isKey = a => !globals.defaults.hasOwnProperty(a); @@ -122,8 +122,6 @@ const _round = Math.round; */ const clamp = (v, min, max) => v < min ? min : v > max ? max : v; -const powCache = {}; - /** * Rounds a number to specified decimal places * @@ -131,13 +129,12 @@ const powCache = {}; * @param {Number} decimalLength - Number of decimal places * @return {Number} */ -const round = (v, decimalLength) => { - if (decimalLength < 0) return v; - if (!decimalLength) return _round(v); - let p = powCache[decimalLength]; - if (!p) p = powCache[decimalLength] = 10 ** decimalLength; - return _round(v * p) / p; -}; + const round = (v, decimalLength) => { + if (decimalLength < 0) return v; + if (!decimalLength) return _round(v); + const p = 10 ** decimalLength; + return _round(v * p) / p; + }; /** * Snaps a value to nearest increment or array value diff --git a/dist/modules/core/render.cjs b/dist/modules/core/render.cjs index d4349157f..ba2e5c9a5 100644 --- a/dist/modules/core/render.cjs +++ b/dist/modules/core/render.cjs @@ -1,8 +1,8 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -10,9 +10,11 @@ var globals = require('./globals.cjs'); var consts = require('./consts.cjs'); var helpers = require('./helpers.cjs'); +var transforms = require('./transforms.cjs'); +var values = require('./values.cjs'); /** - * @import { + * @import { * Tickable, * Renderable, * CallbackArgument, @@ -55,7 +57,6 @@ const render = (tickable, time, muteCallbacks, internalRender, tickMode) => { const _hasChildren = tickable._hasChildren; const tickableDelay = tickable._delay; const tickablePrevAbsoluteTime = tickable._currentTime; // TODO: rename ._currentTime to ._absoluteCurrentTime - const tickableEndTime = tickableDelay + iterationDuration; const tickableAbsoluteTime = time - tickableDelay; const tickablePrevTime = helpers.clamp(tickablePrevAbsoluteTime, -tickableDelay, duration); @@ -187,30 +188,9 @@ const render = (tickable, time, muteCallbacks, internalRender, tickMode) => { number = /** @type {Number} */(tweenModifier(helpers.round(helpers.lerp(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision))); value = `${number}${tween._unit}`; } else if (tweenValueType === consts.valueTypes.COLOR) { - const fn = tween._fromNumbers; - const tn = tween._toNumbers; - const r = helpers.round(helpers.clamp(/** @type {Number} */(tweenModifier(helpers.lerp(fn[0], tn[0], tweenProgress))), 0, 255), 0); - const g = helpers.round(helpers.clamp(/** @type {Number} */(tweenModifier(helpers.lerp(fn[1], tn[1], tweenProgress))), 0, 255), 0); - const b = helpers.round(helpers.clamp(/** @type {Number} */(tweenModifier(helpers.lerp(fn[2], tn[2], tweenProgress))), 0, 255), 0); - const a = helpers.clamp(/** @type {Number} */(tweenModifier(helpers.round(helpers.lerp(fn[3], tn[3], tweenProgress), tweenPrecision))), 0, 1); - value = `rgba(${r},${g},${b},${a})`; - if (tweenHasComposition) { - const ns = tween._numbers; - ns[0] = r; - ns[1] = g; - ns[2] = b; - ns[3] = a; - } + value = values.composeColorValue(tween, tweenProgress, tweenPrecision); } else if (tweenValueType === consts.valueTypes.COMPLEX) { - value = tween._strings[0]; - for (let j = 0, l = tween._toNumbers.length; j < l; j++) { - const n = /** @type {Number} */(tweenModifier(helpers.round(helpers.lerp(tween._fromNumbers[j], tween._toNumbers[j], tweenProgress), tweenPrecision))); - const s = tween._strings[j + 1]; - value += `${s ? n + s : n}`; - if (tweenHasComposition) { - tween._numbers[j] = n; - } - } + value = values.composeComplexValue(tween, tweenProgress, tweenPrecision); } // For additive tweens and Animatables @@ -253,14 +233,8 @@ const render = (tickable, time, muteCallbacks, internalRender, tickMode) => { } - // NOTE: Possible improvement: Use translate(x,y) / translate3d(x,y,z) syntax - // to reduce memory usage on string composition if (tweenTransformsNeedUpdate && tween._renderTransforms) { - let str = consts.emptyString; - for (let key in tweenTargetTransformsProperties) { - str += `${consts.transformsFragmentStrings[key]}${tweenTargetTransformsProperties[key]}) `; - } - tweenStyle.transform = str; + tweenStyle.transform = transforms.buildTransformString(tweenTargetTransformsProperties); tweenTransformsNeedUpdate = 0; } @@ -371,7 +345,6 @@ const tick = (tickable, time, muteCallbacks, internalRender, tickMode) => { // Renders on timeline are triggered by its children so it needs to be set after rendering the children if (!muteCallbacks && tlChildrenHasRendered) tl.onRender(/** @type {CallbackArgument} */(tl)); - // Triggers the timeline onComplete() once all chindren all completed and the current time has reached the end if ((tlChildrenHaveCompleted || tlIsRunningBackwards) && tl._currentTime >= tl.duration) { // Make sure the paused flag is false in case it has been skipped in the render function diff --git a/dist/modules/core/render.js b/dist/modules/core/render.js index 1ea5f8dd1..c868673f9 100644 --- a/dist/modules/core/render.js +++ b/dist/modules/core/render.js @@ -1,16 +1,18 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { globals } from './globals.js'; -import { minValue, tickModes, valueTypes, compositionTypes, tweenTypes, transformsSymbol, transformsFragmentStrings, emptyString } from './consts.js'; +import { minValue, tickModes, valueTypes, compositionTypes, tweenTypes, transformsSymbol } from './consts.js'; import { forEachChildren, round, now, clamp, lerp } from './helpers.js'; +import { buildTransformString } from './transforms.js'; +import { composeColorValue, composeComplexValue } from './values.js'; /** - * @import { + * @import { * Tickable, * Renderable, * CallbackArgument, @@ -53,7 +55,6 @@ const render = (tickable, time, muteCallbacks, internalRender, tickMode) => { const _hasChildren = tickable._hasChildren; const tickableDelay = tickable._delay; const tickablePrevAbsoluteTime = tickable._currentTime; // TODO: rename ._currentTime to ._absoluteCurrentTime - const tickableEndTime = tickableDelay + iterationDuration; const tickableAbsoluteTime = time - tickableDelay; const tickablePrevTime = clamp(tickablePrevAbsoluteTime, -tickableDelay, duration); @@ -185,30 +186,9 @@ const render = (tickable, time, muteCallbacks, internalRender, tickMode) => { number = /** @type {Number} */(tweenModifier(round(lerp(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision))); value = `${number}${tween._unit}`; } else if (tweenValueType === valueTypes.COLOR) { - const fn = tween._fromNumbers; - const tn = tween._toNumbers; - const r = round(clamp(/** @type {Number} */(tweenModifier(lerp(fn[0], tn[0], tweenProgress))), 0, 255), 0); - const g = round(clamp(/** @type {Number} */(tweenModifier(lerp(fn[1], tn[1], tweenProgress))), 0, 255), 0); - const b = round(clamp(/** @type {Number} */(tweenModifier(lerp(fn[2], tn[2], tweenProgress))), 0, 255), 0); - const a = clamp(/** @type {Number} */(tweenModifier(round(lerp(fn[3], tn[3], tweenProgress), tweenPrecision))), 0, 1); - value = `rgba(${r},${g},${b},${a})`; - if (tweenHasComposition) { - const ns = tween._numbers; - ns[0] = r; - ns[1] = g; - ns[2] = b; - ns[3] = a; - } + value = composeColorValue(tween, tweenProgress, tweenPrecision); } else if (tweenValueType === valueTypes.COMPLEX) { - value = tween._strings[0]; - for (let j = 0, l = tween._toNumbers.length; j < l; j++) { - const n = /** @type {Number} */(tweenModifier(round(lerp(tween._fromNumbers[j], tween._toNumbers[j], tweenProgress), tweenPrecision))); - const s = tween._strings[j + 1]; - value += `${s ? n + s : n}`; - if (tweenHasComposition) { - tween._numbers[j] = n; - } - } + value = composeComplexValue(tween, tweenProgress, tweenPrecision); } // For additive tweens and Animatables @@ -251,14 +231,8 @@ const render = (tickable, time, muteCallbacks, internalRender, tickMode) => { } - // NOTE: Possible improvement: Use translate(x,y) / translate3d(x,y,z) syntax - // to reduce memory usage on string composition if (tweenTransformsNeedUpdate && tween._renderTransforms) { - let str = emptyString; - for (let key in tweenTargetTransformsProperties) { - str += `${transformsFragmentStrings[key]}${tweenTargetTransformsProperties[key]}) `; - } - tweenStyle.transform = str; + tweenStyle.transform = buildTransformString(tweenTargetTransformsProperties); tweenTransformsNeedUpdate = 0; } @@ -369,7 +343,6 @@ const tick = (tickable, time, muteCallbacks, internalRender, tickMode) => { // Renders on timeline are triggered by its children so it needs to be set after rendering the children if (!muteCallbacks && tlChildrenHasRendered) tl.onRender(/** @type {CallbackArgument} */(tl)); - // Triggers the timeline onComplete() once all chindren all completed and the current time has reached the end if ((tlChildrenHaveCompleted || tlIsRunningBackwards) && tl._currentTime >= tl.duration) { // Make sure the paused flag is false in case it has been skipped in the render function diff --git a/dist/modules/core/styles.cjs b/dist/modules/core/styles.cjs index bea43a16a..5714cdc87 100644 --- a/dist/modules/core/styles.cjs +++ b/dist/modules/core/styles.cjs @@ -1,14 +1,15 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; var consts = require('./consts.cjs'); var helpers = require('./helpers.cjs'); +var transforms = require('./transforms.cjs'); /** * @import { @@ -59,58 +60,78 @@ const sanitizePropertyName = (propertyName, target, tweenType) => { /** * @template {Renderable} T * @param {T} renderable + * @param {Boolean} [inlineStylesOnly] * @return {T} */ -const cleanInlineStyles = renderable => { - // Allow cleanInlineStyles() to be called on timelines +const revertValues = (renderable, inlineStylesOnly = false) => { + // Allow revertValues() to be called on timelines if (renderable._hasChildren) { - helpers.forEachChildren(renderable, cleanInlineStyles, true); + helpers.forEachChildren(renderable, (/** @type {Renderable} */child) => revertValues(child, inlineStylesOnly), true); } else { const animation = /** @type {JSAnimation} */(renderable); animation.pause(); helpers.forEachChildren(animation, (/** @type {Tween} */tween) => { const tweenProperty = tween.property; const tweenTarget = tween.target; - if (tweenTarget[consts.isDomSymbol]) { - const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; - const originalInlinedValue = tween._inlineValue; - const tweenHadNoInlineValue = helpers.isNil(originalInlinedValue) || originalInlinedValue === consts.emptyString; - if (tween._tweenType === consts.tweenTypes.TRANSFORM) { - const cachedTransforms = tweenTarget[consts.transformsSymbol]; - if (tweenHadNoInlineValue) { - delete cachedTransforms[tweenProperty]; - } else { - cachedTransforms[tweenProperty] = originalInlinedValue; - } - if (tween._renderTransforms) { - if (!Object.keys(cachedTransforms).length) { - targetStyle.removeProperty('transform'); + const tweenType = tween._tweenType; + const originalInlinedValue = tween._inlineValue; + const tweenHadNoInlineValue = helpers.isNil(originalInlinedValue) || originalInlinedValue === consts.emptyString; + if (tweenType === consts.tweenTypes.OBJECT) { + if (!inlineStylesOnly && !tweenHadNoInlineValue) { + tweenTarget[tweenProperty] = originalInlinedValue; + } + } else if (tweenTarget[consts.isDomSymbol]) { + if (tweenType === consts.tweenTypes.ATTRIBUTE) { + if (!inlineStylesOnly) { + if (tweenHadNoInlineValue) { + /** @type {DOMTarget} */(tweenTarget).removeAttribute(tweenProperty); } else { - let str = consts.emptyString; - for (let key in cachedTransforms) { - str += consts.transformsFragmentStrings[key] + cachedTransforms[key] + ') '; - } - targetStyle.transform = str; + /** @type {DOMTarget} */(tweenTarget).setAttribute(tweenProperty, /** @type {String} */(originalInlinedValue)); } } } else { - if (tweenHadNoInlineValue) { - targetStyle.removeProperty(helpers.toLowerCase(tweenProperty)); + const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; + if (tweenType === consts.tweenTypes.TRANSFORM) { + const cachedTransforms = tweenTarget[consts.transformsSymbol]; + if (tweenHadNoInlineValue) { + delete cachedTransforms[tweenProperty]; + } else { + cachedTransforms[tweenProperty] = originalInlinedValue; + } + if (tween._renderTransforms) { + if (!Object.keys(cachedTransforms).length) { + targetStyle.removeProperty('transform'); + } else { + targetStyle.transform = transforms.buildTransformString(cachedTransforms); + } + } } else { - targetStyle[tweenProperty] = originalInlinedValue; + if (tweenHadNoInlineValue) { + targetStyle.removeProperty(helpers.toLowerCase(tweenProperty)); + } else { + targetStyle[tweenProperty] = originalInlinedValue; + } } } - if (animation._tail === tween) { - animation.targets.forEach(t => { - if (t.getAttribute && t.getAttribute('style') === consts.emptyString) { - t.removeAttribute('style'); - } }); - } + } + if (tweenTarget[consts.isDomSymbol] && animation._tail === tween) { + animation.targets.forEach(t => { + if (t.getAttribute && t.getAttribute('style') === consts.emptyString) { + t.removeAttribute('style'); + } }); } }); } return renderable; }; +/** + * @template {Renderable} T + * @param {T} renderable + * @return {T} + */ +const cleanInlineStyles = renderable => revertValues(renderable, true); + exports.cleanInlineStyles = cleanInlineStyles; +exports.revertValues = revertValues; exports.sanitizePropertyName = sanitizePropertyName; diff --git a/dist/modules/core/styles.d.ts b/dist/modules/core/styles.d.ts index 8be6491e5..1598d26f6 100644 --- a/dist/modules/core/styles.d.ts +++ b/dist/modules/core/styles.d.ts @@ -1,4 +1,5 @@ export function sanitizePropertyName(propertyName: string, target: Target, tweenType: tweenTypes): string; +export function revertValues(renderable: T, inlineStylesOnly?: boolean): T; export function cleanInlineStyles(renderable: T): T; import type { Target } from '../types/index.js'; import { tweenTypes } from './consts.js'; diff --git a/dist/modules/core/styles.js b/dist/modules/core/styles.js index 63a3248fa..8940328d3 100644 --- a/dist/modules/core/styles.js +++ b/dist/modules/core/styles.js @@ -1,12 +1,13 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -import { tweenTypes, shortTransforms, isDomSymbol, transformsSymbol, transformsFragmentStrings, emptyString } from './consts.js'; -import { forEachChildren, isSvg, toLowerCase, isNil } from './helpers.js'; +import { tweenTypes, isDomSymbol, transformsSymbol, emptyString, shortTransforms } from './consts.js'; +import { forEachChildren, toLowerCase, isNil, isSvg } from './helpers.js'; +import { buildTransformString } from './transforms.js'; /** * @import { @@ -57,57 +58,76 @@ const sanitizePropertyName = (propertyName, target, tweenType) => { /** * @template {Renderable} T * @param {T} renderable + * @param {Boolean} [inlineStylesOnly] * @return {T} */ -const cleanInlineStyles = renderable => { - // Allow cleanInlineStyles() to be called on timelines +const revertValues = (renderable, inlineStylesOnly = false) => { + // Allow revertValues() to be called on timelines if (renderable._hasChildren) { - forEachChildren(renderable, cleanInlineStyles, true); + forEachChildren(renderable, (/** @type {Renderable} */child) => revertValues(child, inlineStylesOnly), true); } else { const animation = /** @type {JSAnimation} */(renderable); animation.pause(); forEachChildren(animation, (/** @type {Tween} */tween) => { const tweenProperty = tween.property; const tweenTarget = tween.target; - if (tweenTarget[isDomSymbol]) { - const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; - const originalInlinedValue = tween._inlineValue; - const tweenHadNoInlineValue = isNil(originalInlinedValue) || originalInlinedValue === emptyString; - if (tween._tweenType === tweenTypes.TRANSFORM) { - const cachedTransforms = tweenTarget[transformsSymbol]; - if (tweenHadNoInlineValue) { - delete cachedTransforms[tweenProperty]; - } else { - cachedTransforms[tweenProperty] = originalInlinedValue; - } - if (tween._renderTransforms) { - if (!Object.keys(cachedTransforms).length) { - targetStyle.removeProperty('transform'); + const tweenType = tween._tweenType; + const originalInlinedValue = tween._inlineValue; + const tweenHadNoInlineValue = isNil(originalInlinedValue) || originalInlinedValue === emptyString; + if (tweenType === tweenTypes.OBJECT) { + if (!inlineStylesOnly && !tweenHadNoInlineValue) { + tweenTarget[tweenProperty] = originalInlinedValue; + } + } else if (tweenTarget[isDomSymbol]) { + if (tweenType === tweenTypes.ATTRIBUTE) { + if (!inlineStylesOnly) { + if (tweenHadNoInlineValue) { + /** @type {DOMTarget} */(tweenTarget).removeAttribute(tweenProperty); } else { - let str = emptyString; - for (let key in cachedTransforms) { - str += transformsFragmentStrings[key] + cachedTransforms[key] + ') '; - } - targetStyle.transform = str; + /** @type {DOMTarget} */(tweenTarget).setAttribute(tweenProperty, /** @type {String} */(originalInlinedValue)); } } } else { - if (tweenHadNoInlineValue) { - targetStyle.removeProperty(toLowerCase(tweenProperty)); + const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; + if (tweenType === tweenTypes.TRANSFORM) { + const cachedTransforms = tweenTarget[transformsSymbol]; + if (tweenHadNoInlineValue) { + delete cachedTransforms[tweenProperty]; + } else { + cachedTransforms[tweenProperty] = originalInlinedValue; + } + if (tween._renderTransforms) { + if (!Object.keys(cachedTransforms).length) { + targetStyle.removeProperty('transform'); + } else { + targetStyle.transform = buildTransformString(cachedTransforms); + } + } } else { - targetStyle[tweenProperty] = originalInlinedValue; + if (tweenHadNoInlineValue) { + targetStyle.removeProperty(toLowerCase(tweenProperty)); + } else { + targetStyle[tweenProperty] = originalInlinedValue; + } } } - if (animation._tail === tween) { - animation.targets.forEach(t => { - if (t.getAttribute && t.getAttribute('style') === emptyString) { - t.removeAttribute('style'); - } }); - } + } + if (tweenTarget[isDomSymbol] && animation._tail === tween) { + animation.targets.forEach(t => { + if (t.getAttribute && t.getAttribute('style') === emptyString) { + t.removeAttribute('style'); + } }); } }); } return renderable; }; -export { cleanInlineStyles, sanitizePropertyName }; +/** + * @template {Renderable} T + * @param {T} renderable + * @return {T} + */ +const cleanInlineStyles = renderable => revertValues(renderable, true); + +export { cleanInlineStyles, revertValues, sanitizePropertyName }; diff --git a/dist/modules/core/targets.cjs b/dist/modules/core/targets.cjs index fc8faa66f..e9aab7321 100644 --- a/dist/modules/core/targets.cjs +++ b/dist/modules/core/targets.cjs @@ -1,8 +1,8 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/core/targets.js b/dist/modules/core/targets.js index 67b580cd0..b87a88cbc 100644 --- a/dist/modules/core/targets.js +++ b/dist/modules/core/targets.js @@ -1,8 +1,8 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { scope } from './globals.js'; diff --git a/dist/modules/core/transforms.cjs b/dist/modules/core/transforms.cjs index 02d89591e..cb6a1b00a 100644 --- a/dist/modules/core/transforms.cjs +++ b/dist/modules/core/transforms.cjs @@ -1,8 +1,8 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -24,26 +24,142 @@ var helpers = require('./helpers.cjs'); */ const parseInlineTransforms = (target, propName, animationInlineStyles) => { const inlineTransforms = target.style.transform; - let inlinedStylesPropertyValue; if (inlineTransforms) { const cachedTransforms = target[consts.transformsSymbol]; - let t; while (t = consts.transformsExecRgx.exec(inlineTransforms)) { - const inlinePropertyName = t[1]; - // const inlinePropertyValue = t[2]; - const inlinePropertyValue = t[2].slice(1, -1); - cachedTransforms[inlinePropertyName] = inlinePropertyValue; - if (inlinePropertyName === propName) { - inlinedStylesPropertyValue = inlinePropertyValue; - // Store the new parsed inline styles if animationInlineStyles is provided - if (animationInlineStyles) { - animationInlineStyles[propName] = inlinePropertyValue; + let pos = 0; + const len = inlineTransforms.length; + let fullTranslateValue; + while (pos < len) { + // Skip whitespace + while (pos < len && inlineTransforms.charCodeAt(pos) === 32) pos++; + if (pos >= len) break; + // Read function name + const nameStart = pos; + while (pos < len && inlineTransforms.charCodeAt(pos) !== 40) pos++; + if (pos >= len) break; + const name = inlineTransforms.substring(nameStart, pos); + // Scan to closing paren, recording top-level comma positions + let depth = 1; + const valueStart = pos + 1; + let c1 = -1, c2 = -1; + pos++; + while (pos < len && depth > 0) { + const c = inlineTransforms.charCodeAt(pos); + if (c === 40) depth++; + else if (c === 41) depth--; + else if (c === 44 && depth === 1) { + if (c1 === -1) c1 = pos; + else if (c2 === -1) c2 = pos; } + pos++; } + const valueEnd = pos - 1; + // Decompose multi-arg functions into individual axis properties + if (name === 'translate' || name === 'translate3d') { + if (c1 === -1) { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.translateZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } + } + fullTranslateValue = inlineTransforms.substring(valueStart, valueEnd); + } else if (name === 'scale' || name === 'scale3d') { + if (c1 === -1) { + cachedTransforms.scale = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.scaleX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.scaleZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } + } + } else { + cachedTransforms[name] = inlineTransforms.substring(valueStart, valueEnd); + } + } + // Resolve the requested property from the cache + if (propName === 'translate3d' && fullTranslateValue) { + if (animationInlineStyles) animationInlineStyles[propName] = fullTranslateValue; + return fullTranslateValue; + } + const cached = cachedTransforms[propName]; + if (!helpers.isUnd(cached)) { + if (animationInlineStyles) animationInlineStyles[propName] = cached; + return cached; } } - return inlineTransforms && !helpers.isUnd(inlinedStylesPropertyValue) ? inlinedStylesPropertyValue : + return propName === 'translate3d' ? '0px, 0px, 0px' : + propName === 'rotate3d' ? '0, 0, 0, 0deg' : helpers.stringStartsWith(propName, 'scale') ? '1' : helpers.stringStartsWith(propName, 'rotate') || helpers.stringStartsWith(propName, 'skew') ? '0deg' : '0px'; }; +/** + * Builds a CSS transform string from the target's cached transform properties. + * Iterates validTransforms in order (perspective > translate > rotate > scale > skew > matrix). + * When adjacent axis properties are all present, emits a shorter shorthand (translateX + translateY -> translate(x, y)) + * The index is advanced past consumed properties so they are not emitted twice. + * Properties without a grouping partner (e.g. translateY alone, scaleZ alone) emit individually. + * + * @param {Record} props + * @return {String} + */ +const buildTransformString = (props) => { + let str = consts.emptyString; + for (let i = 0, l = consts.validTransforms.length; i < l; i++) { + const key = consts.validTransforms[i]; + const val = props[key]; + if (val !== undefined) { + // Group translateX with adjacent translateY / translateZ + if (key === 'translateX') { + const next = props.translateY; + if (next !== undefined) { + const next2 = props.translateZ; + if (next2 !== undefined) { + str += `translate3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `translate(${val},${next}) `; + i += 1; + } + continue; + } + } + // Group scaleX with adjacent scaleY / scaleZ (only when standalone scale is absent) + if (key === 'scaleX' && props.scale === undefined) { + const next = props.scaleY; + if (next !== undefined) { + const next2 = props.scaleZ; + if (next2 !== undefined) { + str += `scale3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `scale(${val},${next}) `; + i += 1; + } + continue; + } + } + // All other properties: emit individually using pre-built fragment string + str += `${consts.transformsFragmentStrings[key]}${val}) `; + } + // Preserve non-animatable rotate3d in correct position (after rotateZ, before scale) + if (key === 'rotateZ') { + if (props.rotate3d !== undefined) str += `rotate3d(${props.rotate3d}) `; + } + } + // Preserve non-animatable matrix/matrix3d from inline styles + if (props.matrix !== undefined) str += `matrix(${props.matrix}) `; + if (props.matrix3d !== undefined) str += `matrix3d(${props.matrix3d}) `; + return str; +}; + +exports.buildTransformString = buildTransformString; exports.parseInlineTransforms = parseInlineTransforms; diff --git a/dist/modules/core/transforms.d.ts b/dist/modules/core/transforms.d.ts index d393500da..34c55b5df 100644 --- a/dist/modules/core/transforms.d.ts +++ b/dist/modules/core/transforms.d.ts @@ -1,2 +1,3 @@ export function parseInlineTransforms(target: DOMTarget, propName: string, animationInlineStyles: any): string; +export function buildTransformString(props: Record): string; import type { DOMTarget } from '../types/index.js'; diff --git a/dist/modules/core/transforms.js b/dist/modules/core/transforms.js index f3f3ee8bf..ea0e05780 100644 --- a/dist/modules/core/transforms.js +++ b/dist/modules/core/transforms.js @@ -1,11 +1,11 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -import { transformsSymbol, transformsExecRgx } from './consts.js'; +import { transformsFragmentStrings, emptyString, validTransforms, transformsSymbol } from './consts.js'; import { isUnd, stringStartsWith } from './helpers.js'; /** @@ -22,26 +22,141 @@ import { isUnd, stringStartsWith } from './helpers.js'; */ const parseInlineTransforms = (target, propName, animationInlineStyles) => { const inlineTransforms = target.style.transform; - let inlinedStylesPropertyValue; if (inlineTransforms) { const cachedTransforms = target[transformsSymbol]; - let t; while (t = transformsExecRgx.exec(inlineTransforms)) { - const inlinePropertyName = t[1]; - // const inlinePropertyValue = t[2]; - const inlinePropertyValue = t[2].slice(1, -1); - cachedTransforms[inlinePropertyName] = inlinePropertyValue; - if (inlinePropertyName === propName) { - inlinedStylesPropertyValue = inlinePropertyValue; - // Store the new parsed inline styles if animationInlineStyles is provided - if (animationInlineStyles) { - animationInlineStyles[propName] = inlinePropertyValue; + let pos = 0; + const len = inlineTransforms.length; + let fullTranslateValue; + while (pos < len) { + // Skip whitespace + while (pos < len && inlineTransforms.charCodeAt(pos) === 32) pos++; + if (pos >= len) break; + // Read function name + const nameStart = pos; + while (pos < len && inlineTransforms.charCodeAt(pos) !== 40) pos++; + if (pos >= len) break; + const name = inlineTransforms.substring(nameStart, pos); + // Scan to closing paren, recording top-level comma positions + let depth = 1; + const valueStart = pos + 1; + let c1 = -1, c2 = -1; + pos++; + while (pos < len && depth > 0) { + const c = inlineTransforms.charCodeAt(pos); + if (c === 40) depth++; + else if (c === 41) depth--; + else if (c === 44 && depth === 1) { + if (c1 === -1) c1 = pos; + else if (c2 === -1) c2 = pos; } + pos++; } + const valueEnd = pos - 1; + // Decompose multi-arg functions into individual axis properties + if (name === 'translate' || name === 'translate3d') { + if (c1 === -1) { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.translateZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } + } + fullTranslateValue = inlineTransforms.substring(valueStart, valueEnd); + } else if (name === 'scale' || name === 'scale3d') { + if (c1 === -1) { + cachedTransforms.scale = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.scaleX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.scaleZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } + } + } else { + cachedTransforms[name] = inlineTransforms.substring(valueStart, valueEnd); + } + } + // Resolve the requested property from the cache + if (propName === 'translate3d' && fullTranslateValue) { + if (animationInlineStyles) animationInlineStyles[propName] = fullTranslateValue; + return fullTranslateValue; + } + const cached = cachedTransforms[propName]; + if (!isUnd(cached)) { + if (animationInlineStyles) animationInlineStyles[propName] = cached; + return cached; } } - return inlineTransforms && !isUnd(inlinedStylesPropertyValue) ? inlinedStylesPropertyValue : + return propName === 'translate3d' ? '0px, 0px, 0px' : + propName === 'rotate3d' ? '0, 0, 0, 0deg' : stringStartsWith(propName, 'scale') ? '1' : stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew') ? '0deg' : '0px'; }; -export { parseInlineTransforms }; +/** + * Builds a CSS transform string from the target's cached transform properties. + * Iterates validTransforms in order (perspective > translate > rotate > scale > skew > matrix). + * When adjacent axis properties are all present, emits a shorter shorthand (translateX + translateY -> translate(x, y)) + * The index is advanced past consumed properties so they are not emitted twice. + * Properties without a grouping partner (e.g. translateY alone, scaleZ alone) emit individually. + * + * @param {Record} props + * @return {String} + */ +const buildTransformString = (props) => { + let str = emptyString; + for (let i = 0, l = validTransforms.length; i < l; i++) { + const key = validTransforms[i]; + const val = props[key]; + if (val !== undefined) { + // Group translateX with adjacent translateY / translateZ + if (key === 'translateX') { + const next = props.translateY; + if (next !== undefined) { + const next2 = props.translateZ; + if (next2 !== undefined) { + str += `translate3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `translate(${val},${next}) `; + i += 1; + } + continue; + } + } + // Group scaleX with adjacent scaleY / scaleZ (only when standalone scale is absent) + if (key === 'scaleX' && props.scale === undefined) { + const next = props.scaleY; + if (next !== undefined) { + const next2 = props.scaleZ; + if (next2 !== undefined) { + str += `scale3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `scale(${val},${next}) `; + i += 1; + } + continue; + } + } + // All other properties: emit individually using pre-built fragment string + str += `${transformsFragmentStrings[key]}${val}) `; + } + // Preserve non-animatable rotate3d in correct position (after rotateZ, before scale) + if (key === 'rotateZ') { + if (props.rotate3d !== undefined) str += `rotate3d(${props.rotate3d}) `; + } + } + // Preserve non-animatable matrix/matrix3d from inline styles + if (props.matrix !== undefined) str += `matrix(${props.matrix}) `; + if (props.matrix3d !== undefined) str += `matrix3d(${props.matrix3d}) `; + return str; +}; + +export { buildTransformString, parseInlineTransforms }; diff --git a/dist/modules/core/units.cjs b/dist/modules/core/units.cjs index e01c066e7..91de18b84 100644 --- a/dist/modules/core/units.cjs +++ b/dist/modules/core/units.cjs @@ -1,8 +1,8 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/core/units.js b/dist/modules/core/units.js index 3cba437f1..ce21dc5a4 100644 --- a/dist/modules/core/units.js +++ b/dist/modules/core/units.js @@ -1,8 +1,8 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { valueTypes, doc } from './consts.js'; diff --git a/dist/modules/core/values.cjs b/dist/modules/core/values.cjs index 87b1a9999..eeb1b1587 100644 --- a/dist/modules/core/values.cjs +++ b/dist/modules/core/values.cjs @@ -1,8 +1,8 @@ /** * Anime.js - core - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -19,6 +19,7 @@ var colors = require('./colors.cjs'); * Tween, * TweenPropValue, * TweenDecomposedValue, +* TargetsArray, * } from '../types/index.js' */ @@ -36,15 +37,16 @@ const setValue = (targetValue, defaultValue) => { * @param {TweenPropValue} value * @param {Target} target * @param {Number} index - * @param {Number} total - * @param {Object} [store] + * @param {TargetsArray} targets + * @param {Object|null} store + * @param {Tween|null} prevTween * @return {any} */ -const getFunctionValue = (value, target, index, total, store) => { +const getFunctionValue = (value, target, index, targets, store, prevTween) => { let func; if (helpers.isFnc(value)) { func = () => { - const computed = /** @type {Function} */(value)(target, index, total); + const computed = /** @type {Function} */(value)(target, index, targets, prevTween); // Fallback to 0 if the function returns undefined / NaN / null / false / 0 return !isNaN(+computed) ? +computed : computed || 0; }; @@ -111,9 +113,17 @@ const getCSSValue = (target, propName, animationInlineStyles) => { */ const getOriginalAnimatableValue = (target, propName, tweenType, animationInlineStyles) => { const type = !helpers.isUnd(tweenType) ? tweenType : getTweenType(target, propName); - return type === consts.tweenTypes.OBJECT ? target[propName] || 0 : - type === consts.tweenTypes.ATTRIBUTE ? /** @type {DOMTarget} */(target).getAttribute(propName) : - type === consts.tweenTypes.TRANSFORM ? transforms.parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : + if (type === consts.tweenTypes.OBJECT) { + const value = target[propName]; + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value || 0; + } + if (type === consts.tweenTypes.ATTRIBUTE) { + const value = /** @type {DOMTarget} */(target).getAttribute(propName); + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value; + } + return type === consts.tweenTypes.TRANSFORM ? transforms.parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : type === consts.tweenTypes.CSS_VAR ? getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles).trimStart() : getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles); }; @@ -215,6 +225,56 @@ const decomposeTweenValue = (tween, targetObject) => { const decomposedOriginalValue = createDecomposedValueTargetObject(); +/** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ +const composeColorValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const r = helpers.round(helpers.clamp(/** @type {Number} */(mod(helpers.lerp(fn[0], tn[0], progress))), 0, 255), 0); + const g = helpers.round(helpers.clamp(/** @type {Number} */(mod(helpers.lerp(fn[1], tn[1], progress))), 0, 255), 0); + const b = helpers.round(helpers.clamp(/** @type {Number} */(mod(helpers.lerp(fn[2], tn[2], progress))), 0, 255), 0); + const a = helpers.clamp(/** @type {Number} */(mod(helpers.round(helpers.lerp(fn[3], tn[3], progress), precision))), 0, 1); + if (tween._composition !== consts.compositionTypes.none) { + const ns = tween._numbers; + ns[0] = r; + ns[1] = g; + ns[2] = b; + ns[3] = a; + } + return `rgba(${r},${g},${b},${a})`; +}; + +/** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ +const composeComplexValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const ts = tween._strings; + const hasComposition = tween._composition !== consts.compositionTypes.none; + let v = ts[0]; + for (let j = 0, l = tn.length; j < l; j++) { + const n = /** @type {Number} */(mod(helpers.round(helpers.lerp(fn[j], tn[j], progress), precision))); + const s = ts[j + 1]; + v += `${s ? n + s : n}`; + if (hasComposition) { + tween._numbers[j] = n; + } + } + return v; +}; + +exports.composeColorValue = composeColorValue; +exports.composeComplexValue = composeComplexValue; exports.createDecomposedValueTargetObject = createDecomposedValueTargetObject; exports.decomposeRawValue = decomposeRawValue; exports.decomposeTweenValue = decomposeTweenValue; diff --git a/dist/modules/core/values.d.ts b/dist/modules/core/values.d.ts index e4fd7d88f..e12fe2ef1 100644 --- a/dist/modules/core/values.d.ts +++ b/dist/modules/core/values.d.ts @@ -1,5 +1,5 @@ export function setValue(targetValue: T | undefined, defaultValue: D): T | D; -export function getFunctionValue(value: TweenPropValue, target: Target, index: number, total: number, store?: any): any; +export function getFunctionValue(value: TweenPropValue, target: Target, index: number, targets: TargetsArray, store: any | null, prevTween: Tween | null): any; export function getTweenType(target: Target, prop: string): tweenTypes; export function getOriginalAnimatableValue(target: Target, propName: string, tweenType?: tweenTypes, animationInlineStyles?: any | void): string | number; export function getRelativeValue(x: number, y: number, operator: string): number; @@ -7,8 +7,11 @@ export function createDecomposedValueTargetObject(): TweenDecomposedValue; export function decomposeRawValue(rawValue: string | number, targetObject: TweenDecomposedValue): TweenDecomposedValue; export function decomposeTweenValue(tween: Tween, targetObject: TweenDecomposedValue): TweenDecomposedValue; export const decomposedOriginalValue: TweenDecomposedValue; +export function composeColorValue(tween: Tween, progress: number, precision: number): string; +export function composeComplexValue(tween: Tween, progress: number, precision: number): string; import type { TweenPropValue } from '../types/index.js'; import type { Target } from '../types/index.js'; +import type { TargetsArray } from '../types/index.js'; +import type { Tween } from '../types/index.js'; import { tweenTypes } from './consts.js'; import type { TweenDecomposedValue } from '../types/index.js'; -import type { Tween } from '../types/index.js'; diff --git a/dist/modules/core/values.js b/dist/modules/core/values.js index fea014d80..a3a75a07a 100644 --- a/dist/modules/core/values.js +++ b/dist/modules/core/values.js @@ -1,12 +1,12 @@ /** * Anime.js - core - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -import { tweenTypes, isDomSymbol, isSvgSymbol, validTransforms, shortTransforms, valueTypes, unitsExecRgx, digitWithExponentRgx, proxyTargetSymbol, cssVarPrefix, cssVariableMatchRgx, emptyString } from './consts.js'; -import { isUnd, isValidSVGAttribute, stringStartsWith, isCol, isFnc, isStr, cloneArray } from './helpers.js'; +import { tweenTypes, isDomSymbol, isSvgSymbol, validTransforms, shortTransforms, valueTypes, unitsExecRgx, digitWithExponentRgx, compositionTypes, proxyTargetSymbol, cssVarPrefix, cssVariableMatchRgx, emptyString } from './consts.js'; +import { isUnd, isValidSVGAttribute, stringStartsWith, isCol, isFnc, isStr, round, lerp, clamp, cloneArray } from './helpers.js'; import { parseInlineTransforms } from './transforms.js'; import { convertColorStringValuesToRgbaArray } from './colors.js'; @@ -17,6 +17,7 @@ import { convertColorStringValuesToRgbaArray } from './colors.js'; * Tween, * TweenPropValue, * TweenDecomposedValue, +* TargetsArray, * } from '../types/index.js' */ @@ -34,15 +35,16 @@ const setValue = (targetValue, defaultValue) => { * @param {TweenPropValue} value * @param {Target} target * @param {Number} index - * @param {Number} total - * @param {Object} [store] + * @param {TargetsArray} targets + * @param {Object|null} store + * @param {Tween|null} prevTween * @return {any} */ -const getFunctionValue = (value, target, index, total, store) => { +const getFunctionValue = (value, target, index, targets, store, prevTween) => { let func; if (isFnc(value)) { func = () => { - const computed = /** @type {Function} */(value)(target, index, total); + const computed = /** @type {Function} */(value)(target, index, targets, prevTween); // Fallback to 0 if the function returns undefined / NaN / null / false / 0 return !isNaN(+computed) ? +computed : computed || 0; }; @@ -109,9 +111,17 @@ const getCSSValue = (target, propName, animationInlineStyles) => { */ const getOriginalAnimatableValue = (target, propName, tweenType, animationInlineStyles) => { const type = !isUnd(tweenType) ? tweenType : getTweenType(target, propName); - return type === tweenTypes.OBJECT ? target[propName] || 0 : - type === tweenTypes.ATTRIBUTE ? /** @type {DOMTarget} */(target).getAttribute(propName) : - type === tweenTypes.TRANSFORM ? parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : + if (type === tweenTypes.OBJECT) { + const value = target[propName]; + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value || 0; + } + if (type === tweenTypes.ATTRIBUTE) { + const value = /** @type {DOMTarget} */(target).getAttribute(propName); + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value; + } + return type === tweenTypes.TRANSFORM ? parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : type === tweenTypes.CSS_VAR ? getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles).trimStart() : getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles); }; @@ -213,4 +223,52 @@ const decomposeTweenValue = (tween, targetObject) => { const decomposedOriginalValue = createDecomposedValueTargetObject(); -export { createDecomposedValueTargetObject, decomposeRawValue, decomposeTweenValue, decomposedOriginalValue, getFunctionValue, getOriginalAnimatableValue, getRelativeValue, getTweenType, setValue }; +/** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ +const composeColorValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const r = round(clamp(/** @type {Number} */(mod(lerp(fn[0], tn[0], progress))), 0, 255), 0); + const g = round(clamp(/** @type {Number} */(mod(lerp(fn[1], tn[1], progress))), 0, 255), 0); + const b = round(clamp(/** @type {Number} */(mod(lerp(fn[2], tn[2], progress))), 0, 255), 0); + const a = clamp(/** @type {Number} */(mod(round(lerp(fn[3], tn[3], progress), precision))), 0, 1); + if (tween._composition !== compositionTypes.none) { + const ns = tween._numbers; + ns[0] = r; + ns[1] = g; + ns[2] = b; + ns[3] = a; + } + return `rgba(${r},${g},${b},${a})`; +}; + +/** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ +const composeComplexValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const ts = tween._strings; + const hasComposition = tween._composition !== compositionTypes.none; + let v = ts[0]; + for (let j = 0, l = tn.length; j < l; j++) { + const n = /** @type {Number} */(mod(round(lerp(fn[j], tn[j], progress), precision))); + const s = ts[j + 1]; + v += `${s ? n + s : n}`; + if (hasComposition) { + tween._numbers[j] = n; + } + } + return v; +}; + +export { composeColorValue, composeComplexValue, createDecomposedValueTargetObject, decomposeRawValue, decomposeTweenValue, decomposedOriginalValue, getFunctionValue, getOriginalAnimatableValue, getRelativeValue, getTweenType, setValue }; diff --git a/dist/modules/draggable/draggable.cjs b/dist/modules/draggable/draggable.cjs index 660d81027..2b4fa10a0 100644 --- a/dist/modules/draggable/draggable.cjs +++ b/dist/modules/draggable/draggable.cjs @@ -1,8 +1,8 @@ /** * Anime.js - draggable - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/draggable/draggable.js b/dist/modules/draggable/draggable.js index 06e5e85a5..dea03df43 100644 --- a/dist/modules/draggable/draggable.js +++ b/dist/modules/draggable/draggable.js @@ -1,8 +1,8 @@ /** * Anime.js - draggable - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { globals, scope } from '../core/globals.js'; diff --git a/dist/modules/draggable/index.cjs b/dist/modules/draggable/index.cjs index 2ba7ccdc7..55e03311a 100644 --- a/dist/modules/draggable/index.cjs +++ b/dist/modules/draggable/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - draggable - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/draggable/index.js b/dist/modules/draggable/index.js index f871a45f5..e2abc97ed 100644 --- a/dist/modules/draggable/index.js +++ b/dist/modules/draggable/index.js @@ -1,8 +1,8 @@ /** * Anime.js - draggable - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { Draggable, createDraggable } from './draggable.js'; diff --git a/dist/modules/easings/cubic-bezier/index.cjs b/dist/modules/easings/cubic-bezier/index.cjs index ffb54a7fc..a3c764b90 100644 --- a/dist/modules/easings/cubic-bezier/index.cjs +++ b/dist/modules/easings/cubic-bezier/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - easings - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/easings/cubic-bezier/index.js b/dist/modules/easings/cubic-bezier/index.js index c9d99c6ed..97302b7c9 100644 --- a/dist/modules/easings/cubic-bezier/index.js +++ b/dist/modules/easings/cubic-bezier/index.js @@ -1,8 +1,8 @@ /** * Anime.js - easings - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { abs } from '../../core/helpers.js'; diff --git a/dist/modules/easings/eases/index.cjs b/dist/modules/easings/eases/index.cjs index 09150ef19..8d97ef146 100644 --- a/dist/modules/easings/eases/index.cjs +++ b/dist/modules/easings/eases/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - easings - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/easings/eases/index.js b/dist/modules/easings/eases/index.js index 589fd6227..350ec226c 100644 --- a/dist/modules/easings/eases/index.js +++ b/dist/modules/easings/eases/index.js @@ -1,8 +1,8 @@ /** * Anime.js - easings - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { eases } from './parser.js'; diff --git a/dist/modules/easings/eases/parser.cjs b/dist/modules/easings/eases/parser.cjs index 1d74d0a5d..5e6ec605d 100644 --- a/dist/modules/easings/eases/parser.cjs +++ b/dist/modules/easings/eases/parser.cjs @@ -1,8 +1,8 @@ /** * Anime.js - easings - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/easings/eases/parser.js b/dist/modules/easings/eases/parser.js index 72fd4573e..4392b5dc1 100644 --- a/dist/modules/easings/eases/parser.js +++ b/dist/modules/easings/eases/parser.js @@ -1,8 +1,8 @@ /** * Anime.js - easings - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { emptyString, minValue } from '../../core/consts.js'; diff --git a/dist/modules/easings/index.cjs b/dist/modules/easings/index.cjs index f39aeed19..0e355d918 100644 --- a/dist/modules/easings/index.cjs +++ b/dist/modules/easings/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - easings - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/easings/index.js b/dist/modules/easings/index.js index ffa65b58b..dcaf566e1 100644 --- a/dist/modules/easings/index.js +++ b/dist/modules/easings/index.js @@ -1,8 +1,8 @@ /** * Anime.js - easings - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { cubicBezier } from './cubic-bezier/index.js'; diff --git a/dist/modules/easings/irregular/index.cjs b/dist/modules/easings/irregular/index.cjs index c2c610bdc..a61f9a4cf 100644 --- a/dist/modules/easings/irregular/index.cjs +++ b/dist/modules/easings/irregular/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - easings - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/easings/irregular/index.js b/dist/modules/easings/irregular/index.js index 0aa562e4b..e712ef1cb 100644 --- a/dist/modules/easings/irregular/index.js +++ b/dist/modules/easings/irregular/index.js @@ -1,8 +1,8 @@ /** * Anime.js - easings - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { clamp } from '../../core/helpers.js'; diff --git a/dist/modules/easings/linear/index.cjs b/dist/modules/easings/linear/index.cjs index 2d0d7339e..27e217080 100644 --- a/dist/modules/easings/linear/index.cjs +++ b/dist/modules/easings/linear/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - easings - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/easings/linear/index.js b/dist/modules/easings/linear/index.js index ce109a926..5ffb36a0a 100644 --- a/dist/modules/easings/linear/index.js +++ b/dist/modules/easings/linear/index.js @@ -1,8 +1,8 @@ /** * Anime.js - easings - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { isStr, parseNumber, isUnd } from '../../core/helpers.js'; diff --git a/dist/modules/easings/none.cjs b/dist/modules/easings/none.cjs index 19153ef6c..ed29c207b 100644 --- a/dist/modules/easings/none.cjs +++ b/dist/modules/easings/none.cjs @@ -1,8 +1,8 @@ /** * Anime.js - easings - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/easings/none.js b/dist/modules/easings/none.js index f6ca005df..4267710c6 100644 --- a/dist/modules/easings/none.js +++ b/dist/modules/easings/none.js @@ -1,8 +1,8 @@ /** * Anime.js - easings - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ /** diff --git a/dist/modules/easings/spring/index.cjs b/dist/modules/easings/spring/index.cjs index e0393e535..e85831492 100644 --- a/dist/modules/easings/spring/index.cjs +++ b/dist/modules/easings/spring/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - easings - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/easings/spring/index.js b/dist/modules/easings/spring/index.js index 937347cf0..e4616dc5b 100644 --- a/dist/modules/easings/spring/index.js +++ b/dist/modules/easings/spring/index.js @@ -1,8 +1,8 @@ /** * Anime.js - easings - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { K, minValue, noop } from '../../core/consts.js'; diff --git a/dist/modules/easings/steps/index.cjs b/dist/modules/easings/steps/index.cjs index 931e3edb2..2c2a75d87 100644 --- a/dist/modules/easings/steps/index.cjs +++ b/dist/modules/easings/steps/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - easings - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/easings/steps/index.js b/dist/modules/easings/steps/index.js index 2247ddb4b..069171a6b 100644 --- a/dist/modules/easings/steps/index.js +++ b/dist/modules/easings/steps/index.js @@ -1,8 +1,8 @@ /** * Anime.js - easings - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { ceil, floor, clamp } from '../../core/helpers.js'; diff --git a/dist/modules/engine/engine.cjs b/dist/modules/engine/engine.cjs index f608d3bd2..7f26b1f5b 100644 --- a/dist/modules/engine/engine.cjs +++ b/dist/modules/engine/engine.cjs @@ -1,8 +1,8 @@ /** * Anime.js - engine - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -77,7 +77,7 @@ class Engine extends clock.Clock { wake() { if (this.useDefaultMainLoop && !this.reqId) { - // Imediatly request a tick to update engine._elapsedTime and get accurate offsetPosition calculation in timer.js + // Imediatly request a tick to update engine._lastTickTime and get accurate offsetPosition calculation in timer.js this.requestTick(helpers.now()); this.reqId = engineTickMethod(tickEngine); } diff --git a/dist/modules/engine/engine.js b/dist/modules/engine/engine.js index 993b7f45b..746c3d3e2 100644 --- a/dist/modules/engine/engine.js +++ b/dist/modules/engine/engine.js @@ -1,8 +1,8 @@ /** * Anime.js - engine - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { globalVersions, defaults, globals } from '../core/globals.js'; @@ -75,7 +75,7 @@ class Engine extends Clock { wake() { if (this.useDefaultMainLoop && !this.reqId) { - // Imediatly request a tick to update engine._elapsedTime and get accurate offsetPosition calculation in timer.js + // Imediatly request a tick to update engine._lastTickTime and get accurate offsetPosition calculation in timer.js this.requestTick(now()); this.reqId = engineTickMethod(tickEngine); } diff --git a/dist/modules/engine/index.cjs b/dist/modules/engine/index.cjs index 1db958446..4c5d89afb 100644 --- a/dist/modules/engine/index.cjs +++ b/dist/modules/engine/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - engine - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/engine/index.js b/dist/modules/engine/index.js index e0cfbf195..ad2a5a458 100644 --- a/dist/modules/engine/index.js +++ b/dist/modules/engine/index.js @@ -1,8 +1,8 @@ /** * Anime.js - engine - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { engine } from './engine.js'; diff --git a/dist/modules/events/index.cjs b/dist/modules/events/index.cjs index 5cae2c522..fc83b391c 100644 --- a/dist/modules/events/index.cjs +++ b/dist/modules/events/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - events - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/events/index.js b/dist/modules/events/index.js index 88c9c4d00..b365ab29e 100644 --- a/dist/modules/events/index.js +++ b/dist/modules/events/index.js @@ -1,8 +1,8 @@ /** * Anime.js - events - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { ScrollObserver, onScroll, scrollContainers } from './scroll.js'; diff --git a/dist/modules/events/scroll.cjs b/dist/modules/events/scroll.cjs index 7953e0ac9..3ffa59a51 100644 --- a/dist/modules/events/scroll.cjs +++ b/dist/modules/events/scroll.cjs @@ -1,8 +1,8 @@ /** * Anime.js - events - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -223,6 +223,7 @@ class ScrollContainer { this.updateBounds(); helpers.forEachChildren(this, (/** @type {ScrollObserver} */child) => { child.refresh(); + child.onResize(child); if (child._debug) { child.debug(); } @@ -434,6 +435,8 @@ class ScrollObserver { /** @type {Callback} */ this.onUpdate = parameters.onUpdate || consts.noop; /** @type {Callback} */ + this.onResize = parameters.onResize || consts.noop; + /** @type {Callback} */ this.onSyncComplete = parameters.onSyncComplete || consts.noop; /** @type {Boolean} */ this.reverted = false; @@ -497,7 +500,9 @@ class ScrollObserver { linked.pause(); this.linked = linked; // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. - if (!helpers.isUnd(/** @type {WAAPIAnimation} */(linked))) /** @type {WAAPIAnimation} */(linked).persist = true; + if (!helpers.isUnd(linked) && !helpers.isUnd(/** @type {WAAPIAnimation} */(linked).persist)) { + /** @type {WAAPIAnimation} */(linked).persist = true; + } // Try to use a target of the linked object if no target parameters specified if (!this._params.target) { /** @type {HTMLElement} */ @@ -699,12 +704,11 @@ class ScrollObserver { // let offsetX = 0; // let offsetY = 0; // let $offsetParent = $el; - /** @type {Element} */ if (linked) { linkedTime = linked.currentTime; linked.seek(0, true); } - /* Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 + // Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 // const isContainerStatic = get(container.element, 'position') === 'static' ? set(container.element, { position: 'relative '}) : false; // while ($el && $el !== container.element && $el !== doc.body) { // const isSticky = get($el, 'position') === 'sticky' ? diff --git a/dist/modules/events/scroll.d.ts b/dist/modules/events/scroll.d.ts index 2545f4b7d..399d5ae6c 100644 --- a/dist/modules/events/scroll.d.ts +++ b/dist/modules/events/scroll.d.ts @@ -55,6 +55,8 @@ export class ScrollObserver { /** @type {Callback} */ onUpdate: Callback; /** @type {Callback} */ + onResize: Callback; + /** @type {Callback} */ onSyncComplete: Callback; /** @type {Boolean} */ reverted: boolean; diff --git a/dist/modules/events/scroll.js b/dist/modules/events/scroll.js index 5a746c674..88c29427e 100644 --- a/dist/modules/events/scroll.js +++ b/dist/modules/events/scroll.js @@ -1,8 +1,8 @@ /** * Anime.js - events - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { noop, doc, isDomSymbol, relativeValuesExecRgx, win } from '../core/consts.js'; @@ -221,6 +221,7 @@ class ScrollContainer { this.updateBounds(); forEachChildren(this, (/** @type {ScrollObserver} */child) => { child.refresh(); + child.onResize(child); if (child._debug) { child.debug(); } @@ -432,6 +433,8 @@ class ScrollObserver { /** @type {Callback} */ this.onUpdate = parameters.onUpdate || noop; /** @type {Callback} */ + this.onResize = parameters.onResize || noop; + /** @type {Callback} */ this.onSyncComplete = parameters.onSyncComplete || noop; /** @type {Boolean} */ this.reverted = false; @@ -495,7 +498,9 @@ class ScrollObserver { linked.pause(); this.linked = linked; // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. - if (!isUnd(/** @type {WAAPIAnimation} */(linked))) /** @type {WAAPIAnimation} */(linked).persist = true; + if (!isUnd(linked) && !isUnd(/** @type {WAAPIAnimation} */(linked).persist)) { + /** @type {WAAPIAnimation} */(linked).persist = true; + } // Try to use a target of the linked object if no target parameters specified if (!this._params.target) { /** @type {HTMLElement} */ @@ -697,12 +702,11 @@ class ScrollObserver { // let offsetX = 0; // let offsetY = 0; // let $offsetParent = $el; - /** @type {Element} */ if (linked) { linkedTime = linked.currentTime; linked.seek(0, true); } - /* Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 + // Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 // const isContainerStatic = get(container.element, 'position') === 'static' ? set(container.element, { position: 'relative '}) : false; // while ($el && $el !== container.element && $el !== doc.body) { // const isSticky = get($el, 'position') === 'sticky' ? diff --git a/dist/modules/index.cjs b/dist/modules/index.cjs index 61ea3da11..ace7cd338 100644 --- a/dist/modules/index.cjs +++ b/dist/modules/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -16,16 +16,19 @@ var scope = require('./scope/scope.cjs'); var scroll = require('./events/scroll.cjs'); var engine = require('./engine/engine.cjs'); var index = require('./easings/index.cjs'); +var layout = require('./layout/layout.cjs'); var index$1 = require('./utils/index.cjs'); var index$2 = require('./svg/index.cjs'); var index$3 = require('./text/index.cjs'); var waapi = require('./waapi/waapi.cjs'); +var globals = require('./core/globals.cjs'); var index$4 = require('./easings/cubic-bezier/index.cjs'); var index$5 = require('./easings/steps/index.cjs'); var index$6 = require('./easings/linear/index.cjs'); var index$7 = require('./easings/irregular/index.cjs'); var index$8 = require('./easings/spring/index.cjs'); var parser = require('./easings/eases/parser.cjs'); +var helpers = require('./core/helpers.cjs'); var chainable = require('./utils/chainable.cjs'); var random = require('./utils/random.cjs'); var time = require('./utils/time.cjs'); @@ -37,6 +40,7 @@ var motionpath = require('./svg/motionpath.cjs'); var drawable = require('./svg/drawable.cjs'); var morphto = require('./svg/morphto.cjs'); var split = require('./text/split.cjs'); +var scramble = require('./text/scramble.cjs'); @@ -57,11 +61,14 @@ exports.onScroll = scroll.onScroll; exports.scrollContainers = scroll.scrollContainers; exports.engine = engine.engine; exports.easings = index; +exports.AutoLayout = layout.AutoLayout; +exports.createLayout = layout.createLayout; exports.utils = index$1; exports.svg = index$2; exports.text = index$3; exports.WAAPIAnimation = waapi.WAAPIAnimation; exports.waapi = waapi.waapi; +exports.globals = globals.globals; exports.cubicBezier = index$4.cubicBezier; exports.steps = index$5.steps; exports.linear = index$6.linear; @@ -70,6 +77,9 @@ exports.Spring = index$8.Spring; exports.createSpring = index$8.createSpring; exports.spring = index$8.spring; exports.eases = parser.eases; +exports.addChild = helpers.addChild; +exports.forEachChildren = helpers.forEachChildren; +exports.removeChild = helpers.removeChild; exports.clamp = chainable.clamp; exports.damp = chainable.damp; exports.degToRad = chainable.degToRad; @@ -100,3 +110,4 @@ exports.morphTo = morphto.morphTo; exports.TextSplitter = split.TextSplitter; exports.split = split.split; exports.splitText = split.splitText; +exports.scrambleText = scramble.scrambleText; diff --git a/dist/modules/index.d.ts b/dist/modules/index.d.ts index 36082f821..559f2bab5 100644 --- a/dist/modules/index.d.ts +++ b/dist/modules/index.d.ts @@ -7,6 +7,7 @@ export * from "./scope/index.js"; export * from "./events/index.js"; export * from "./engine/index.js"; export * from "./easings/index.js"; +export * from "./layout/index.js"; export * from "./utils/index.js"; export * from "./svg/index.js"; export * from "./text/index.js"; @@ -16,3 +17,4 @@ export * as easings from "./easings/index.js"; export * as utils from "./utils/index.js"; export * as svg from "./svg/index.js"; export * as text from "./text/index.js"; +export { globals } from "./core/globals.js"; diff --git a/dist/modules/index.js b/dist/modules/index.js index fe5878322..1debd2fb7 100644 --- a/dist/modules/index.js +++ b/dist/modules/index.js @@ -1,8 +1,8 @@ /** * Anime.js - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { Timer, createTimer } from './timer/timer.js'; @@ -15,6 +15,7 @@ export { ScrollObserver, onScroll, scrollContainers } from './events/scroll.js'; export { engine } from './engine/engine.js'; import * as index from './easings/index.js'; export { index as easings }; +export { AutoLayout, createLayout } from './layout/layout.js'; import * as index$1 from './utils/index.js'; export { index$1 as utils }; import * as index$2 from './svg/index.js'; @@ -22,12 +23,14 @@ export { index$2 as svg }; import * as index$3 from './text/index.js'; export { index$3 as text }; export { WAAPIAnimation, waapi } from './waapi/waapi.js'; +export { globals } from './core/globals.js'; export { cubicBezier } from './easings/cubic-bezier/index.js'; export { steps } from './easings/steps/index.js'; export { linear } from './easings/linear/index.js'; export { irregular } from './easings/irregular/index.js'; export { Spring, createSpring, spring } from './easings/spring/index.js'; export { eases } from './easings/eases/parser.js'; +export { addChild, forEachChildren, removeChild } from './core/helpers.js'; export { clamp, damp, degToRad, lerp, mapRange, padEnd, padStart, radToDeg, round, roundPad, snap, wrap } from './utils/chainable.js'; export { createSeededRandom, random, randomPick, shuffle } from './utils/random.js'; export { keepTime, sync } from './utils/time.js'; @@ -39,3 +42,4 @@ export { createMotionPath } from './svg/motionpath.js'; export { createDrawable } from './svg/drawable.js'; export { morphTo } from './svg/morphto.js'; export { TextSplitter, split, splitText } from './text/split.js'; +export { scrambleText } from './text/scramble.js'; diff --git a/dist/modules/layout/index.cjs b/dist/modules/layout/index.cjs new file mode 100644 index 000000000..73cee66fb --- /dev/null +++ b/dist/modules/layout/index.cjs @@ -0,0 +1,15 @@ +/** + * Anime.js - layout - CJS + * @version v4.4.1 + * @license MIT + * @copyright 2026 - Julian Garnier + */ + +'use strict'; + +var layout = require('./layout.cjs'); + + + +exports.AutoLayout = layout.AutoLayout; +exports.createLayout = layout.createLayout; diff --git a/dist/modules/layout/index.d.ts b/dist/modules/layout/index.d.ts new file mode 100644 index 000000000..ed4c2a724 --- /dev/null +++ b/dist/modules/layout/index.d.ts @@ -0,0 +1 @@ +export * from "./layout.js"; diff --git a/dist/modules/layout/index.js b/dist/modules/layout/index.js new file mode 100644 index 000000000..c7402a119 --- /dev/null +++ b/dist/modules/layout/index.js @@ -0,0 +1,8 @@ +/** + * Anime.js - layout - ESM + * @version v4.4.1 + * @license MIT + * @copyright 2026 - Julian Garnier + */ + +export { AutoLayout, createLayout } from './layout.js'; diff --git a/dist/modules/layout/layout.cjs b/dist/modules/layout/layout.cjs new file mode 100644 index 000000000..671fe78df --- /dev/null +++ b/dist/modules/layout/layout.cjs @@ -0,0 +1,1596 @@ +/** + * Anime.js - layout - CJS + * @version v4.4.1 + * @license MIT + * @copyright 2026 - Julian Garnier + */ + +'use strict'; + +var helpers = require('../core/helpers.cjs'); +var targets = require('../core/targets.cjs'); +var parser = require('../easings/eases/parser.cjs'); +var values = require('../core/values.cjs'); +var timeline = require('../timeline/timeline.cjs'); +var waapi = require('../waapi/waapi.cjs'); +var globals = require('../core/globals.cjs'); + +/** + * @import { + * AnimationParams, + * RenderableCallbacks, + * TickableCallbacks, + * TimelineParams, + * TimerParams, + * } from '../types/index.js' +*/ + +/** + * @import { + * ScrollObserver, + * } from '../events/scroll.js' +*/ + +/** + * @import { + * Timeline, + * } from '../timeline/timeline.js' +*/ + +/** + * @import { + * WAAPIAnimation + * } from '../waapi/waapi.js' +*/ + +/** + * @import { + * Spring, + } from '../easings/spring/index.js' +*/ + +/** + * @import { + * DOMTarget, + * DOMTargetSelector, + * FunctionValue, + * EasingParam, + } from '../types/index.js' +*/ + +/** + * @typedef {DOMTargetSelector|Array} LayoutChildrenParam + */ + +/** + * @typedef {Object} LayoutAnimationTimingsParams + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] + */ + +/** + * @typedef {Record} LayoutStateAnimationProperties + */ + +/** + * @typedef {LayoutStateAnimationProperties & LayoutAnimationTimingsParams} LayoutStateParams + */ + +/** + * @typedef {Object} LayoutSpecificAnimationParams + * @property {Number|String} [id] + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] + * @property {EasingParam} [playbackEase] + * @property {LayoutStateParams} [swapAt] + * @property {LayoutStateParams} [enterFrom] + * @property {LayoutStateParams} [leaveTo] + */ + +/** + * @typedef {LayoutSpecificAnimationParams & TimerParams & TickableCallbacks & RenderableCallbacks} LayoutAnimationParams + */ + +/** + * @typedef {Object} LayoutOptions + * @property {LayoutChildrenParam} [children] + * @property {Array} [properties] + */ + +/** + * @typedef {LayoutAnimationParams & LayoutOptions} AutoLayoutParams + */ + +/** + * @typedef {Record & { + * transform: String, + * x: Number, + * y: Number, + * left: Number, + * top: Number, + * clientLeft: Number, + * clientTop: Number, + * width: Number, + * height: Number, + * }} LayoutNodeProperties + */ + +/** + * @typedef {Object} LayoutNode + * @property {String} id + * @property {DOMTarget} $el + * @property {Number} index + * @property {Array} targets + * @property {Number} delay + * @property {Number} duration + * @property {EasingParam} ease + * @property {DOMTarget} $measure + * @property {LayoutSnapshot} state + * @property {AutoLayout} layout + * @property {LayoutNode|null} parentNode + * @property {Boolean} isTarget + * @property {Boolean} isEntering + * @property {Boolean} isLeaving + * @property {Boolean} hasTransform + * @property {Array} inlineStyles + * @property {String|null} inlineTransforms + * @property {String|null} inlineTransition + * @property {Boolean} branchAdded + * @property {Boolean} branchRemoved + * @property {Boolean} branchNotRendered + * @property {Boolean} sizeChanged + * @property {Boolean} isInlined + * @property {Boolean} hasVisibilitySwap + * @property {Boolean} hasDisplayNone + * @property {Boolean} hasVisibilityHidden + * @property {String|null} measuredInlineTransform + * @property {String|null} measuredInlineTransition + * @property {String|null} measuredDisplay + * @property {String|null} measuredVisibility + * @property {String|null} measuredPosition + * @property {Boolean} measuredHasDisplayNone + * @property {Boolean} measuredHasVisibilityHidden + * @property {Boolean} measuredIsVisible + * @property {Boolean} measuredIsRemoved + * @property {Boolean} measuredIsInsideRoot + * @property {LayoutNodeProperties} properties + * @property {LayoutNode|null} _head + * @property {LayoutNode|null} _tail + * @property {LayoutNode|null} _prev + * @property {LayoutNode|null} _next + */ + +/** + * @callback LayoutNodeIterator + * @param {LayoutNode} node + * @param {Number} index + * @return {void} + */ + +let layoutId = 0; +let nodeId = 0; + +/** + * @param {DOMTarget} root + * @param {DOMTarget} $el + * @return {Boolean} + */ +const isElementInRoot = (root, $el) => { + if (!root || !$el) return false; + return root === $el || root.contains($el); +}; + +/** + * @param {DOMTarget|null} $el + * @return {String|null} + */ +const muteElementTransition = $el => { + if (!$el) return null; + const style = $el.style; + const transition = style.transition || ''; + style.setProperty('transition', 'none', 'important'); + return transition; +}; + +/** + * @param {DOMTarget|null} $el + * @param {String|null} transition + */ +const restoreElementTransition = ($el, transition) => { + if (!$el) return; + const style = $el.style; + if (transition) { + style.transition = transition; + } else { + style.removeProperty('transition'); + } +}; + +/** + * @param {LayoutNode} node + */ +const muteNodeTransition = node => { + const store = node.layout.transitionMuteStore; + const $el = node.$el; + const $measure = node.$measure; + if ($el && !store.has($el)) store.set($el, muteElementTransition($el)); + if ($measure && !store.has($measure)) store.set($measure, muteElementTransition($measure)); +}; + +/** + * @param {Map} store + */ +const restoreLayoutTransition = store => { + store.forEach((value, $el) => restoreElementTransition($el, value)); + store.clear(); +}; + +const hiddenComputedStyle = /** @type {CSSStyleDeclaration} */({ + display: 'none', + visibility: 'hidden', + opacity: '0', + transform: 'none', + position: 'static', +}); + +/** + * @param {LayoutNode|null} node + */ +const detachNode = node => { + if (!node) return; + const parent = node.parentNode; + if (!parent) return; + if (parent._head === node) parent._head = node._next; + if (parent._tail === node) parent._tail = node._prev; + if (node._prev) node._prev._next = node._next; + if (node._next) node._next._prev = node._prev; + node._prev = null; + node._next = null; + node.parentNode = null; +}; + +/** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @param {LayoutSnapshot} state + * @param {LayoutNode} recycledNode + * @return {LayoutNode} + */ +const createNode = ($el, parentNode, state, recycledNode) => { + let dataId = $el.dataset.layoutId; + if (!dataId) dataId = $el.dataset.layoutId = `node-${nodeId++}`; + const node = recycledNode ? recycledNode : /** @type {LayoutNode} */({}); + node.$el = $el; + node.$measure = $el; + node.id = dataId; + node.index = 0; + node.targets = null; + node.delay = 0; + node.duration = 0; + node.ease = null; + node.state = state; + node.layout = state.layout; + node.parentNode = parentNode || null; + node.isTarget = false; + node.isEntering = false; + node.isLeaving = false; + node.isInlined = false; + node.hasTransform = false; + node.inlineStyles = []; + node.inlineTransforms = null; + node.inlineTransition = null; + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.sizeChanged = false; + node.hasVisibilitySwap = false; + node.hasDisplayNone = false; + node.hasVisibilityHidden = false; + node.measuredInlineTransform = null; + node.measuredInlineTransition = null; + node.measuredDisplay = null; + node.measuredVisibility = null; + node.measuredPosition = null; + node.measuredHasDisplayNone = false; + node.measuredHasVisibilityHidden = false; + node.measuredIsVisible = false; + node.measuredIsRemoved = false; + node.measuredIsInsideRoot = false; + node.properties = /** @type {LayoutNodeProperties} */({ + transform: 'none', + x: 0, + y: 0, + left: 0, + top: 0, + clientLeft: 0, + clientTop: 0, + width: 0, + height: 0, + }); + node.layout.properties.forEach(prop => node.properties[prop] = 0); + node._head = null; + node._tail = null; + node._prev = null; + node._next = null; + return node; +}; + +/** + * @param {LayoutNode} node + * @param {DOMTarget} $measure + * @param {CSSStyleDeclaration} computedStyle + * @param {Boolean} skipMeasurements + * @return {LayoutNode} + */ +const recordNodeState = (node, $measure, computedStyle, skipMeasurements) => { + const $el = node.$el; + const root = node.layout.root; + const isRoot = root === $el; + const properties = node.properties; + const rootNode = node.state.rootNode; + const parentNode = node.parentNode; + const computedTransforms = computedStyle.transform; + const inlineTransforms = $el.style.transform; + const parentNotRendered = parentNode ? parentNode.measuredIsRemoved : false; + const position = computedStyle.position; + if (isRoot) node.layout.absoluteCoords = position === 'fixed' || position === 'absolute'; + node.$measure = $measure; + node.inlineTransforms = inlineTransforms; + node.hasTransform = computedTransforms && computedTransforms !== 'none'; + node.measuredIsInsideRoot = isElementInRoot(root, $measure); + node.measuredInlineTransform = null; + node.measuredDisplay = computedStyle.display; + node.measuredVisibility = computedStyle.visibility; + node.measuredPosition = position; + node.measuredHasDisplayNone = computedStyle.display === 'none'; + node.measuredHasVisibilityHidden = computedStyle.visibility === 'hidden'; + node.measuredIsVisible = !(node.measuredHasDisplayNone || node.measuredHasVisibilityHidden); + node.measuredIsRemoved = node.measuredHasDisplayNone || node.measuredHasVisibilityHidden || parentNotRendered; + // Check if element has adjacent text that would reflow when taken out of flow + let hasAdjacentText = false; + let s = $el.previousSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.previousSibling; + if (s && s.nodeType === Node.TEXT_NODE) { + hasAdjacentText = true; + } else { + s = $el.nextSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.nextSibling; + hasAdjacentText = s !== null && s.nodeType === Node.TEXT_NODE; + } + node.isInlined = hasAdjacentText; + + // Mute transforms (and transition to avoid triggering an animation) before the position calculation + if (node.hasTransform && !skipMeasurements) { + const transitionMuteStore = node.layout.transitionMuteStore; + if (!transitionMuteStore.get($el)) node.inlineTransition = muteElementTransition($el); + if ($measure === $el) { + $el.style.transform = 'none'; + } else { + if (!transitionMuteStore.get($measure)) node.measuredInlineTransition = muteElementTransition($measure); + node.measuredInlineTransform = $measure.style.transform; + $measure.style.transform = 'none'; + } + } + + let left = 0; + let top = 0; + let width = 0; + let height = 0; + + if (!skipMeasurements) { + const rect = $measure.getBoundingClientRect(); + left = rect.left; + top = rect.top; + width = rect.width; + height = rect.height; + } + + for (let name in properties) { + const computedProp = name === 'transform' ? computedTransforms : computedStyle[name] || (computedStyle.getPropertyValue && computedStyle.getPropertyValue(name)); + if (!helpers.isUnd(computedProp)) properties[name] = computedProp; + } + + properties.left = left; + properties.top = top; + properties.clientLeft = skipMeasurements ? 0 : $measure.clientLeft; + properties.clientTop = skipMeasurements ? 0 : $measure.clientTop; + // Compute local x/y relative to parent + let absoluteLeft, absoluteTop; + if (isRoot) { + if (!node.layout.absoluteCoords) { + absoluteLeft = 0; + absoluteTop = 0; + } else { + absoluteLeft = left; + absoluteTop = top; + } + } else { + const p = parentNode || rootNode; + const parentLeft = p.properties.left; + const parentTop = p.properties.top; + const borderLeft = p.properties.clientLeft; + const borderTop = p.properties.clientTop; + if (!node.layout.absoluteCoords) { + if (p === rootNode) { + const rootLeft = rootNode.properties.left; + const rootTop = rootNode.properties.top; + const rootBorderLeft = rootNode.properties.clientLeft; + const rootBorderTop = rootNode.properties.clientTop; + absoluteLeft = left - rootLeft - rootBorderLeft; + absoluteTop = top - rootTop - rootBorderTop; + } else { + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; + } + } else { + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; + } + } + properties.x = absoluteLeft; + properties.y = absoluteTop; + properties.width = width; + properties.height = height; + return node; +}; + +/** + * @param {LayoutNode} node + * @param {LayoutStateAnimationProperties} [props] + */ +const updateNodeProperties = (node, props) => { + if (!props) return; + for (let name in props) { + node.properties[name] = props[name]; + } +}; + +/** + * @param {LayoutNode} node + * @param {LayoutAnimationTimingsParams} params + */ +const updateNodeTimingParams = (node, params) => { + const easeFunctionResult = values.getFunctionValue(params.ease, node.$el, node.index, node.targets, null, null); + const keyEasing = helpers.isFnc(easeFunctionResult) ? easeFunctionResult : params.ease; + const hasSpring = !helpers.isUnd(keyEasing) && !helpers.isUnd(/** @type {Spring} */(keyEasing).ease); + node.ease = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; + node.duration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : values.getFunctionValue(params.duration, node.$el, node.index, node.targets, null, null); + node.delay = values.getFunctionValue(params.delay, node.$el, node.index, node.targets, null, null); +}; + +/** + * @param {LayoutNode} node + */ +const recordNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + stylesStore.length = 0; + node.layout.recordedProperties.forEach(prop => { + stylesStore.push(prop, style[prop] || ''); + }); +}; + +/** + * @param {LayoutNode} node + */ +const restoreNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + for (let i = 0, l = stylesStore.length; i < l; i += 2) { + const property = stylesStore[i]; + const styleValue = stylesStore[i + 1]; + if (styleValue && styleValue !== '') { + style[property] = styleValue; + } else { + style[property] = ''; + style.removeProperty(property); + } + } +}; + +/** + * @param {LayoutNode} node + */ +const restoreNodeTransform = node => { + const inlineTransforms = node.inlineTransforms; + const nodeStyle = node.$el.style; + if (!node.hasTransform || !inlineTransforms || (node.hasTransform && nodeStyle.transform === 'none') || (inlineTransforms && inlineTransforms === 'none')) { + nodeStyle.removeProperty('transform'); + } else if (inlineTransforms) { + nodeStyle.transform = inlineTransforms; + } + const $measure = node.$measure; + if (node.hasTransform && $measure !== node.$el) { + const measuredStyle = $measure.style; + const measuredInline = node.measuredInlineTransform; + if (measuredInline && measuredInline !== '') { + measuredStyle.transform = measuredInline; + } else { + measuredStyle.removeProperty('transform'); + } + } + node.measuredInlineTransform = null; + if (node.inlineTransition !== null) { + restoreElementTransition(node.$el, node.inlineTransition); + node.inlineTransition = null; + } + if ($measure !== node.$el && node.measuredInlineTransition !== null) { + restoreElementTransition($measure, node.measuredInlineTransition); + node.measuredInlineTransition = null; + } +}; + +/** + * @param {LayoutNode} node + */ +const restoreNodeVisualState = node => { + if (node.measuredIsRemoved || node.hasVisibilitySwap) { + node.$el.style.removeProperty('display'); + node.$el.style.removeProperty('visibility'); + if (node.hasVisibilitySwap) { + node.$measure.style.removeProperty('display'); + node.$measure.style.removeProperty('visibility'); + } + } + // if (node.measuredIsRemoved) { + node.layout.pendingRemoval.delete(node.$el); + // } +}; + +/** + * @param {LayoutNode} node + * @param {LayoutNode} targetNode + * @param {LayoutSnapshot} newState + * @return {LayoutNode} + */ +const cloneNodeProperties = (node, targetNode, newState) => { + targetNode.properties = /** @type {LayoutNodeProperties} */({ ...node.properties }); + targetNode.state = newState; + targetNode.isTarget = node.isTarget; + targetNode.hasTransform = node.hasTransform; + targetNode.inlineTransforms = node.inlineTransforms; + targetNode.measuredIsVisible = node.measuredIsVisible; + targetNode.measuredDisplay = node.measuredDisplay; + targetNode.measuredIsRemoved = node.measuredIsRemoved; + targetNode.measuredHasDisplayNone = node.measuredHasDisplayNone; + targetNode.measuredHasVisibilityHidden = node.measuredHasVisibilityHidden; + targetNode.hasDisplayNone = node.hasDisplayNone; + targetNode.isInlined = node.isInlined; + targetNode.hasVisibilityHidden = node.hasVisibilityHidden; + return targetNode; +}; + +class LayoutSnapshot { + /** + * @param {AutoLayout} layout + */ + constructor(layout) { + /** @type {AutoLayout} */ + this.layout = layout; + /** @type {LayoutNode|null} */ + this.rootNode = null; + /** @type {Set} */ + this.rootNodes = new Set(); + /** @type {Map} */ + this.nodes = new Map(); + /** @type {Number} */ + this.scrollX = 0; + /** @type {Number} */ + this.scrollY = 0; + } + + /** + * @return {this} + */ + revert() { + this.forEachNode(node => { + this.layout.pendingRemoval.delete(node.$el); + node.$el.removeAttribute('data-layout-id'); + node.$measure.removeAttribute('data-layout-id'); + }); + this.rootNode = null; + this.rootNodes.clear(); + this.nodes.clear(); + return this; + } + + /** + * @param {DOMTarget} $el + * @return {LayoutNode} + */ + getNode($el) { + if (!$el || !$el.dataset) return; + return this.nodes.get($el.dataset.layoutId); + } + + /** + * @param {DOMTarget} $el + * @param {String} prop + * @return {Number|String} + */ + getComputedValue($el, prop) { + const node = this.getNode($el); + if (!node) return; + return /** @type {Number|String} */(node.properties[prop]); + } + + /** + * @param {LayoutNode|null} rootNode + * @param {LayoutNodeIterator} cb + */ + forEach(rootNode, cb) { + let node = rootNode; + let i = 0; + while (node) { + cb(node, i++); + if (node._head) { + node = node._head; + } else if (node._next) { + node = node._next; + } else { + while (node && !node._next) { + node = node.parentNode; + } + if (node) node = node._next; + } + } + } + + /** + * @param {LayoutNodeIterator} cb + */ + forEachRootNode(cb) { + this.forEach(this.rootNode, cb); + } + + /** + * @param {LayoutNodeIterator} cb + */ + forEachNode(cb) { + for (const rootNode of this.rootNodes) { + this.forEach(rootNode, cb); + } + } + + /** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @return {LayoutNode|null} + */ + registerElement($el, parentNode) { + if (!$el || $el.nodeType !== 1) return null; + + if (!this.layout.transitionMuteStore.has($el)) this.layout.transitionMuteStore.set($el, muteElementTransition($el)); + + /** @type {Array} */ + const stack = [$el, parentNode]; + const root = this.layout.root; + let firstNode = null; + + while (stack.length) { + /** @type {LayoutNode|null} */ + const $parent = /** @type {LayoutNode|null} */(stack.pop()); + /** @type {DOMTarget|null} */ + const $current = /** @type {DOMTarget|null} */(stack.pop()); + + if (!$current || $current.nodeType !== 1 || helpers.isSvg($current)) continue; + + const skipMeasurements = $parent ? $parent.measuredIsRemoved : false; + const computedStyle = skipMeasurements ? hiddenComputedStyle : getComputedStyle($current); + const hasDisplayNone = skipMeasurements ? true : computedStyle.display === 'none'; + const hasVisibilityHidden = skipMeasurements ? true : computedStyle.visibility === 'hidden'; + const isVisible = !hasDisplayNone && !hasVisibilityHidden; + const existingId = $current.dataset.layoutId; + const isInsideRoot = isElementInRoot(root, $current); + + let node = existingId ? this.nodes.get(existingId) : null; + + if (node && node.$el !== $current) { + const nodeInsideRoot = isElementInRoot(root, node.$el); + const measuredVisible = node.measuredIsVisible; + const shouldReassignNode = !nodeInsideRoot && (isInsideRoot || (!isInsideRoot && !measuredVisible && isVisible)); + const shouldReuseMeasurements = nodeInsideRoot && !measuredVisible && isVisible; + // Rebind nodes that move into the root or whose detached twin just became visible + if (shouldReassignNode) { + detachNode(node); + node = createNode($current, $parent, this, node); + // for hidden element with in-root sibling, keep the hidden node but borrow measurements from its visible in-root twin element + } else if (shouldReuseMeasurements) { + recordNodeState(node, $current, computedStyle, skipMeasurements); + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; + } + if (!firstNode) firstNode = node; + continue; + // No reassignment needed so keep walking descendants under the current parent + } else { + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), $parent); + $child = $child.previousElementSibling; + } + if (!firstNode) firstNode = node; + continue; + } + } else { + node = createNode($current, $parent, this, node); + } + + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.isTarget = false; + node.sizeChanged = false; + node.hasVisibilityHidden = hasVisibilityHidden; + node.hasDisplayNone = hasDisplayNone; + node.hasVisibilitySwap = (hasVisibilityHidden && !node.measuredHasVisibilityHidden) || (hasDisplayNone && !node.measuredHasDisplayNone); + + this.nodes.set(node.id, node); + + node.parentNode = $parent || null; + node._prev = null; + node._next = null; + + if ($parent) { + this.rootNodes.delete(node); + if (!$parent._head) { + $parent._head = node; + $parent._tail = node; + } else { + $parent._tail._next = node; + node._prev = $parent._tail; + $parent._tail = node; + } + } else { + // Each disconnected subtree becomes its own root in the snapshot graph + this.rootNodes.add(node); + } + + recordNodeState(node, node.$el, computedStyle, skipMeasurements); + + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; + } + + if (!firstNode) firstNode = node; + } + + return firstNode; + } + + /** + * @param {DOMTarget} $el + * @param {Set} candidates + * @return {LayoutNode|null} + */ + ensureDetachedNode($el, candidates) { + if (!$el || $el === this.layout.root) return null; + const existingId = $el.dataset.layoutId; + const existingNode = existingId ? this.nodes.get(existingId) : null; + if (existingNode && existingNode.$el === $el) return existingNode; + let parentNode = null; + let $ancestor = $el.parentElement; + while ($ancestor && $ancestor !== this.layout.root) { + if (candidates.has($ancestor)) { + parentNode = this.ensureDetachedNode($ancestor, candidates); + break; + } + $ancestor = $ancestor.parentElement; + } + return this.registerElement($el, parentNode); + } + + /** + * @return {this} + */ + record() { + const layout = this.layout; + const children = layout.children; + const root = layout.root; + const toParse = helpers.isArr(children) ? children : [children]; + const scoped = []; + const scopeRoot = children === '*' ? root : globals.scope.root; + + // Mute transition and transforms of root ancestors before recording the state + + /** @type {Array} */ + const rootAncestorTransformStore = []; + let $ancestor = root.parentElement; + while ($ancestor && $ancestor.nodeType === 1) { + const computedStyle = getComputedStyle($ancestor); + if (computedStyle.transform && computedStyle.transform !== 'none') { + const inlineTransform = $ancestor.style.transform || ''; + const inlineTransition = muteElementTransition($ancestor); + rootAncestorTransformStore.push($ancestor, inlineTransform, inlineTransition); + $ancestor.style.transform = 'none'; + } + $ancestor = $ancestor.parentElement; + } + + for (let i = 0, l = toParse.length; i < l; i++) { + const child = toParse[i]; + scoped[i] = helpers.isStr(child) ? scopeRoot.querySelectorAll(child) : child; + } + + const parsedChildren = targets.registerTargets(scoped); + + this.nodes.clear(); + this.rootNodes.clear(); + + const rootNode = this.registerElement(root, null); + // Root node are always targets + rootNode.isTarget = true; + this.rootNode = rootNode; + + const inRootNodeIds = new Set(); + // Update index and total for inital timing calculation + let index = 0; + const allNodeTargets = []; + this.nodes.forEach((node) => { allNodeTargets.push(node.$el); }); + this.nodes.forEach((node, id) => { + node.index = index++; + node.targets = allNodeTargets; + // Track ids of nodes that belong to the current root to filter detached matches + if (node && node.measuredIsInsideRoot) { + inRootNodeIds.add(id); + } + }); + + // Elements with a layout id outside the root that match the children selector + const detachedElementsLookup = new Set(); + const orderedDetachedElements = []; + + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + if (!$el || $el.nodeType !== 1 || $el === root) continue; + const insideRoot = isElementInRoot(root, $el); + if (!insideRoot) { + const layoutNodeId = $el.dataset.layoutId; + if (!layoutNodeId || !inRootNodeIds.has(layoutNodeId)) continue; + } + if (!detachedElementsLookup.has($el)) { + detachedElementsLookup.add($el); + orderedDetachedElements.push($el); + } + } + + for (let i = 0, l = orderedDetachedElements.length; i < l; i++) { + this.ensureDetachedNode(orderedDetachedElements[i], detachedElementsLookup); + } + + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + const node = this.getNode($el); + if (node) { + let cur = node; + while (cur) { + if (cur.isTarget) break; + cur.isTarget = true; + cur = cur.parentNode; + } + } + } + + this.scrollX = window.scrollX; + this.scrollY = window.scrollY; + + this.forEachNode(restoreNodeTransform); + + // Restore transition and transforms of root ancestors + + for (let i = 0, l = rootAncestorTransformStore.length; i < l; i += 3) { + const $el = /** @type {DOMTarget} */(rootAncestorTransformStore[i]); + const inlineTransform = /** @type {String} */(rootAncestorTransformStore[i + 1]); + const inlineTransition = /** @type {String|null} */(rootAncestorTransformStore[i + 2]); + if (inlineTransform && inlineTransform !== '') { + $el.style.transform = inlineTransform; + } else { + $el.style.removeProperty('transform'); + } + restoreElementTransition($el, inlineTransition); + } + + return this; + } +} + +/** + * @param {LayoutStateParams} params + * @return {[LayoutStateAnimationProperties, LayoutAnimationTimingsParams]} + */ +function splitPropertiesFromParams(params) { + /** @type {LayoutStateAnimationProperties} */ + const properties = {}; + /** @type {LayoutAnimationTimingsParams} */ + const parameters = {}; + for (let name in params) { + const value = params[name]; + const isEase = name === 'ease'; + const isTiming = name === 'duration' || name === 'delay'; + if (isTiming || isEase) { + if (isEase) { + parameters[name] = /** @type {EasingParam} */(value); + } else { + parameters[name] = /** @type {Number|FunctionValue} */(value); + } + } else { + properties[name] = /** @type {Number|String} */(value); + } + } + return [properties, parameters]; +} + +class AutoLayout { + /** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + */ + constructor(root, params = {}) { + if (globals.scope.current) globals.scope.current.register(this); + const swapAtSplitParams = splitPropertiesFromParams(params.swapAt); + const enterFromSplitParams = splitPropertiesFromParams(params.enterFrom); + const leaveToSplitParams = splitPropertiesFromParams(params.leaveTo); + const transitionProperties = params.properties; + /** @type {Number|FunctionValue} */ + params.duration = values.setValue(params.duration, 350); + /** @type {Number|FunctionValue} */ + params.delay = values.setValue(params.delay, 0); + /** @type {EasingParam|FunctionValue} */ + params.ease = values.setValue(params.ease, 'inOut(3.5)'); + /** @type {AutoLayoutParams} */ + this.params = params; + /** @type {DOMTarget} */ + this.root = /** @type {DOMTarget} */(targets.registerTargets(root)[0]); + /** @type {Number|String} */ + this.id = params.id || layoutId++; + /** @type {LayoutChildrenParam} */ + this.children = params.children || '*'; + /** @type {Boolean} */ + this.absoluteCoords = false; + /** @type {LayoutStateParams} */ + this.swapAtParams = helpers.mergeObjects(params.swapAt || { opacity: 0 }, { ease: 'inOut(1.75)' }); + /** @type {LayoutStateParams} */ + this.enterFromParams = params.enterFrom || { opacity: 0 }; + /** @type {LayoutStateParams} */ + this.leaveToParams = params.leaveTo || { opacity: 0 }; + /** @type {Set} */ + this.properties = new Set([ + 'opacity', + 'fontSize', + 'color', + 'backgroundColor', + 'borderRadius', + 'border', + 'filter', + 'clipPath', + ]); + if (swapAtSplitParams[0]) for (let name in swapAtSplitParams[0]) this.properties.add(name); + if (enterFromSplitParams[0]) for (let name in enterFromSplitParams[0]) this.properties.add(name); + if (leaveToSplitParams[0]) for (let name in leaveToSplitParams[0]) this.properties.add(name); + if (transitionProperties) for (let i = 0, l = transitionProperties.length; i < l; i++) this.properties.add(transitionProperties[i]); + /** @type {Set} */ + this.recordedProperties = new Set([ + 'display', + 'visibility', + 'translate', + 'position', + 'left', + 'top', + 'marginLeft', + 'marginTop', + 'width', + 'height', + 'maxWidth', + 'maxHeight', + 'minWidth', + 'minHeight', + ]); + this.properties.forEach(prop => this.recordedProperties.add(prop)); + /** @type {WeakSet} */ + this.pendingRemoval = new WeakSet(); + /** @type {Map} */ + this.transitionMuteStore = new Map(); + /** @type {LayoutSnapshot} */ + this.oldState = new LayoutSnapshot(this); + /** @type {LayoutSnapshot} */ + this.newState = new LayoutSnapshot(this); + /** @type {Timeline} */ + this.timeline = null; + /** @type {WAAPIAnimation} */ + this.transformAnimation = null; + /** @type {Array} */ + this.animating = []; + /** @type {Array} */ + this.swapping = []; + /** @type {Array} */ + this.leaving = []; + /** @type {Array} */ + this.entering = []; + // Record the current state as the old state to init the data attributes and allow imediate .animate() + this.oldState.record(); + // And all layout transition muted during the record + restoreLayoutTransition(this.transitionMuteStore); + } + + /** + * @return {this} + */ + revert() { + this.root.classList.remove('is-animated'); + if (this.timeline) { + this.timeline.complete(); + this.timeline = null; + } + if (this.transformAnimation) { + this.transformAnimation.complete(); + this.transformAnimation = null; + } + this.animating.length = this.swapping.length = this.leaving.length = this.entering.length = 0; + this.oldState.revert(); + this.newState.revert(); + requestAnimationFrame(() => restoreLayoutTransition(this.transitionMuteStore)); + return this; + } + + /** + * @return {this} + */ + record() { + // Commit transforms before measuring + if (this.transformAnimation) { + this.transformAnimation.cancel(); + this.transformAnimation = null; + } + // Record the old state + this.oldState.record(); + // Cancel any running timeline + if (this.timeline) { + this.timeline.cancel(); + this.timeline = null; + } + // Restore previously captured inline styles + this.newState.forEachRootNode(restoreNodeInlineStyles); + return this; + } + + /** + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + animate(params = {}) { + /** @type { LayoutAnimationTimingsParams } */ + const animationTimings = { + ease: values.setValue(params.ease, this.params.ease), + delay: values.setValue(params.delay, this.params.delay), + duration: values.setValue(params.duration, this.params.duration), + }; + /** @type {TimelineParams} */ + const tlParams = { + id: this.id + }; + const onComplete = values.setValue(params.onComplete, this.params.onComplete); + const onPause = values.setValue(params.onPause, this.params.onPause); + for (let name in globals.defaults) { + if (name !== 'ease' && name !== 'duration' && name !== 'delay') { + if (!helpers.isUnd(params[name])) { + tlParams[name] = params[name]; + } else if (!helpers.isUnd(this.params[name])) { + tlParams[name] = this.params[name]; + } + } + } + tlParams.onComplete = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const ed = globals.globals.editor; + const isScrollControled = (ap && ap.linked) || (ed && ed.showPanel); + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + return; + } + // Make sure to call .cancel() after restoreNodeInlineStyles(node); otehrwise the commited styles get reverted + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(node => { + restoreNodeVisualState(node); + restoreNodeInlineStyles(node); + }); + for (let i = 0, l = transformed.length; i < l; i++) { + const $el = transformed[i]; + $el.style.transform = newState.getComputedValue($el, 'transform'); + } + if (this.root.classList.contains('is-animated')) { + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + } + // Avoid CSS transitions at the end of the animation by restoring them on the next frame + requestAnimationFrame(() => { + if (this.root.classList.contains('is-animated')) return; + restoreLayoutTransition(this.transitionMuteStore); + }); + }; + tlParams.onPause = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const isScrollControled = ap && ap.linked; + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + return; + } + if (!this.root.classList.contains('is-animated')) return; + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(restoreNodeVisualState); + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + }; + tlParams.composition = false; + + const swapAtParams = helpers.mergeObjects(helpers.mergeObjects(params.swapAt || {}, this.swapAtParams), animationTimings); + const enterFromParams = helpers.mergeObjects(helpers.mergeObjects(params.enterFrom || {}, this.enterFromParams), animationTimings); + const leaveToParams = helpers.mergeObjects(helpers.mergeObjects(params.leaveTo || {}, this.leaveToParams), animationTimings); + const [ swapAtProps, swapAtTimings ] = splitPropertiesFromParams(swapAtParams); + const [ enterFromProps, enterFromTimings ] = splitPropertiesFromParams(enterFromParams); + const [ leaveToProps, leaveToTimings ] = splitPropertiesFromParams(leaveToParams); + + const oldState = this.oldState; + const newState = this.newState; + const animating = this.animating; + const swapping = this.swapping; + const entering = this.entering; + const leaving = this.leaving; + const pendingRemoval = this.pendingRemoval; + + animating.length = swapping.length = entering.length = leaving.length = 0; + + // Mute old state CSS transitions to prevent wrong properties calculation + oldState.forEachRootNode(muteNodeTransition); + // Capture the new state before animation + newState.record(); + newState.forEachRootNode(recordNodeInlineStyles); + + const targets = []; + const animated = []; + const transformed = []; + const animatedSwap = []; + const rootNode = newState.rootNode; + const $root = rootNode.$el; + + newState.forEachRootNode(node => { + + const $el = node.$el; + const id = node.id; + const parent = node.parentNode; + const parentAdded = parent ? parent.branchAdded : false; + const parentRemoved = parent ? parent.branchRemoved : false; + const parentNotRendered = parent ? parent.branchNotRendered : false; + + let oldStateNode = oldState.nodes.get(id); + + const hasNoOldState = !oldStateNode; + + if (hasNoOldState) { + oldStateNode = cloneNodeProperties(node, /** @type {LayoutNode} */({}), oldState); + oldState.nodes.set(id, oldStateNode); + oldStateNode.measuredIsRemoved = true; + } else if (oldStateNode.measuredIsRemoved && !node.measuredIsRemoved) { + cloneNodeProperties(node, oldStateNode, oldState); + oldStateNode.measuredIsRemoved = true; + } + + const oldParentNode = oldStateNode.parentNode; + const oldParentId = oldParentNode ? oldParentNode.id : null; + const newParentId = parent ? parent.id : null; + const parentChanged = oldParentId !== newParentId; + const elementChanged = oldStateNode.$el !== node.$el; + const wasRemovedBefore = oldStateNode.measuredIsRemoved; + const isRemovedNow = node.measuredIsRemoved; + + // Recalculate postion relative to their parent for elements that have been moved + if (!oldStateNode.measuredIsRemoved && !isRemovedNow && !hasNoOldState && (parentChanged || elementChanged)) { + const oldAbsoluteLeft = oldStateNode.properties.left; + const oldAbsoluteTop = oldStateNode.properties.top; + const newParent = parent || newState.rootNode; + const oldParent = newParent.id ? oldState.nodes.get(newParent.id) : null; + const parentLeft = oldParent ? oldParent.properties.left : newParent.properties.left; + const parentTop = oldParent ? oldParent.properties.top : newParent.properties.top; + const borderLeft = oldParent ? oldParent.properties.clientLeft : newParent.properties.clientLeft; + const borderTop = oldParent ? oldParent.properties.clientTop : newParent.properties.clientTop; + oldStateNode.properties.x = oldAbsoluteLeft - parentLeft - borderLeft; + oldStateNode.properties.y = oldAbsoluteTop - parentTop - borderTop; + } + + if (node.hasVisibilitySwap) { + if (node.hasVisibilityHidden) { + node.$el.style.visibility = 'visible'; + node.$measure.style.visibility = 'hidden'; + } + if (node.hasDisplayNone) { + node.$el.style.display = oldStateNode.measuredDisplay || node.measuredDisplay || ''; + // Setting visibility 'hidden' instead of display none to avoid calculation issues + node.$measure.style.visibility = 'hidden'; + // @TODO: check why setting display here can cause calculation issues + // node.$measure.style.display = 'none'; + } + } + + const wasPendingRemoval = pendingRemoval.has($el); + const wasVisibleBefore = oldStateNode.measuredIsVisible; + const isVisibleNow = node.measuredIsVisible; + const becomeVisible = !wasVisibleBefore && isVisibleNow && !parentNotRendered; + const topLevelAdded = !isRemovedNow && (wasRemovedBefore || wasPendingRemoval) && !parentAdded; + const newlyRemoved = isRemovedNow && !wasRemovedBefore && !parentRemoved; + const topLevelRemoved = newlyRemoved || isRemovedNow && wasPendingRemoval && !parentRemoved; + + node.branchAdded = parentAdded || topLevelAdded; + node.branchRemoved = parentRemoved || topLevelRemoved; + node.branchNotRendered = parentNotRendered || isRemovedNow; + + if (isRemovedNow && wasVisibleBefore) { + node.$el.style.display = oldStateNode.measuredDisplay; + node.$el.style.visibility = 'visible'; + cloneNodeProperties(oldStateNode, node, newState); + } + + // Node is leaving + if (newlyRemoved) { + if (node.isTarget) { + leaving.push($el); + node.isLeaving = true; + } + pendingRemoval.add($el); + } else if (!isRemovedNow && wasPendingRemoval) { + pendingRemoval.delete($el); + } + + // Node is entering + if ((topLevelAdded && !parentNotRendered) || becomeVisible) { + updateNodeProperties(oldStateNode, enterFromProps); + if (node.isTarget) { + entering.push($el); + node.isEntering = true; + } + // Node is leaving + } else if (topLevelRemoved && !parentNotRendered) { + updateNodeProperties(node, leaveToProps); + } + + // Node is animating + // The animating array is used only to calculate delays and duration on root children + if (node !== rootNode && node.isTarget && !node.isEntering && !node.isLeaving) { + animating.push($el); + } + + targets.push($el); + + }); + + let enteringIndex = 0; + let leavingIndex = 0; + let animatingIndex = 0; + + newState.forEachRootNode(node => { + + const $el = node.$el; + const parent = node.parentNode; + const oldStateNode = oldState.nodes.get(node.id); + const nodeProperties = node.properties; + const oldStateNodeProperties = oldStateNode.properties; + + // Use closest animated parent index and total values so that children staggered delays are in sync with their parent + let animatedParent = parent !== rootNode && parent; + while (animatedParent && !animatedParent.isTarget && animatedParent !== rootNode) { + animatedParent = animatedParent.parentNode; + } + + // Root is always animated first in sync with the first child (animating.length is the total of children) + if (node === rootNode) { + node.index = 0; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); + } else if (node.isEntering) { + node.index = animatedParent ? animatedParent.index : enteringIndex; + node.targets = animatedParent ? animating : entering; + updateNodeTimingParams(node, enterFromTimings); + enteringIndex++; + } else if (node.isLeaving) { + node.index = animatedParent ? animatedParent.index : leavingIndex; + node.targets = animatedParent ? animating : leaving; + leavingIndex++; + updateNodeTimingParams(node, leaveToTimings); + } else if (node.isTarget) { + node.index = animatingIndex++; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); + } else { + node.index = animatedParent ? animatedParent.index : 0; + node.targets = animating; + updateNodeTimingParams(node, swapAtTimings); + } + + // Make sure the old state node has its inex and total values up to date for valid "from" function values calculation + oldStateNode.index = node.index; + oldStateNode.targets = node.targets; + + // Computes all values up front so we can check for changes and we don't have to re-compute them inside the animation props + for (let prop in nodeProperties) { + nodeProperties[prop] = values.getFunctionValue(nodeProperties[prop], $el, node.index, node.targets, null, null); + oldStateNodeProperties[prop] = values.getFunctionValue(oldStateNodeProperties[prop], $el, oldStateNode.index, oldStateNode.targets, null, null); + } + + // Use a 1px tolerance to detect dimensions changes to prevent width / height animations on barelly visible elements + const sizeTolerance = 1; + const widthChanged = Math.abs(nodeProperties.width - oldStateNodeProperties.width) > sizeTolerance; + const heightChanged = Math.abs(nodeProperties.height - oldStateNodeProperties.height) > sizeTolerance; + + node.sizeChanged = (widthChanged || heightChanged); + + // const hiddenStateChanged = (topLevelAdded || newlyRemoved) && wasRemovedBefore !== isRemovedNow; + + if (node.isTarget && (!node.measuredIsRemoved && oldStateNode.measuredIsVisible || node.measuredIsRemoved && node.measuredIsVisible)) { + if (nodeProperties.transform !== 'none' || oldStateNodeProperties.transform !== 'none') { + node.hasTransform = true; + transformed.push($el); + } + for (let prop in nodeProperties) { + // if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop] || hiddenStateChanged)) { + if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop])) { + animated.push($el); + break; + } + } + } + + if (!node.isTarget) { + swapping.push($el); + if (node.sizeChanged && parent && parent.isTarget && parent.sizeChanged) { + if (swapAtProps.transform) { + node.hasTransform = true; + transformed.push($el); + } + animatedSwap.push($el); + } + } + + }); + + const timingParams = { + delay: (/** @type {HTMLElement} */$el) => newState.getNode($el).delay, + duration: (/** @type {HTMLElement} */$el) => newState.getNode($el).duration, + ease: (/** @type {HTMLElement} */$el) => newState.getNode($el).ease, + }; + + tlParams.defaults = timingParams; + + this.timeline = timeline.createTimeline(tlParams); + + // Imediatly return the timeline if no layout changes detected + if (!animated.length && !transformed.length && !swapping.length) { + // Make sure to restore all CSS transition if no animation + restoreLayoutTransition(this.transitionMuteStore); + return this.timeline.complete(); + } + + if (targets.length) { + + this.root.classList.add('is-animated'); + + for (let i = 0, l = targets.length; i < l; i++) { + const $el = targets[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; + + // muteNodeTransition(newNode); + + // Don't animate positions of inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + // Display grid can mess with the absolute positioning, so set it to block during transition + if (oldNode.measuredDisplay === 'grid' || newNode.measuredDisplay === 'grid') $el.style.setProperty('display', 'block', 'important'); + // All children must be in position absolute or fixed + if ($el !== $root || this.absoluteCoords) { + $el.style.position = this.absoluteCoords ? 'fixed' : 'absolute'; + $el.style.left = '0px'; + $el.style.top = '0px'; + $el.style.marginLeft = '0px'; + $el.style.marginTop = '0px'; + $el.style.translate = `${oldNodeState.x}px ${oldNodeState.y}px`; + } + if ($el === $root && newNode.measuredPosition === 'static') { + $el.style.position = 'relative'; + // Cancel left / trop in case the static element had muted values now activated by potision relative + $el.style.left = '0px'; + $el.style.top = '0px'; + } + } + // Animate dimensions for all elements (including inlined) + $el.style.width = `${oldNodeState.width}px`; + $el.style.height = `${oldNodeState.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + } + + // Restore the scroll position if the oldState differs from the current state + if (oldState.scrollX !== window.scrollX || oldState.scrollY !== window.scrollY) { + // Restoring in the next frame avoids race conditions if for example a waapi animation commit styles that affect the root height + requestAnimationFrame(() => window.scrollTo(oldState.scrollX, oldState.scrollY)); + } + + for (let i = 0, l = animated.length; i < l; i++) { + const $el = animated[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; + const newNodeState = newNode.properties; + let nodeHasChanged = false; + /** @type {AnimationParams} */ + const animatedProps = { + composition: 'none', + }; + if (oldNodeState.width !== newNodeState.width) { + animatedProps.width = [oldNodeState.width, newNodeState.width]; + nodeHasChanged = true; + } + if (oldNodeState.height !== newNodeState.height) { + animatedProps.height = [oldNodeState.height, newNodeState.height]; + nodeHasChanged = true; + } + // If the node has transforms we handle the translate animation in waapi otherwise translate and other transforms can be out of sync + // And we don't animate the position of inlined elements + if (!newNode.hasTransform && !newNode.isInlined) { + animatedProps.translate = [`${oldNodeState.x}px ${oldNodeState.y}px`, `${newNodeState.x}px ${newNodeState.y}px`]; + nodeHasChanged = true; + } + this.properties.forEach(prop => { + const oldVal = oldNodeState[prop]; + const newVal = newNodeState[prop]; + if (prop !== 'transform' && oldVal !== newVal) { + animatedProps[prop] = [oldVal, newVal]; + nodeHasChanged = true; + } + }); + if (nodeHasChanged) { + this.timeline.add($el, animatedProps, 0); + } + } + + } + + if (swapping.length) { + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const oldNode = oldState.getNode($el); + const oldNodeProps = oldNode.properties; + $el.style.width = `${oldNodeProps.width}px`; + $el.style.height = `${oldNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // We don't animate the position of inlined elements + if (!oldNode.isInlined) { + $el.style.translate = `${oldNodeProps.x}px ${oldNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${oldState.getComputedValue($el, prop)}`; + } + }); + } + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const newNode = newState.getNode($el); + const newNodeProps = newNode.properties; + this.timeline.call(() => { + $el.style.width = `${newNodeProps.width}px`; + $el.style.height = `${newNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // Don't set translate for inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + $el.style.translate = `${newNodeProps.x}px ${newNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${newState.getComputedValue($el, prop)}`; + } + }); + }, newNode.delay + newNode.duration / 2); + } + + if (animatedSwap.length) { + const ease = parser.parseEase(newState.nodes.get(animatedSwap[0].dataset.layoutId).ease); + const inverseEased = t => 1 - ease(1 - t); + const animatedSwapParams = /** @type {AnimationParams} */({}); + if (swapAtProps) { + for (let prop in swapAtProps) { + if (prop !== 'transform') { + animatedSwapParams[prop] = [ + { from: (/** @type {HTMLElement} */$el) => oldState.getComputedValue($el, prop), to: swapAtProps[prop] }, + { from: swapAtProps[prop], to: (/** @type {HTMLElement} */$el) => newState.getComputedValue($el, prop), ease: inverseEased } + ]; + } + } + } + this.timeline.add(animatedSwap, animatedSwapParams, 0); + } + + } + + const transformedLength = transformed.length; + + if (transformedLength) { + // We only need to set the transform property here since translate is already defined in the targets loop + for (let i = 0; i < transformedLength; i++) { + const $el = transformed[i]; + const node = newState.getNode($el); + // Don't set translate for inlined elements (to avoid text reflow) + if (!node.isInlined) { + $el.style.translate = `${oldState.getComputedValue($el, 'x')}px ${oldState.getComputedValue($el, 'y')}px`; + } + $el.style.transform = oldState.getComputedValue($el, 'transform'); + if (animatedSwap.includes($el)) { + node.ease = values.getFunctionValue(swapAtParams.ease, $el, node.index, node.targets, null, null); + node.duration = values.getFunctionValue(swapAtParams.duration, $el, node.index, node.targets, null, null); + } + } + this.transformAnimation = waapi.waapi.animate(transformed, { + translate: (/** @type {HTMLElement} */$el) => { + const node = newState.getNode($el); + // Don't animate translate for inlined elements (to avoid text reflow) + if (node.isInlined) return '0px 0px'; + return `${newState.getComputedValue($el, 'x')}px ${newState.getComputedValue($el, 'y')}px`; + }, + transform: (/** @type {HTMLElement} */$el) => { + const newValue = newState.getComputedValue($el, 'transform'); + if (!animatedSwap.includes($el)) return newValue; + const oldValue = oldState.getComputedValue($el, 'transform'); + const node = newState.getNode($el); + return [oldValue, values.getFunctionValue(swapAtProps.transform, $el, node.index, node.targets, null, null), newValue] + }, + autoplay: false, + // persist: true, + ...timingParams, + }); + this.timeline.sync(this.transformAnimation, 0); + } + + return this.timeline.init(); + } + + /** + * @param {(layout: this) => void} callback + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + update(callback, params = {}) { + this.record(); + callback(this); + return this.animate(params); + } +} + +/** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + * @return {AutoLayout} + */ +const createLayout = (root, params) => new AutoLayout(root, params); + +exports.AutoLayout = AutoLayout; +exports.createLayout = createLayout; diff --git a/dist/modules/layout/layout.d.ts b/dist/modules/layout/layout.d.ts new file mode 100644 index 000000000..b8d8c424e --- /dev/null +++ b/dist/modules/layout/layout.d.ts @@ -0,0 +1,218 @@ +export class AutoLayout { + /** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + */ + constructor(root: DOMTargetSelector, params?: AutoLayoutParams); + /** @type {AutoLayoutParams} */ + params: AutoLayoutParams; + /** @type {DOMTarget} */ + root: DOMTarget; + /** @type {Number|String} */ + id: number | string; + /** @type {LayoutChildrenParam} */ + children: LayoutChildrenParam; + /** @type {Boolean} */ + absoluteCoords: boolean; + /** @type {LayoutStateParams} */ + swapAtParams: LayoutStateParams; + /** @type {LayoutStateParams} */ + enterFromParams: LayoutStateParams; + /** @type {LayoutStateParams} */ + leaveToParams: LayoutStateParams; + /** @type {Set} */ + properties: Set; + /** @type {Set} */ + recordedProperties: Set; + /** @type {WeakSet} */ + pendingRemoval: WeakSet; + /** @type {Map} */ + transitionMuteStore: Map; + /** @type {LayoutSnapshot} */ + oldState: LayoutSnapshot; + /** @type {LayoutSnapshot} */ + newState: LayoutSnapshot; + /** @type {Timeline} */ + timeline: Timeline; + /** @type {WAAPIAnimation} */ + transformAnimation: WAAPIAnimation; + /** @type {Array} */ + animating: Array; + /** @type {Array} */ + swapping: Array; + /** @type {Array} */ + leaving: Array; + /** @type {Array} */ + entering: Array; + /** + * @return {this} + */ + revert(): this; + /** + * @return {this} + */ + record(): this; + /** + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + animate(params?: LayoutAnimationParams): Timeline; + /** + * @param {(layout: this) => void} callback + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + update(callback: (layout: this) => void, params?: LayoutAnimationParams): Timeline; +} +export function createLayout(root: DOMTargetSelector, params?: AutoLayoutParams): AutoLayout; +export type LayoutChildrenParam = DOMTargetSelector | Array; +export type LayoutAnimationTimingsParams = { + delay?: number | FunctionValue; + duration?: number | FunctionValue; + ease?: EasingParam | FunctionValue; +}; +export type LayoutStateAnimationProperties = Record; +export type LayoutStateParams = LayoutStateAnimationProperties & LayoutAnimationTimingsParams; +export type LayoutSpecificAnimationParams = { + id?: number | string; + delay?: number | FunctionValue; + duration?: number | FunctionValue; + ease?: EasingParam | FunctionValue; + playbackEase?: EasingParam; + swapAt?: LayoutStateParams; + enterFrom?: LayoutStateParams; + leaveTo?: LayoutStateParams; +}; +export type LayoutAnimationParams = LayoutSpecificAnimationParams & TimerParams & TickableCallbacks & RenderableCallbacks; +export type LayoutOptions = { + children?: LayoutChildrenParam; + properties?: Array; +}; +export type AutoLayoutParams = LayoutAnimationParams & LayoutOptions; +export type LayoutNodeProperties = Record & { + transform: string; + x: number; + y: number; + left: number; + top: number; + clientLeft: number; + clientTop: number; + width: number; + height: number; +}; +export type LayoutNode = { + id: string; + $el: DOMTarget; + index: number; + targets: Array; + delay: number; + duration: number; + ease: EasingParam; + $measure: DOMTarget; + state: LayoutSnapshot; + layout: AutoLayout; + parentNode: LayoutNode | null; + isTarget: boolean; + isEntering: boolean; + isLeaving: boolean; + hasTransform: boolean; + inlineStyles: Array; + inlineTransforms: string | null; + inlineTransition: string | null; + branchAdded: boolean; + branchRemoved: boolean; + branchNotRendered: boolean; + sizeChanged: boolean; + isInlined: boolean; + hasVisibilitySwap: boolean; + hasDisplayNone: boolean; + hasVisibilityHidden: boolean; + measuredInlineTransform: string | null; + measuredInlineTransition: string | null; + measuredDisplay: string | null; + measuredVisibility: string | null; + measuredPosition: string | null; + measuredHasDisplayNone: boolean; + measuredHasVisibilityHidden: boolean; + measuredIsVisible: boolean; + measuredIsRemoved: boolean; + measuredIsInsideRoot: boolean; + properties: LayoutNodeProperties; + _head: LayoutNode | null; + _tail: LayoutNode | null; + _prev: LayoutNode | null; + _next: LayoutNode | null; +}; +export type LayoutNodeIterator = (node: LayoutNode, index: number) => void; +import type { DOMTarget } from '../types/index.js'; +declare class LayoutSnapshot { + /** + * @param {AutoLayout} layout + */ + constructor(layout: AutoLayout); + /** @type {AutoLayout} */ + layout: AutoLayout; + /** @type {LayoutNode|null} */ + rootNode: LayoutNode | null; + /** @type {Set} */ + rootNodes: Set; + /** @type {Map} */ + nodes: Map; + /** @type {Number} */ + scrollX: number; + /** @type {Number} */ + scrollY: number; + /** + * @return {this} + */ + revert(): this; + /** + * @param {DOMTarget} $el + * @return {LayoutNode} + */ + getNode($el: DOMTarget): LayoutNode; + /** + * @param {DOMTarget} $el + * @param {String} prop + * @return {Number|String} + */ + getComputedValue($el: DOMTarget, prop: string): number | string; + /** + * @param {LayoutNode|null} rootNode + * @param {LayoutNodeIterator} cb + */ + forEach(rootNode: LayoutNode | null, cb: LayoutNodeIterator): void; + /** + * @param {LayoutNodeIterator} cb + */ + forEachRootNode(cb: LayoutNodeIterator): void; + /** + * @param {LayoutNodeIterator} cb + */ + forEachNode(cb: LayoutNodeIterator): void; + /** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @return {LayoutNode|null} + */ + registerElement($el: DOMTarget, parentNode: LayoutNode | null): LayoutNode | null; + /** + * @param {DOMTarget} $el + * @param {Set} candidates + * @return {LayoutNode|null} + */ + ensureDetachedNode($el: DOMTarget, candidates: Set): LayoutNode | null; + /** + * @return {this} + */ + record(): this; +} +import type { Timeline } from '../timeline/timeline.js'; +import type { WAAPIAnimation } from '../waapi/waapi.js'; +import type { DOMTargetSelector } from '../types/index.js'; +import type { FunctionValue } from '../types/index.js'; +import type { EasingParam } from '../types/index.js'; +import type { TimerParams } from '../types/index.js'; +import type { TickableCallbacks } from '../types/index.js'; +import type { RenderableCallbacks } from '../types/index.js'; +export {}; diff --git a/dist/modules/layout/layout.js b/dist/modules/layout/layout.js new file mode 100644 index 000000000..b6336c4b4 --- /dev/null +++ b/dist/modules/layout/layout.js @@ -0,0 +1,1593 @@ +/** + * Anime.js - layout - ESM + * @version v4.4.1 + * @license MIT + * @copyright 2026 - Julian Garnier + */ + +import { mergeObjects, isUnd, isSvg, isStr, isFnc, isArr } from '../core/helpers.js'; +import { registerTargets } from '../core/targets.js'; +import { parseEase } from '../easings/eases/parser.js'; +import { setValue, getFunctionValue } from '../core/values.js'; +import { createTimeline } from '../timeline/timeline.js'; +import { waapi } from '../waapi/waapi.js'; +import { scope, defaults, globals } from '../core/globals.js'; + +/** + * @import { + * AnimationParams, + * RenderableCallbacks, + * TickableCallbacks, + * TimelineParams, + * TimerParams, + * } from '../types/index.js' +*/ + +/** + * @import { + * ScrollObserver, + * } from '../events/scroll.js' +*/ + +/** + * @import { + * Timeline, + * } from '../timeline/timeline.js' +*/ + +/** + * @import { + * WAAPIAnimation + * } from '../waapi/waapi.js' +*/ + +/** + * @import { + * Spring, + } from '../easings/spring/index.js' +*/ + +/** + * @import { + * DOMTarget, + * DOMTargetSelector, + * FunctionValue, + * EasingParam, + } from '../types/index.js' +*/ + +/** + * @typedef {DOMTargetSelector|Array} LayoutChildrenParam + */ + +/** + * @typedef {Object} LayoutAnimationTimingsParams + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] + */ + +/** + * @typedef {Record} LayoutStateAnimationProperties + */ + +/** + * @typedef {LayoutStateAnimationProperties & LayoutAnimationTimingsParams} LayoutStateParams + */ + +/** + * @typedef {Object} LayoutSpecificAnimationParams + * @property {Number|String} [id] + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] + * @property {EasingParam} [playbackEase] + * @property {LayoutStateParams} [swapAt] + * @property {LayoutStateParams} [enterFrom] + * @property {LayoutStateParams} [leaveTo] + */ + +/** + * @typedef {LayoutSpecificAnimationParams & TimerParams & TickableCallbacks & RenderableCallbacks} LayoutAnimationParams + */ + +/** + * @typedef {Object} LayoutOptions + * @property {LayoutChildrenParam} [children] + * @property {Array} [properties] + */ + +/** + * @typedef {LayoutAnimationParams & LayoutOptions} AutoLayoutParams + */ + +/** + * @typedef {Record & { + * transform: String, + * x: Number, + * y: Number, + * left: Number, + * top: Number, + * clientLeft: Number, + * clientTop: Number, + * width: Number, + * height: Number, + * }} LayoutNodeProperties + */ + +/** + * @typedef {Object} LayoutNode + * @property {String} id + * @property {DOMTarget} $el + * @property {Number} index + * @property {Array} targets + * @property {Number} delay + * @property {Number} duration + * @property {EasingParam} ease + * @property {DOMTarget} $measure + * @property {LayoutSnapshot} state + * @property {AutoLayout} layout + * @property {LayoutNode|null} parentNode + * @property {Boolean} isTarget + * @property {Boolean} isEntering + * @property {Boolean} isLeaving + * @property {Boolean} hasTransform + * @property {Array} inlineStyles + * @property {String|null} inlineTransforms + * @property {String|null} inlineTransition + * @property {Boolean} branchAdded + * @property {Boolean} branchRemoved + * @property {Boolean} branchNotRendered + * @property {Boolean} sizeChanged + * @property {Boolean} isInlined + * @property {Boolean} hasVisibilitySwap + * @property {Boolean} hasDisplayNone + * @property {Boolean} hasVisibilityHidden + * @property {String|null} measuredInlineTransform + * @property {String|null} measuredInlineTransition + * @property {String|null} measuredDisplay + * @property {String|null} measuredVisibility + * @property {String|null} measuredPosition + * @property {Boolean} measuredHasDisplayNone + * @property {Boolean} measuredHasVisibilityHidden + * @property {Boolean} measuredIsVisible + * @property {Boolean} measuredIsRemoved + * @property {Boolean} measuredIsInsideRoot + * @property {LayoutNodeProperties} properties + * @property {LayoutNode|null} _head + * @property {LayoutNode|null} _tail + * @property {LayoutNode|null} _prev + * @property {LayoutNode|null} _next + */ + +/** + * @callback LayoutNodeIterator + * @param {LayoutNode} node + * @param {Number} index + * @return {void} + */ + +let layoutId = 0; +let nodeId = 0; + +/** + * @param {DOMTarget} root + * @param {DOMTarget} $el + * @return {Boolean} + */ +const isElementInRoot = (root, $el) => { + if (!root || !$el) return false; + return root === $el || root.contains($el); +}; + +/** + * @param {DOMTarget|null} $el + * @return {String|null} + */ +const muteElementTransition = $el => { + if (!$el) return null; + const style = $el.style; + const transition = style.transition || ''; + style.setProperty('transition', 'none', 'important'); + return transition; +}; + +/** + * @param {DOMTarget|null} $el + * @param {String|null} transition + */ +const restoreElementTransition = ($el, transition) => { + if (!$el) return; + const style = $el.style; + if (transition) { + style.transition = transition; + } else { + style.removeProperty('transition'); + } +}; + +/** + * @param {LayoutNode} node + */ +const muteNodeTransition = node => { + const store = node.layout.transitionMuteStore; + const $el = node.$el; + const $measure = node.$measure; + if ($el && !store.has($el)) store.set($el, muteElementTransition($el)); + if ($measure && !store.has($measure)) store.set($measure, muteElementTransition($measure)); +}; + +/** + * @param {Map} store + */ +const restoreLayoutTransition = store => { + store.forEach((value, $el) => restoreElementTransition($el, value)); + store.clear(); +}; + +const hiddenComputedStyle = /** @type {CSSStyleDeclaration} */({ + display: 'none', + visibility: 'hidden', + opacity: '0', + transform: 'none', + position: 'static', +}); + +/** + * @param {LayoutNode|null} node + */ +const detachNode = node => { + if (!node) return; + const parent = node.parentNode; + if (!parent) return; + if (parent._head === node) parent._head = node._next; + if (parent._tail === node) parent._tail = node._prev; + if (node._prev) node._prev._next = node._next; + if (node._next) node._next._prev = node._prev; + node._prev = null; + node._next = null; + node.parentNode = null; +}; + +/** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @param {LayoutSnapshot} state + * @param {LayoutNode} recycledNode + * @return {LayoutNode} + */ +const createNode = ($el, parentNode, state, recycledNode) => { + let dataId = $el.dataset.layoutId; + if (!dataId) dataId = $el.dataset.layoutId = `node-${nodeId++}`; + const node = recycledNode ? recycledNode : /** @type {LayoutNode} */({}); + node.$el = $el; + node.$measure = $el; + node.id = dataId; + node.index = 0; + node.targets = null; + node.delay = 0; + node.duration = 0; + node.ease = null; + node.state = state; + node.layout = state.layout; + node.parentNode = parentNode || null; + node.isTarget = false; + node.isEntering = false; + node.isLeaving = false; + node.isInlined = false; + node.hasTransform = false; + node.inlineStyles = []; + node.inlineTransforms = null; + node.inlineTransition = null; + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.sizeChanged = false; + node.hasVisibilitySwap = false; + node.hasDisplayNone = false; + node.hasVisibilityHidden = false; + node.measuredInlineTransform = null; + node.measuredInlineTransition = null; + node.measuredDisplay = null; + node.measuredVisibility = null; + node.measuredPosition = null; + node.measuredHasDisplayNone = false; + node.measuredHasVisibilityHidden = false; + node.measuredIsVisible = false; + node.measuredIsRemoved = false; + node.measuredIsInsideRoot = false; + node.properties = /** @type {LayoutNodeProperties} */({ + transform: 'none', + x: 0, + y: 0, + left: 0, + top: 0, + clientLeft: 0, + clientTop: 0, + width: 0, + height: 0, + }); + node.layout.properties.forEach(prop => node.properties[prop] = 0); + node._head = null; + node._tail = null; + node._prev = null; + node._next = null; + return node; +}; + +/** + * @param {LayoutNode} node + * @param {DOMTarget} $measure + * @param {CSSStyleDeclaration} computedStyle + * @param {Boolean} skipMeasurements + * @return {LayoutNode} + */ +const recordNodeState = (node, $measure, computedStyle, skipMeasurements) => { + const $el = node.$el; + const root = node.layout.root; + const isRoot = root === $el; + const properties = node.properties; + const rootNode = node.state.rootNode; + const parentNode = node.parentNode; + const computedTransforms = computedStyle.transform; + const inlineTransforms = $el.style.transform; + const parentNotRendered = parentNode ? parentNode.measuredIsRemoved : false; + const position = computedStyle.position; + if (isRoot) node.layout.absoluteCoords = position === 'fixed' || position === 'absolute'; + node.$measure = $measure; + node.inlineTransforms = inlineTransforms; + node.hasTransform = computedTransforms && computedTransforms !== 'none'; + node.measuredIsInsideRoot = isElementInRoot(root, $measure); + node.measuredInlineTransform = null; + node.measuredDisplay = computedStyle.display; + node.measuredVisibility = computedStyle.visibility; + node.measuredPosition = position; + node.measuredHasDisplayNone = computedStyle.display === 'none'; + node.measuredHasVisibilityHidden = computedStyle.visibility === 'hidden'; + node.measuredIsVisible = !(node.measuredHasDisplayNone || node.measuredHasVisibilityHidden); + node.measuredIsRemoved = node.measuredHasDisplayNone || node.measuredHasVisibilityHidden || parentNotRendered; + // Check if element has adjacent text that would reflow when taken out of flow + let hasAdjacentText = false; + let s = $el.previousSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.previousSibling; + if (s && s.nodeType === Node.TEXT_NODE) { + hasAdjacentText = true; + } else { + s = $el.nextSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.nextSibling; + hasAdjacentText = s !== null && s.nodeType === Node.TEXT_NODE; + } + node.isInlined = hasAdjacentText; + + // Mute transforms (and transition to avoid triggering an animation) before the position calculation + if (node.hasTransform && !skipMeasurements) { + const transitionMuteStore = node.layout.transitionMuteStore; + if (!transitionMuteStore.get($el)) node.inlineTransition = muteElementTransition($el); + if ($measure === $el) { + $el.style.transform = 'none'; + } else { + if (!transitionMuteStore.get($measure)) node.measuredInlineTransition = muteElementTransition($measure); + node.measuredInlineTransform = $measure.style.transform; + $measure.style.transform = 'none'; + } + } + + let left = 0; + let top = 0; + let width = 0; + let height = 0; + + if (!skipMeasurements) { + const rect = $measure.getBoundingClientRect(); + left = rect.left; + top = rect.top; + width = rect.width; + height = rect.height; + } + + for (let name in properties) { + const computedProp = name === 'transform' ? computedTransforms : computedStyle[name] || (computedStyle.getPropertyValue && computedStyle.getPropertyValue(name)); + if (!isUnd(computedProp)) properties[name] = computedProp; + } + + properties.left = left; + properties.top = top; + properties.clientLeft = skipMeasurements ? 0 : $measure.clientLeft; + properties.clientTop = skipMeasurements ? 0 : $measure.clientTop; + // Compute local x/y relative to parent + let absoluteLeft, absoluteTop; + if (isRoot) { + if (!node.layout.absoluteCoords) { + absoluteLeft = 0; + absoluteTop = 0; + } else { + absoluteLeft = left; + absoluteTop = top; + } + } else { + const p = parentNode || rootNode; + const parentLeft = p.properties.left; + const parentTop = p.properties.top; + const borderLeft = p.properties.clientLeft; + const borderTop = p.properties.clientTop; + if (!node.layout.absoluteCoords) { + if (p === rootNode) { + const rootLeft = rootNode.properties.left; + const rootTop = rootNode.properties.top; + const rootBorderLeft = rootNode.properties.clientLeft; + const rootBorderTop = rootNode.properties.clientTop; + absoluteLeft = left - rootLeft - rootBorderLeft; + absoluteTop = top - rootTop - rootBorderTop; + } else { + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; + } + } else { + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; + } + } + properties.x = absoluteLeft; + properties.y = absoluteTop; + properties.width = width; + properties.height = height; + return node; +}; + +/** + * @param {LayoutNode} node + * @param {LayoutStateAnimationProperties} [props] + */ +const updateNodeProperties = (node, props) => { + if (!props) return; + for (let name in props) { + node.properties[name] = props[name]; + } +}; + +/** + * @param {LayoutNode} node + * @param {LayoutAnimationTimingsParams} params + */ +const updateNodeTimingParams = (node, params) => { + const easeFunctionResult = getFunctionValue(params.ease, node.$el, node.index, node.targets, null, null); + const keyEasing = isFnc(easeFunctionResult) ? easeFunctionResult : params.ease; + const hasSpring = !isUnd(keyEasing) && !isUnd(/** @type {Spring} */(keyEasing).ease); + node.ease = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; + node.duration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(params.duration, node.$el, node.index, node.targets, null, null); + node.delay = getFunctionValue(params.delay, node.$el, node.index, node.targets, null, null); +}; + +/** + * @param {LayoutNode} node + */ +const recordNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + stylesStore.length = 0; + node.layout.recordedProperties.forEach(prop => { + stylesStore.push(prop, style[prop] || ''); + }); +}; + +/** + * @param {LayoutNode} node + */ +const restoreNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + for (let i = 0, l = stylesStore.length; i < l; i += 2) { + const property = stylesStore[i]; + const styleValue = stylesStore[i + 1]; + if (styleValue && styleValue !== '') { + style[property] = styleValue; + } else { + style[property] = ''; + style.removeProperty(property); + } + } +}; + +/** + * @param {LayoutNode} node + */ +const restoreNodeTransform = node => { + const inlineTransforms = node.inlineTransforms; + const nodeStyle = node.$el.style; + if (!node.hasTransform || !inlineTransforms || (node.hasTransform && nodeStyle.transform === 'none') || (inlineTransforms && inlineTransforms === 'none')) { + nodeStyle.removeProperty('transform'); + } else if (inlineTransforms) { + nodeStyle.transform = inlineTransforms; + } + const $measure = node.$measure; + if (node.hasTransform && $measure !== node.$el) { + const measuredStyle = $measure.style; + const measuredInline = node.measuredInlineTransform; + if (measuredInline && measuredInline !== '') { + measuredStyle.transform = measuredInline; + } else { + measuredStyle.removeProperty('transform'); + } + } + node.measuredInlineTransform = null; + if (node.inlineTransition !== null) { + restoreElementTransition(node.$el, node.inlineTransition); + node.inlineTransition = null; + } + if ($measure !== node.$el && node.measuredInlineTransition !== null) { + restoreElementTransition($measure, node.measuredInlineTransition); + node.measuredInlineTransition = null; + } +}; + +/** + * @param {LayoutNode} node + */ +const restoreNodeVisualState = node => { + if (node.measuredIsRemoved || node.hasVisibilitySwap) { + node.$el.style.removeProperty('display'); + node.$el.style.removeProperty('visibility'); + if (node.hasVisibilitySwap) { + node.$measure.style.removeProperty('display'); + node.$measure.style.removeProperty('visibility'); + } + } + // if (node.measuredIsRemoved) { + node.layout.pendingRemoval.delete(node.$el); + // } +}; + +/** + * @param {LayoutNode} node + * @param {LayoutNode} targetNode + * @param {LayoutSnapshot} newState + * @return {LayoutNode} + */ +const cloneNodeProperties = (node, targetNode, newState) => { + targetNode.properties = /** @type {LayoutNodeProperties} */({ ...node.properties }); + targetNode.state = newState; + targetNode.isTarget = node.isTarget; + targetNode.hasTransform = node.hasTransform; + targetNode.inlineTransforms = node.inlineTransforms; + targetNode.measuredIsVisible = node.measuredIsVisible; + targetNode.measuredDisplay = node.measuredDisplay; + targetNode.measuredIsRemoved = node.measuredIsRemoved; + targetNode.measuredHasDisplayNone = node.measuredHasDisplayNone; + targetNode.measuredHasVisibilityHidden = node.measuredHasVisibilityHidden; + targetNode.hasDisplayNone = node.hasDisplayNone; + targetNode.isInlined = node.isInlined; + targetNode.hasVisibilityHidden = node.hasVisibilityHidden; + return targetNode; +}; + +class LayoutSnapshot { + /** + * @param {AutoLayout} layout + */ + constructor(layout) { + /** @type {AutoLayout} */ + this.layout = layout; + /** @type {LayoutNode|null} */ + this.rootNode = null; + /** @type {Set} */ + this.rootNodes = new Set(); + /** @type {Map} */ + this.nodes = new Map(); + /** @type {Number} */ + this.scrollX = 0; + /** @type {Number} */ + this.scrollY = 0; + } + + /** + * @return {this} + */ + revert() { + this.forEachNode(node => { + this.layout.pendingRemoval.delete(node.$el); + node.$el.removeAttribute('data-layout-id'); + node.$measure.removeAttribute('data-layout-id'); + }); + this.rootNode = null; + this.rootNodes.clear(); + this.nodes.clear(); + return this; + } + + /** + * @param {DOMTarget} $el + * @return {LayoutNode} + */ + getNode($el) { + if (!$el || !$el.dataset) return; + return this.nodes.get($el.dataset.layoutId); + } + + /** + * @param {DOMTarget} $el + * @param {String} prop + * @return {Number|String} + */ + getComputedValue($el, prop) { + const node = this.getNode($el); + if (!node) return; + return /** @type {Number|String} */(node.properties[prop]); + } + + /** + * @param {LayoutNode|null} rootNode + * @param {LayoutNodeIterator} cb + */ + forEach(rootNode, cb) { + let node = rootNode; + let i = 0; + while (node) { + cb(node, i++); + if (node._head) { + node = node._head; + } else if (node._next) { + node = node._next; + } else { + while (node && !node._next) { + node = node.parentNode; + } + if (node) node = node._next; + } + } + } + + /** + * @param {LayoutNodeIterator} cb + */ + forEachRootNode(cb) { + this.forEach(this.rootNode, cb); + } + + /** + * @param {LayoutNodeIterator} cb + */ + forEachNode(cb) { + for (const rootNode of this.rootNodes) { + this.forEach(rootNode, cb); + } + } + + /** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @return {LayoutNode|null} + */ + registerElement($el, parentNode) { + if (!$el || $el.nodeType !== 1) return null; + + if (!this.layout.transitionMuteStore.has($el)) this.layout.transitionMuteStore.set($el, muteElementTransition($el)); + + /** @type {Array} */ + const stack = [$el, parentNode]; + const root = this.layout.root; + let firstNode = null; + + while (stack.length) { + /** @type {LayoutNode|null} */ + const $parent = /** @type {LayoutNode|null} */(stack.pop()); + /** @type {DOMTarget|null} */ + const $current = /** @type {DOMTarget|null} */(stack.pop()); + + if (!$current || $current.nodeType !== 1 || isSvg($current)) continue; + + const skipMeasurements = $parent ? $parent.measuredIsRemoved : false; + const computedStyle = skipMeasurements ? hiddenComputedStyle : getComputedStyle($current); + const hasDisplayNone = skipMeasurements ? true : computedStyle.display === 'none'; + const hasVisibilityHidden = skipMeasurements ? true : computedStyle.visibility === 'hidden'; + const isVisible = !hasDisplayNone && !hasVisibilityHidden; + const existingId = $current.dataset.layoutId; + const isInsideRoot = isElementInRoot(root, $current); + + let node = existingId ? this.nodes.get(existingId) : null; + + if (node && node.$el !== $current) { + const nodeInsideRoot = isElementInRoot(root, node.$el); + const measuredVisible = node.measuredIsVisible; + const shouldReassignNode = !nodeInsideRoot && (isInsideRoot || (!isInsideRoot && !measuredVisible && isVisible)); + const shouldReuseMeasurements = nodeInsideRoot && !measuredVisible && isVisible; + // Rebind nodes that move into the root or whose detached twin just became visible + if (shouldReassignNode) { + detachNode(node); + node = createNode($current, $parent, this, node); + // for hidden element with in-root sibling, keep the hidden node but borrow measurements from its visible in-root twin element + } else if (shouldReuseMeasurements) { + recordNodeState(node, $current, computedStyle, skipMeasurements); + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; + } + if (!firstNode) firstNode = node; + continue; + // No reassignment needed so keep walking descendants under the current parent + } else { + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), $parent); + $child = $child.previousElementSibling; + } + if (!firstNode) firstNode = node; + continue; + } + } else { + node = createNode($current, $parent, this, node); + } + + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.isTarget = false; + node.sizeChanged = false; + node.hasVisibilityHidden = hasVisibilityHidden; + node.hasDisplayNone = hasDisplayNone; + node.hasVisibilitySwap = (hasVisibilityHidden && !node.measuredHasVisibilityHidden) || (hasDisplayNone && !node.measuredHasDisplayNone); + + this.nodes.set(node.id, node); + + node.parentNode = $parent || null; + node._prev = null; + node._next = null; + + if ($parent) { + this.rootNodes.delete(node); + if (!$parent._head) { + $parent._head = node; + $parent._tail = node; + } else { + $parent._tail._next = node; + node._prev = $parent._tail; + $parent._tail = node; + } + } else { + // Each disconnected subtree becomes its own root in the snapshot graph + this.rootNodes.add(node); + } + + recordNodeState(node, node.$el, computedStyle, skipMeasurements); + + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; + } + + if (!firstNode) firstNode = node; + } + + return firstNode; + } + + /** + * @param {DOMTarget} $el + * @param {Set} candidates + * @return {LayoutNode|null} + */ + ensureDetachedNode($el, candidates) { + if (!$el || $el === this.layout.root) return null; + const existingId = $el.dataset.layoutId; + const existingNode = existingId ? this.nodes.get(existingId) : null; + if (existingNode && existingNode.$el === $el) return existingNode; + let parentNode = null; + let $ancestor = $el.parentElement; + while ($ancestor && $ancestor !== this.layout.root) { + if (candidates.has($ancestor)) { + parentNode = this.ensureDetachedNode($ancestor, candidates); + break; + } + $ancestor = $ancestor.parentElement; + } + return this.registerElement($el, parentNode); + } + + /** + * @return {this} + */ + record() { + const layout = this.layout; + const children = layout.children; + const root = layout.root; + const toParse = isArr(children) ? children : [children]; + const scoped = []; + const scopeRoot = children === '*' ? root : scope.root; + + // Mute transition and transforms of root ancestors before recording the state + + /** @type {Array} */ + const rootAncestorTransformStore = []; + let $ancestor = root.parentElement; + while ($ancestor && $ancestor.nodeType === 1) { + const computedStyle = getComputedStyle($ancestor); + if (computedStyle.transform && computedStyle.transform !== 'none') { + const inlineTransform = $ancestor.style.transform || ''; + const inlineTransition = muteElementTransition($ancestor); + rootAncestorTransformStore.push($ancestor, inlineTransform, inlineTransition); + $ancestor.style.transform = 'none'; + } + $ancestor = $ancestor.parentElement; + } + + for (let i = 0, l = toParse.length; i < l; i++) { + const child = toParse[i]; + scoped[i] = isStr(child) ? scopeRoot.querySelectorAll(child) : child; + } + + const parsedChildren = registerTargets(scoped); + + this.nodes.clear(); + this.rootNodes.clear(); + + const rootNode = this.registerElement(root, null); + // Root node are always targets + rootNode.isTarget = true; + this.rootNode = rootNode; + + const inRootNodeIds = new Set(); + // Update index and total for inital timing calculation + let index = 0; + const allNodeTargets = []; + this.nodes.forEach((node) => { allNodeTargets.push(node.$el); }); + this.nodes.forEach((node, id) => { + node.index = index++; + node.targets = allNodeTargets; + // Track ids of nodes that belong to the current root to filter detached matches + if (node && node.measuredIsInsideRoot) { + inRootNodeIds.add(id); + } + }); + + // Elements with a layout id outside the root that match the children selector + const detachedElementsLookup = new Set(); + const orderedDetachedElements = []; + + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + if (!$el || $el.nodeType !== 1 || $el === root) continue; + const insideRoot = isElementInRoot(root, $el); + if (!insideRoot) { + const layoutNodeId = $el.dataset.layoutId; + if (!layoutNodeId || !inRootNodeIds.has(layoutNodeId)) continue; + } + if (!detachedElementsLookup.has($el)) { + detachedElementsLookup.add($el); + orderedDetachedElements.push($el); + } + } + + for (let i = 0, l = orderedDetachedElements.length; i < l; i++) { + this.ensureDetachedNode(orderedDetachedElements[i], detachedElementsLookup); + } + + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + const node = this.getNode($el); + if (node) { + let cur = node; + while (cur) { + if (cur.isTarget) break; + cur.isTarget = true; + cur = cur.parentNode; + } + } + } + + this.scrollX = window.scrollX; + this.scrollY = window.scrollY; + + this.forEachNode(restoreNodeTransform); + + // Restore transition and transforms of root ancestors + + for (let i = 0, l = rootAncestorTransformStore.length; i < l; i += 3) { + const $el = /** @type {DOMTarget} */(rootAncestorTransformStore[i]); + const inlineTransform = /** @type {String} */(rootAncestorTransformStore[i + 1]); + const inlineTransition = /** @type {String|null} */(rootAncestorTransformStore[i + 2]); + if (inlineTransform && inlineTransform !== '') { + $el.style.transform = inlineTransform; + } else { + $el.style.removeProperty('transform'); + } + restoreElementTransition($el, inlineTransition); + } + + return this; + } +} + +/** + * @param {LayoutStateParams} params + * @return {[LayoutStateAnimationProperties, LayoutAnimationTimingsParams]} + */ +function splitPropertiesFromParams(params) { + /** @type {LayoutStateAnimationProperties} */ + const properties = {}; + /** @type {LayoutAnimationTimingsParams} */ + const parameters = {}; + for (let name in params) { + const value = params[name]; + const isEase = name === 'ease'; + const isTiming = name === 'duration' || name === 'delay'; + if (isTiming || isEase) { + if (isEase) { + parameters[name] = /** @type {EasingParam} */(value); + } else { + parameters[name] = /** @type {Number|FunctionValue} */(value); + } + } else { + properties[name] = /** @type {Number|String} */(value); + } + } + return [properties, parameters]; +} + +class AutoLayout { + /** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + */ + constructor(root, params = {}) { + if (scope.current) scope.current.register(this); + const swapAtSplitParams = splitPropertiesFromParams(params.swapAt); + const enterFromSplitParams = splitPropertiesFromParams(params.enterFrom); + const leaveToSplitParams = splitPropertiesFromParams(params.leaveTo); + const transitionProperties = params.properties; + /** @type {Number|FunctionValue} */ + params.duration = setValue(params.duration, 350); + /** @type {Number|FunctionValue} */ + params.delay = setValue(params.delay, 0); + /** @type {EasingParam|FunctionValue} */ + params.ease = setValue(params.ease, 'inOut(3.5)'); + /** @type {AutoLayoutParams} */ + this.params = params; + /** @type {DOMTarget} */ + this.root = /** @type {DOMTarget} */(registerTargets(root)[0]); + /** @type {Number|String} */ + this.id = params.id || layoutId++; + /** @type {LayoutChildrenParam} */ + this.children = params.children || '*'; + /** @type {Boolean} */ + this.absoluteCoords = false; + /** @type {LayoutStateParams} */ + this.swapAtParams = mergeObjects(params.swapAt || { opacity: 0 }, { ease: 'inOut(1.75)' }); + /** @type {LayoutStateParams} */ + this.enterFromParams = params.enterFrom || { opacity: 0 }; + /** @type {LayoutStateParams} */ + this.leaveToParams = params.leaveTo || { opacity: 0 }; + /** @type {Set} */ + this.properties = new Set([ + 'opacity', + 'fontSize', + 'color', + 'backgroundColor', + 'borderRadius', + 'border', + 'filter', + 'clipPath', + ]); + if (swapAtSplitParams[0]) for (let name in swapAtSplitParams[0]) this.properties.add(name); + if (enterFromSplitParams[0]) for (let name in enterFromSplitParams[0]) this.properties.add(name); + if (leaveToSplitParams[0]) for (let name in leaveToSplitParams[0]) this.properties.add(name); + if (transitionProperties) for (let i = 0, l = transitionProperties.length; i < l; i++) this.properties.add(transitionProperties[i]); + /** @type {Set} */ + this.recordedProperties = new Set([ + 'display', + 'visibility', + 'translate', + 'position', + 'left', + 'top', + 'marginLeft', + 'marginTop', + 'width', + 'height', + 'maxWidth', + 'maxHeight', + 'minWidth', + 'minHeight', + ]); + this.properties.forEach(prop => this.recordedProperties.add(prop)); + /** @type {WeakSet} */ + this.pendingRemoval = new WeakSet(); + /** @type {Map} */ + this.transitionMuteStore = new Map(); + /** @type {LayoutSnapshot} */ + this.oldState = new LayoutSnapshot(this); + /** @type {LayoutSnapshot} */ + this.newState = new LayoutSnapshot(this); + /** @type {Timeline} */ + this.timeline = null; + /** @type {WAAPIAnimation} */ + this.transformAnimation = null; + /** @type {Array} */ + this.animating = []; + /** @type {Array} */ + this.swapping = []; + /** @type {Array} */ + this.leaving = []; + /** @type {Array} */ + this.entering = []; + // Record the current state as the old state to init the data attributes and allow imediate .animate() + this.oldState.record(); + // And all layout transition muted during the record + restoreLayoutTransition(this.transitionMuteStore); + } + + /** + * @return {this} + */ + revert() { + this.root.classList.remove('is-animated'); + if (this.timeline) { + this.timeline.complete(); + this.timeline = null; + } + if (this.transformAnimation) { + this.transformAnimation.complete(); + this.transformAnimation = null; + } + this.animating.length = this.swapping.length = this.leaving.length = this.entering.length = 0; + this.oldState.revert(); + this.newState.revert(); + requestAnimationFrame(() => restoreLayoutTransition(this.transitionMuteStore)); + return this; + } + + /** + * @return {this} + */ + record() { + // Commit transforms before measuring + if (this.transformAnimation) { + this.transformAnimation.cancel(); + this.transformAnimation = null; + } + // Record the old state + this.oldState.record(); + // Cancel any running timeline + if (this.timeline) { + this.timeline.cancel(); + this.timeline = null; + } + // Restore previously captured inline styles + this.newState.forEachRootNode(restoreNodeInlineStyles); + return this; + } + + /** + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + animate(params = {}) { + /** @type { LayoutAnimationTimingsParams } */ + const animationTimings = { + ease: setValue(params.ease, this.params.ease), + delay: setValue(params.delay, this.params.delay), + duration: setValue(params.duration, this.params.duration), + }; + /** @type {TimelineParams} */ + const tlParams = { + id: this.id + }; + const onComplete = setValue(params.onComplete, this.params.onComplete); + const onPause = setValue(params.onPause, this.params.onPause); + for (let name in defaults) { + if (name !== 'ease' && name !== 'duration' && name !== 'delay') { + if (!isUnd(params[name])) { + tlParams[name] = params[name]; + } else if (!isUnd(this.params[name])) { + tlParams[name] = this.params[name]; + } + } + } + tlParams.onComplete = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const ed = globals.editor; + const isScrollControled = (ap && ap.linked) || (ed && ed.showPanel); + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + return; + } + // Make sure to call .cancel() after restoreNodeInlineStyles(node); otehrwise the commited styles get reverted + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(node => { + restoreNodeVisualState(node); + restoreNodeInlineStyles(node); + }); + for (let i = 0, l = transformed.length; i < l; i++) { + const $el = transformed[i]; + $el.style.transform = newState.getComputedValue($el, 'transform'); + } + if (this.root.classList.contains('is-animated')) { + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + } + // Avoid CSS transitions at the end of the animation by restoring them on the next frame + requestAnimationFrame(() => { + if (this.root.classList.contains('is-animated')) return; + restoreLayoutTransition(this.transitionMuteStore); + }); + }; + tlParams.onPause = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const isScrollControled = ap && ap.linked; + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + return; + } + if (!this.root.classList.contains('is-animated')) return; + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(restoreNodeVisualState); + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + }; + tlParams.composition = false; + + const swapAtParams = mergeObjects(mergeObjects(params.swapAt || {}, this.swapAtParams), animationTimings); + const enterFromParams = mergeObjects(mergeObjects(params.enterFrom || {}, this.enterFromParams), animationTimings); + const leaveToParams = mergeObjects(mergeObjects(params.leaveTo || {}, this.leaveToParams), animationTimings); + const [ swapAtProps, swapAtTimings ] = splitPropertiesFromParams(swapAtParams); + const [ enterFromProps, enterFromTimings ] = splitPropertiesFromParams(enterFromParams); + const [ leaveToProps, leaveToTimings ] = splitPropertiesFromParams(leaveToParams); + + const oldState = this.oldState; + const newState = this.newState; + const animating = this.animating; + const swapping = this.swapping; + const entering = this.entering; + const leaving = this.leaving; + const pendingRemoval = this.pendingRemoval; + + animating.length = swapping.length = entering.length = leaving.length = 0; + + // Mute old state CSS transitions to prevent wrong properties calculation + oldState.forEachRootNode(muteNodeTransition); + // Capture the new state before animation + newState.record(); + newState.forEachRootNode(recordNodeInlineStyles); + + const targets = []; + const animated = []; + const transformed = []; + const animatedSwap = []; + const rootNode = newState.rootNode; + const $root = rootNode.$el; + + newState.forEachRootNode(node => { + + const $el = node.$el; + const id = node.id; + const parent = node.parentNode; + const parentAdded = parent ? parent.branchAdded : false; + const parentRemoved = parent ? parent.branchRemoved : false; + const parentNotRendered = parent ? parent.branchNotRendered : false; + + let oldStateNode = oldState.nodes.get(id); + + const hasNoOldState = !oldStateNode; + + if (hasNoOldState) { + oldStateNode = cloneNodeProperties(node, /** @type {LayoutNode} */({}), oldState); + oldState.nodes.set(id, oldStateNode); + oldStateNode.measuredIsRemoved = true; + } else if (oldStateNode.measuredIsRemoved && !node.measuredIsRemoved) { + cloneNodeProperties(node, oldStateNode, oldState); + oldStateNode.measuredIsRemoved = true; + } + + const oldParentNode = oldStateNode.parentNode; + const oldParentId = oldParentNode ? oldParentNode.id : null; + const newParentId = parent ? parent.id : null; + const parentChanged = oldParentId !== newParentId; + const elementChanged = oldStateNode.$el !== node.$el; + const wasRemovedBefore = oldStateNode.measuredIsRemoved; + const isRemovedNow = node.measuredIsRemoved; + + // Recalculate postion relative to their parent for elements that have been moved + if (!oldStateNode.measuredIsRemoved && !isRemovedNow && !hasNoOldState && (parentChanged || elementChanged)) { + const oldAbsoluteLeft = oldStateNode.properties.left; + const oldAbsoluteTop = oldStateNode.properties.top; + const newParent = parent || newState.rootNode; + const oldParent = newParent.id ? oldState.nodes.get(newParent.id) : null; + const parentLeft = oldParent ? oldParent.properties.left : newParent.properties.left; + const parentTop = oldParent ? oldParent.properties.top : newParent.properties.top; + const borderLeft = oldParent ? oldParent.properties.clientLeft : newParent.properties.clientLeft; + const borderTop = oldParent ? oldParent.properties.clientTop : newParent.properties.clientTop; + oldStateNode.properties.x = oldAbsoluteLeft - parentLeft - borderLeft; + oldStateNode.properties.y = oldAbsoluteTop - parentTop - borderTop; + } + + if (node.hasVisibilitySwap) { + if (node.hasVisibilityHidden) { + node.$el.style.visibility = 'visible'; + node.$measure.style.visibility = 'hidden'; + } + if (node.hasDisplayNone) { + node.$el.style.display = oldStateNode.measuredDisplay || node.measuredDisplay || ''; + // Setting visibility 'hidden' instead of display none to avoid calculation issues + node.$measure.style.visibility = 'hidden'; + // @TODO: check why setting display here can cause calculation issues + // node.$measure.style.display = 'none'; + } + } + + const wasPendingRemoval = pendingRemoval.has($el); + const wasVisibleBefore = oldStateNode.measuredIsVisible; + const isVisibleNow = node.measuredIsVisible; + const becomeVisible = !wasVisibleBefore && isVisibleNow && !parentNotRendered; + const topLevelAdded = !isRemovedNow && (wasRemovedBefore || wasPendingRemoval) && !parentAdded; + const newlyRemoved = isRemovedNow && !wasRemovedBefore && !parentRemoved; + const topLevelRemoved = newlyRemoved || isRemovedNow && wasPendingRemoval && !parentRemoved; + + node.branchAdded = parentAdded || topLevelAdded; + node.branchRemoved = parentRemoved || topLevelRemoved; + node.branchNotRendered = parentNotRendered || isRemovedNow; + + if (isRemovedNow && wasVisibleBefore) { + node.$el.style.display = oldStateNode.measuredDisplay; + node.$el.style.visibility = 'visible'; + cloneNodeProperties(oldStateNode, node, newState); + } + + // Node is leaving + if (newlyRemoved) { + if (node.isTarget) { + leaving.push($el); + node.isLeaving = true; + } + pendingRemoval.add($el); + } else if (!isRemovedNow && wasPendingRemoval) { + pendingRemoval.delete($el); + } + + // Node is entering + if ((topLevelAdded && !parentNotRendered) || becomeVisible) { + updateNodeProperties(oldStateNode, enterFromProps); + if (node.isTarget) { + entering.push($el); + node.isEntering = true; + } + // Node is leaving + } else if (topLevelRemoved && !parentNotRendered) { + updateNodeProperties(node, leaveToProps); + } + + // Node is animating + // The animating array is used only to calculate delays and duration on root children + if (node !== rootNode && node.isTarget && !node.isEntering && !node.isLeaving) { + animating.push($el); + } + + targets.push($el); + + }); + + let enteringIndex = 0; + let leavingIndex = 0; + let animatingIndex = 0; + + newState.forEachRootNode(node => { + + const $el = node.$el; + const parent = node.parentNode; + const oldStateNode = oldState.nodes.get(node.id); + const nodeProperties = node.properties; + const oldStateNodeProperties = oldStateNode.properties; + + // Use closest animated parent index and total values so that children staggered delays are in sync with their parent + let animatedParent = parent !== rootNode && parent; + while (animatedParent && !animatedParent.isTarget && animatedParent !== rootNode) { + animatedParent = animatedParent.parentNode; + } + + // Root is always animated first in sync with the first child (animating.length is the total of children) + if (node === rootNode) { + node.index = 0; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); + } else if (node.isEntering) { + node.index = animatedParent ? animatedParent.index : enteringIndex; + node.targets = animatedParent ? animating : entering; + updateNodeTimingParams(node, enterFromTimings); + enteringIndex++; + } else if (node.isLeaving) { + node.index = animatedParent ? animatedParent.index : leavingIndex; + node.targets = animatedParent ? animating : leaving; + leavingIndex++; + updateNodeTimingParams(node, leaveToTimings); + } else if (node.isTarget) { + node.index = animatingIndex++; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); + } else { + node.index = animatedParent ? animatedParent.index : 0; + node.targets = animating; + updateNodeTimingParams(node, swapAtTimings); + } + + // Make sure the old state node has its inex and total values up to date for valid "from" function values calculation + oldStateNode.index = node.index; + oldStateNode.targets = node.targets; + + // Computes all values up front so we can check for changes and we don't have to re-compute them inside the animation props + for (let prop in nodeProperties) { + nodeProperties[prop] = getFunctionValue(nodeProperties[prop], $el, node.index, node.targets, null, null); + oldStateNodeProperties[prop] = getFunctionValue(oldStateNodeProperties[prop], $el, oldStateNode.index, oldStateNode.targets, null, null); + } + + // Use a 1px tolerance to detect dimensions changes to prevent width / height animations on barelly visible elements + const sizeTolerance = 1; + const widthChanged = Math.abs(nodeProperties.width - oldStateNodeProperties.width) > sizeTolerance; + const heightChanged = Math.abs(nodeProperties.height - oldStateNodeProperties.height) > sizeTolerance; + + node.sizeChanged = (widthChanged || heightChanged); + + // const hiddenStateChanged = (topLevelAdded || newlyRemoved) && wasRemovedBefore !== isRemovedNow; + + if (node.isTarget && (!node.measuredIsRemoved && oldStateNode.measuredIsVisible || node.measuredIsRemoved && node.measuredIsVisible)) { + if (nodeProperties.transform !== 'none' || oldStateNodeProperties.transform !== 'none') { + node.hasTransform = true; + transformed.push($el); + } + for (let prop in nodeProperties) { + // if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop] || hiddenStateChanged)) { + if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop])) { + animated.push($el); + break; + } + } + } + + if (!node.isTarget) { + swapping.push($el); + if (node.sizeChanged && parent && parent.isTarget && parent.sizeChanged) { + if (swapAtProps.transform) { + node.hasTransform = true; + transformed.push($el); + } + animatedSwap.push($el); + } + } + + }); + + const timingParams = { + delay: (/** @type {HTMLElement} */$el) => newState.getNode($el).delay, + duration: (/** @type {HTMLElement} */$el) => newState.getNode($el).duration, + ease: (/** @type {HTMLElement} */$el) => newState.getNode($el).ease, + }; + + tlParams.defaults = timingParams; + + this.timeline = createTimeline(tlParams); + + // Imediatly return the timeline if no layout changes detected + if (!animated.length && !transformed.length && !swapping.length) { + // Make sure to restore all CSS transition if no animation + restoreLayoutTransition(this.transitionMuteStore); + return this.timeline.complete(); + } + + if (targets.length) { + + this.root.classList.add('is-animated'); + + for (let i = 0, l = targets.length; i < l; i++) { + const $el = targets[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; + + // muteNodeTransition(newNode); + + // Don't animate positions of inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + // Display grid can mess with the absolute positioning, so set it to block during transition + if (oldNode.measuredDisplay === 'grid' || newNode.measuredDisplay === 'grid') $el.style.setProperty('display', 'block', 'important'); + // All children must be in position absolute or fixed + if ($el !== $root || this.absoluteCoords) { + $el.style.position = this.absoluteCoords ? 'fixed' : 'absolute'; + $el.style.left = '0px'; + $el.style.top = '0px'; + $el.style.marginLeft = '0px'; + $el.style.marginTop = '0px'; + $el.style.translate = `${oldNodeState.x}px ${oldNodeState.y}px`; + } + if ($el === $root && newNode.measuredPosition === 'static') { + $el.style.position = 'relative'; + // Cancel left / trop in case the static element had muted values now activated by potision relative + $el.style.left = '0px'; + $el.style.top = '0px'; + } + } + // Animate dimensions for all elements (including inlined) + $el.style.width = `${oldNodeState.width}px`; + $el.style.height = `${oldNodeState.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + } + + // Restore the scroll position if the oldState differs from the current state + if (oldState.scrollX !== window.scrollX || oldState.scrollY !== window.scrollY) { + // Restoring in the next frame avoids race conditions if for example a waapi animation commit styles that affect the root height + requestAnimationFrame(() => window.scrollTo(oldState.scrollX, oldState.scrollY)); + } + + for (let i = 0, l = animated.length; i < l; i++) { + const $el = animated[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; + const newNodeState = newNode.properties; + let nodeHasChanged = false; + /** @type {AnimationParams} */ + const animatedProps = { + composition: 'none', + }; + if (oldNodeState.width !== newNodeState.width) { + animatedProps.width = [oldNodeState.width, newNodeState.width]; + nodeHasChanged = true; + } + if (oldNodeState.height !== newNodeState.height) { + animatedProps.height = [oldNodeState.height, newNodeState.height]; + nodeHasChanged = true; + } + // If the node has transforms we handle the translate animation in waapi otherwise translate and other transforms can be out of sync + // And we don't animate the position of inlined elements + if (!newNode.hasTransform && !newNode.isInlined) { + animatedProps.translate = [`${oldNodeState.x}px ${oldNodeState.y}px`, `${newNodeState.x}px ${newNodeState.y}px`]; + nodeHasChanged = true; + } + this.properties.forEach(prop => { + const oldVal = oldNodeState[prop]; + const newVal = newNodeState[prop]; + if (prop !== 'transform' && oldVal !== newVal) { + animatedProps[prop] = [oldVal, newVal]; + nodeHasChanged = true; + } + }); + if (nodeHasChanged) { + this.timeline.add($el, animatedProps, 0); + } + } + + } + + if (swapping.length) { + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const oldNode = oldState.getNode($el); + const oldNodeProps = oldNode.properties; + $el.style.width = `${oldNodeProps.width}px`; + $el.style.height = `${oldNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // We don't animate the position of inlined elements + if (!oldNode.isInlined) { + $el.style.translate = `${oldNodeProps.x}px ${oldNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${oldState.getComputedValue($el, prop)}`; + } + }); + } + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const newNode = newState.getNode($el); + const newNodeProps = newNode.properties; + this.timeline.call(() => { + $el.style.width = `${newNodeProps.width}px`; + $el.style.height = `${newNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // Don't set translate for inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + $el.style.translate = `${newNodeProps.x}px ${newNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${newState.getComputedValue($el, prop)}`; + } + }); + }, newNode.delay + newNode.duration / 2); + } + + if (animatedSwap.length) { + const ease = parseEase(newState.nodes.get(animatedSwap[0].dataset.layoutId).ease); + const inverseEased = t => 1 - ease(1 - t); + const animatedSwapParams = /** @type {AnimationParams} */({}); + if (swapAtProps) { + for (let prop in swapAtProps) { + if (prop !== 'transform') { + animatedSwapParams[prop] = [ + { from: (/** @type {HTMLElement} */$el) => oldState.getComputedValue($el, prop), to: swapAtProps[prop] }, + { from: swapAtProps[prop], to: (/** @type {HTMLElement} */$el) => newState.getComputedValue($el, prop), ease: inverseEased } + ]; + } + } + } + this.timeline.add(animatedSwap, animatedSwapParams, 0); + } + + } + + const transformedLength = transformed.length; + + if (transformedLength) { + // We only need to set the transform property here since translate is already defined in the targets loop + for (let i = 0; i < transformedLength; i++) { + const $el = transformed[i]; + const node = newState.getNode($el); + // Don't set translate for inlined elements (to avoid text reflow) + if (!node.isInlined) { + $el.style.translate = `${oldState.getComputedValue($el, 'x')}px ${oldState.getComputedValue($el, 'y')}px`; + } + $el.style.transform = oldState.getComputedValue($el, 'transform'); + if (animatedSwap.includes($el)) { + node.ease = getFunctionValue(swapAtParams.ease, $el, node.index, node.targets, null, null); + node.duration = getFunctionValue(swapAtParams.duration, $el, node.index, node.targets, null, null); + } + } + this.transformAnimation = waapi.animate(transformed, { + translate: (/** @type {HTMLElement} */$el) => { + const node = newState.getNode($el); + // Don't animate translate for inlined elements (to avoid text reflow) + if (node.isInlined) return '0px 0px'; + return `${newState.getComputedValue($el, 'x')}px ${newState.getComputedValue($el, 'y')}px`; + }, + transform: (/** @type {HTMLElement} */$el) => { + const newValue = newState.getComputedValue($el, 'transform'); + if (!animatedSwap.includes($el)) return newValue; + const oldValue = oldState.getComputedValue($el, 'transform'); + const node = newState.getNode($el); + return [oldValue, getFunctionValue(swapAtProps.transform, $el, node.index, node.targets, null, null), newValue] + }, + autoplay: false, + // persist: true, + ...timingParams, + }); + this.timeline.sync(this.transformAnimation, 0); + } + + return this.timeline.init(); + } + + /** + * @param {(layout: this) => void} callback + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + update(callback, params = {}) { + this.record(); + callback(this); + return this.animate(params); + } +} + +/** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + * @return {AutoLayout} + */ +const createLayout = (root, params) => new AutoLayout(root, params); + +export { AutoLayout, createLayout }; diff --git a/dist/modules/scope/index.cjs b/dist/modules/scope/index.cjs index 2c27b00eb..938faf7b1 100644 --- a/dist/modules/scope/index.cjs +++ b/dist/modules/scope/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - scope - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/scope/index.js b/dist/modules/scope/index.js index 7fc6a9c3f..ee31220de 100644 --- a/dist/modules/scope/index.js +++ b/dist/modules/scope/index.js @@ -1,8 +1,8 @@ /** * Anime.js - scope - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { Scope, createScope } from './scope.js'; diff --git a/dist/modules/scope/scope.cjs b/dist/modules/scope/scope.cjs index b028752a9..9a6ed9d81 100644 --- a/dist/modules/scope/scope.cjs +++ b/dist/modules/scope/scope.cjs @@ -1,8 +1,8 @@ /** * Anime.js - scope - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/scope/scope.js b/dist/modules/scope/scope.js index d046d0784..86f80e1ea 100644 --- a/dist/modules/scope/scope.js +++ b/dist/modules/scope/scope.js @@ -1,8 +1,8 @@ /** * Anime.js - scope - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { doc, win } from '../core/consts.js'; diff --git a/dist/modules/svg/drawable.cjs b/dist/modules/svg/drawable.cjs index 4f3fb0464..59e8977d4 100644 --- a/dist/modules/svg/drawable.cjs +++ b/dist/modules/svg/drawable.cjs @@ -1,8 +1,8 @@ /** * Anime.js - svg - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/svg/drawable.js b/dist/modules/svg/drawable.js index 0482e3662..5ce0328df 100644 --- a/dist/modules/svg/drawable.js +++ b/dist/modules/svg/drawable.js @@ -1,8 +1,8 @@ /** * Anime.js - svg - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { proxyTargetSymbol, K } from '../core/consts.js'; diff --git a/dist/modules/svg/helpers.cjs b/dist/modules/svg/helpers.cjs index 0b0dd354c..f796306c0 100644 --- a/dist/modules/svg/helpers.cjs +++ b/dist/modules/svg/helpers.cjs @@ -1,8 +1,8 @@ /** * Anime.js - svg - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/svg/helpers.js b/dist/modules/svg/helpers.js index 196baa8f2..a1bce9eff 100644 --- a/dist/modules/svg/helpers.js +++ b/dist/modules/svg/helpers.js @@ -1,8 +1,8 @@ /** * Anime.js - svg - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { isSvg } from '../core/helpers.js'; diff --git a/dist/modules/svg/index.cjs b/dist/modules/svg/index.cjs index cc9696141..a5f6fb790 100644 --- a/dist/modules/svg/index.cjs +++ b/dist/modules/svg/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - svg - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/svg/index.js b/dist/modules/svg/index.js index 42ad08241..54bb90fbc 100644 --- a/dist/modules/svg/index.js +++ b/dist/modules/svg/index.js @@ -1,8 +1,8 @@ /** * Anime.js - svg - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { createMotionPath } from './motionpath.js'; diff --git a/dist/modules/svg/morphto.cjs b/dist/modules/svg/morphto.cjs index 6eb028309..2a4c3f3d2 100644 --- a/dist/modules/svg/morphto.cjs +++ b/dist/modules/svg/morphto.cjs @@ -1,13 +1,12 @@ /** * Anime.js - svg - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; -var consts = require('../core/consts.cjs'); var helpers$1 = require('../core/helpers.cjs'); var helpers = require('./helpers.cjs'); @@ -23,7 +22,7 @@ var helpers = require('./helpers.cjs'); * @param {Number} [precision] * @return {FunctionValue} */ -const morphTo = (path2, precision = .33) => ($path1) => { +const morphTo = (path2, precision = .33) => ($path1, index, total, prevTween) => { const tagName1 = ($path1.tagName || '').toLowerCase(); if (!tagName1.match(/^(path|polygon|polyline)$/)) { throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use , or .`); @@ -38,7 +37,7 @@ const morphTo = (path2, precision = .33) => ($path1) => { } const isPath = $path1.tagName === 'path'; const separator = isPath ? ' ' : ','; - const previousPoints = $path1[consts.morphPointsSymbol]; + const previousPoints = prevTween ? prevTween._value : null; if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints); let v1 = '', v2 = ''; @@ -60,8 +59,6 @@ const morphTo = (path2, precision = .33) => ($path1) => { } } - $path1[consts.morphPointsSymbol] = v2; - return [v1, v2]; }; diff --git a/dist/modules/svg/morphto.js b/dist/modules/svg/morphto.js index 8468193af..8482a381d 100644 --- a/dist/modules/svg/morphto.js +++ b/dist/modules/svg/morphto.js @@ -1,11 +1,10 @@ /** * Anime.js - svg - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -import { morphPointsSymbol } from '../core/consts.js'; import { round } from '../core/helpers.js'; import { getPath } from './helpers.js'; @@ -21,7 +20,7 @@ import { getPath } from './helpers.js'; * @param {Number} [precision] * @return {FunctionValue} */ -const morphTo = (path2, precision = .33) => ($path1) => { +const morphTo = (path2, precision = .33) => ($path1, index, total, prevTween) => { const tagName1 = ($path1.tagName || '').toLowerCase(); if (!tagName1.match(/^(path|polygon|polyline)$/)) { throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use , or .`); @@ -36,7 +35,7 @@ const morphTo = (path2, precision = .33) => ($path1) => { } const isPath = $path1.tagName === 'path'; const separator = isPath ? ' ' : ','; - const previousPoints = $path1[morphPointsSymbol]; + const previousPoints = prevTween ? prevTween._value : null; if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints); let v1 = '', v2 = ''; @@ -58,8 +57,6 @@ const morphTo = (path2, precision = .33) => ($path1) => { } } - $path1[morphPointsSymbol] = v2; - return [v1, v2]; }; diff --git a/dist/modules/svg/motionpath.cjs b/dist/modules/svg/motionpath.cjs index 60a978364..b97afb01d 100644 --- a/dist/modules/svg/motionpath.cjs +++ b/dist/modules/svg/motionpath.cjs @@ -1,8 +1,8 @@ /** * Anime.js - svg - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/svg/motionpath.js b/dist/modules/svg/motionpath.js index 27902cca8..81eb67475 100644 --- a/dist/modules/svg/motionpath.js +++ b/dist/modules/svg/motionpath.js @@ -1,8 +1,8 @@ /** * Anime.js - svg - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { isSvgSymbol } from '../core/consts.js'; diff --git a/dist/modules/text/index.cjs b/dist/modules/text/index.cjs index 2c2d7519a..2cd155341 100644 --- a/dist/modules/text/index.cjs +++ b/dist/modules/text/index.cjs @@ -1,16 +1,18 @@ /** * Anime.js - text - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; var split = require('./split.cjs'); +var scramble = require('./scramble.cjs'); exports.TextSplitter = split.TextSplitter; exports.split = split.split; exports.splitText = split.splitText; +exports.scrambleText = scramble.scrambleText; diff --git a/dist/modules/text/index.d.ts b/dist/modules/text/index.d.ts index 35a025e60..6f257136f 100644 --- a/dist/modules/text/index.d.ts +++ b/dist/modules/text/index.d.ts @@ -1 +1,2 @@ export * from "./split.js"; +export * from "./scramble.js"; diff --git a/dist/modules/text/index.js b/dist/modules/text/index.js index 70472cea0..08cbcb84b 100644 --- a/dist/modules/text/index.js +++ b/dist/modules/text/index.js @@ -1,8 +1,9 @@ /** * Anime.js - text - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { TextSplitter, split, splitText } from './split.js'; +export { scrambleText } from './scramble.js'; diff --git a/dist/modules/text/scramble.cjs b/dist/modules/text/scramble.cjs new file mode 100644 index 000000000..21c76a569 --- /dev/null +++ b/dist/modules/text/scramble.cjs @@ -0,0 +1,272 @@ +/** + * Anime.js - text - CJS + * @version v4.4.1 + * @license MIT + * @copyright 2026 - Julian Garnier + */ + +'use strict'; + +var random = require('../utils/random.cjs'); +var globals = require('../core/globals.cjs'); +var helpers = require('../core/helpers.cjs'); +var parser = require('../easings/eases/parser.cjs'); +var consts = require('../core/consts.cjs'); + +/** + * @import { + * ScrambleTextParams, + * FunctionValue, + * } from '../types/index.js' +*/ + +/** + * '-' is the range operator; place it at the start or end of the string to use it as a literal (e.g. '-abc' or 'abc-') + * @param {String} str + * @return {String} + */ +const expandCharRanges = (str) => { + let result = ''; + for (let i = 0, l = str.length; i < l; i++) { + if (i + 2 < l && str[i + 1] === '-' && str.charCodeAt(i) < str.charCodeAt(i + 2)) { + const start = str.charCodeAt(i); + const end = str.charCodeAt(i + 2); + for (let c = start; c <= end; c++) result += String.fromCharCode(c); + i += 2; + } else { + result += str[i]; + } + } + return result; +}; + +const charSets = { + lowercase: 'a-z', + uppercase: 'A-Z', + numbers: '0-9', + symbols: '!%#_|*+=', + braille: 'â €-⣿', + blocks: 'â–€-â–Ÿ', + shades: 'â–‘-â–“', +}; + +const originalTexts = new WeakMap(); + +/** + * Returns a function-based tween value that scrambles the target's text content, + * progressively revealing the original text. + * + * @param {ScrambleTextParams} [params] + * @return {FunctionValue} + */ +const scrambleText = (params = {}) => { + if (!params) params = {}; + const charsParam = params.chars; + const easeFn = parser.parseEase(params.ease || 'linear'); + const text = params.text; + const fromParam = params.from; + const reversed = params.reversed || false; + const perturbation = params.perturbation || 0; + const cursorParam = params.cursor; + const cursorChars = cursorParam === true ? '_' + : typeof cursorParam === 'number' ? String.fromCharCode(cursorParam) + : typeof cursorParam === 'string' ? cursorParam + : ''; + const cursorLen = cursorChars.length; + const seed = params.seed || 0; + const override = params.override !== undefined ? params.override : true; + const revealRate = params.revealRate || 60; + const interval = 1000 * globals.globals.timeScale / revealRate; + const settleDuration = params.settleDuration || 300 * globals.globals.timeScale; + const settleRate = params.settleRate || 30; + const durationParam = params.duration; + const revealDelayParam = params.revealDelay; + const delayParam = params.delay; + const onChange = params.onChange || consts.noop; + + return (target, index, targets, prevTween) => { + const rawChars = typeof charsParam === 'function' ? charsParam(target, index, targets) : (charsParam || 'a-zA-Z0-9!%#_'); + const characters = expandCharRanges(charSets[rawChars] || rawChars); + const totalChars = characters.length - 1; + const duration = typeof durationParam === 'function' ? durationParam(target, index, targets) : durationParam; + const revealDelay = typeof revealDelayParam === 'function' ? revealDelayParam(target, index, targets) : (revealDelayParam || 0); + const delay = typeof delayParam === 'function' ? delayParam(target, index, targets) : (delayParam || 0); + const rng = seed ? random.createSeededRandom(seed) : random.createSeededRandom(); + if (!originalTexts.has(target)) originalTexts.set(target, target.textContent); + const startingText = prevTween ? prevTween._value : target.textContent; + const targetText = text !== undefined + ? (typeof text === 'function' ? text(target, index, targets) : text) + : prevTween ? prevTween._value + : originalTexts.get(target); + const settledText = targetText === ' ' || targetText === ' ' ? ' ' : targetText; + const startLength = startingText === ' ' ? 0 : startingText.length; + const endLength = settledText.length; + const overrideChars = override === true ? characters + : typeof override === 'string' && override.length > 0 ? expandCharRanges(charSets[/** @type {String} */(override)] || /** @type {String} */(override)) + : null; + const totalOverrideChars = overrideChars ? overrideChars.length - 1 : 0; + // Space override uses   so the browser doesn't collapse consecutive spaces in innerHTML + const overrideChar = override === ' ' ? ' ' : null; + // When starting from blank, only animate the target text length to avoid padding beyond it + const animLength = override === '' ? endLength : Math.max(startLength, endLength); + // Compute total duration from interval spacing and settle time, or use the explicit duration + const animDuration = duration > 0 ? duration : (animLength - 1) * interval + settleDuration; + const computedDuration = helpers.round((animDuration + revealDelay) / globals.globals.timeScale, 0) * globals.globals.timeScale; + const revealDelayRatio = revealDelay > 0 ? helpers.round(revealDelay / computedDuration, 12) : 0; + // Auto-resolve reveal direction: shrinking text reveals from right, growing from left + const resolvedFrom = fromParam === undefined || fromParam === 'auto' ? (endLength < startLength ? 'right' : 'left') : fromParam; + const charOrder = new Int32Array(animLength); + if (resolvedFrom === 'random') { + for (let i = 0; i < animLength; i++) charOrder[i] = i; + for (let i = animLength - 1; i > 0; i--) { + const j = rng(0, i); + const t = charOrder[i]; charOrder[i] = charOrder[j]; charOrder[j] = t; + } + } else { + const ref = resolvedFrom === 'right' ? (override === '' || !startLength ? animLength : startLength) - 1 + : resolvedFrom === 'center' ? ((override === '' || !startLength ? animLength : startLength) - 1) / 2 + : typeof resolvedFrom === 'number' ? resolvedFrom + : 0; + const abs = Math.abs; + const indices = new Array(animLength); + for (let i = 0; i < animLength; i++) indices[i] = i; + indices.sort((a, b) => abs(a - ref) - abs(b - ref)); + for (let i = 0; i < animLength; i++) charOrder[indices[i]] = i; + } + if (reversed) { + const last = animLength - 1; + for (let i = 0; i < animLength; i++) charOrder[i] = last - charOrder[i]; + } + // settleRatio is the fraction of the animation each character spends in the active scrambling zone + const settleRatio = helpers.round(settleDuration / animDuration, 12); + // settleSpacing is the time gap between consecutive characters entering the active zone + const settleSpacing = helpers.round((1 - settleRatio) / animLength, 12); + const cursorZone = cursorLen * settleSpacing; + // stepRatio controls how often scramble characters refresh (based on settleRate) + const stepRatio = helpers.round(1000 * globals.globals.timeScale / (settleRate * computedDuration), 12); + // Pre-compute per-character start and settle times + const charStarts = new Float32Array(animLength); + const charEnds = new Float32Array(animLength); + const scale = perturbation > 0 ? perturbation * settleRatio : 0; + for (let c = 0; c < animLength; c++) { + const so = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + const eo = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + charStarts[c] = charOrder[c] * settleSpacing + so; + charEnds[c] = Math.ceil((charStarts[c] + settleRatio + eo) / stepRatio) * stepRatio; + } + // When text shrinks with non-sequential from modes, delay target settle times past all extras + if (endLength < animLength && resolvedFrom !== 'left' && resolvedFrom !== 'right' && resolvedFrom !== 'random') { + let maxExtraEnd = 0; + for (let c = endLength; c < animLength; c++) { + if (charEnds[c] > maxExtraEnd) maxExtraEnd = charEnds[c]; + } + const targets = new Array(endLength); + for (let c = 0; c < endLength; c++) targets[c] = c; + targets.sort((a, b) => charOrder[a] - charOrder[b]); + const targetSpacing = (1 - maxExtraEnd) / endLength; + for (let i = 0; i < endLength; i++) { + const revealTime = maxExtraEnd + i * targetSpacing; + if (revealTime > charEnds[targets[i]]) { + charEnds[targets[i]] = revealTime; + } + } + } + // charCache holds the current scramble character for each position, refreshed at settleRate + const charCache = new Array(animLength); + for (let c = 0; c < animLength; c++) { + charCache[c] = characters[rng(0, totalChars)]; + } + // overrideCache holds scramble characters for the starting text (override: true or custom string) + const overrideCache = overrideChars ? (overrideChars === characters ? charCache : new Array(animLength)) : null; + if (overrideCache && overrideCache !== charCache) { + for (let c = 0; c < animLength; c++) { + overrideCache[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, overrideChars.length - 1)]; + } + } + // Build the initial display text based on override mode + let fillStartText = startingText; + if (!prevTween) { + if (override === '') { + fillStartText = ''; + } else if (overrideChars) { + fillStartText = ''; + for (let c = 0; c < startLength; c++) { + fillStartText += startingText[c] === ' ' ? ' ' : /** @type {Array} */(overrideCache)[c]; + } + } + } + + let lastValue = -1; + let lastStep = -1; + let scrambled = ''; + const hasOverride = override !== ''; + const hasOverrideChars = !!overrideChars; + const hasCursor = cursorLen > 0; + + return { + from: 0, + to: 1, + duration: computedDuration, + delay: delay, + ease: 'linear', + modifier: (v) => { + if (v === lastValue) return scrambled; + lastValue = v; + if (delay > 0 && v <= 0) { scrambled = startingText; return startingText; } + if (v <= 0) { scrambled = fillStartText; return fillStartText; } + if (v >= 1) { scrambled = settledText; return settledText; } + scrambled = ''; + // Only refresh scramble characters when we cross a settleRate step boundary + const currentStep = (v / stepRatio) | 0; + const refreshChars = currentStep !== lastStep; + if (refreshChars) lastStep = currentStep; + // Subtract delay ratio to get the effective animation progress + const linear = revealDelayRatio > 0 ? (v - revealDelayRatio) / (1 - revealDelayRatio) : v; + const t = linear > 0 ? easeFn(linear) : 0; + for (let c = 0; c < animLength; c++) { + // Each character has its own start/end window based on its reveal order + const charStart = charStarts[c]; + const charEnd = charEnds[c]; + // Settled zone: character has finished its transition + if (t >= charEnd) { + if (c < endLength) scrambled += settledText[c]; + continue; + } + // Pre-transition zone: reveal wave hasn't reached this character yet + if (t <= 0 || t < charStart) { + if (hasOverride && c < startLength) { + if (hasOverrideChars) { + if (startingText[c] === ' ') { + scrambled += ' '; + } else { + if (refreshChars) /** @type {Array} */(overrideCache)[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, totalOverrideChars)]; + scrambled += /** @type {Array} */(overrideCache)[c]; + } + } else { + // Default (override: false): show the original starting text + scrambled += startingText[c]; + } + } + continue; + } + // Active zone: character is between charStart and charEnd + const isSpace = (c < endLength && settledText[c] === ' ') || (c < startLength && startingText[c] === ' '); + if (isSpace) { + scrambled += ' '; + } else if (hasCursor && t - charStart < cursorZone) { + // Cursor sub-zone: show cursor character based on position within cursor width + scrambled += cursorChars[cursorLen - 1 - (((t - charStart) / settleSpacing) | 0)]; + } else { + // Scramble zone: show cycling random characters + if (refreshChars) charCache[c] = characters[rng(0, totalChars)]; + scrambled += charCache[c]; + } + } + if (refreshChars) onChange(scrambled, t); + return scrambled; + } + } + } +}; + +exports.scrambleText = scrambleText; diff --git a/dist/modules/text/scramble.d.ts b/dist/modules/text/scramble.d.ts new file mode 100644 index 000000000..4c48f8e13 --- /dev/null +++ b/dist/modules/text/scramble.d.ts @@ -0,0 +1,3 @@ +export function scrambleText(params?: ScrambleTextParams): FunctionValue; +import type { ScrambleTextParams } from '../types/index.js'; +import type { FunctionValue } from '../types/index.js'; diff --git a/dist/modules/text/scramble.js b/dist/modules/text/scramble.js new file mode 100644 index 000000000..86ec9fbc3 --- /dev/null +++ b/dist/modules/text/scramble.js @@ -0,0 +1,270 @@ +/** + * Anime.js - text - ESM + * @version v4.4.1 + * @license MIT + * @copyright 2026 - Julian Garnier + */ + +import { createSeededRandom } from '../utils/random.js'; +import { globals } from '../core/globals.js'; +import { round } from '../core/helpers.js'; +import { parseEase } from '../easings/eases/parser.js'; +import { noop } from '../core/consts.js'; + +/** + * @import { + * ScrambleTextParams, + * FunctionValue, + * } from '../types/index.js' +*/ + +/** + * '-' is the range operator; place it at the start or end of the string to use it as a literal (e.g. '-abc' or 'abc-') + * @param {String} str + * @return {String} + */ +const expandCharRanges = (str) => { + let result = ''; + for (let i = 0, l = str.length; i < l; i++) { + if (i + 2 < l && str[i + 1] === '-' && str.charCodeAt(i) < str.charCodeAt(i + 2)) { + const start = str.charCodeAt(i); + const end = str.charCodeAt(i + 2); + for (let c = start; c <= end; c++) result += String.fromCharCode(c); + i += 2; + } else { + result += str[i]; + } + } + return result; +}; + +const charSets = { + lowercase: 'a-z', + uppercase: 'A-Z', + numbers: '0-9', + symbols: '!%#_|*+=', + braille: 'â €-⣿', + blocks: 'â–€-â–Ÿ', + shades: 'â–‘-â–“', +}; + +const originalTexts = new WeakMap(); + +/** + * Returns a function-based tween value that scrambles the target's text content, + * progressively revealing the original text. + * + * @param {ScrambleTextParams} [params] + * @return {FunctionValue} + */ +const scrambleText = (params = {}) => { + if (!params) params = {}; + const charsParam = params.chars; + const easeFn = parseEase(params.ease || 'linear'); + const text = params.text; + const fromParam = params.from; + const reversed = params.reversed || false; + const perturbation = params.perturbation || 0; + const cursorParam = params.cursor; + const cursorChars = cursorParam === true ? '_' + : typeof cursorParam === 'number' ? String.fromCharCode(cursorParam) + : typeof cursorParam === 'string' ? cursorParam + : ''; + const cursorLen = cursorChars.length; + const seed = params.seed || 0; + const override = params.override !== undefined ? params.override : true; + const revealRate = params.revealRate || 60; + const interval = 1000 * globals.timeScale / revealRate; + const settleDuration = params.settleDuration || 300 * globals.timeScale; + const settleRate = params.settleRate || 30; + const durationParam = params.duration; + const revealDelayParam = params.revealDelay; + const delayParam = params.delay; + const onChange = params.onChange || noop; + + return (target, index, targets, prevTween) => { + const rawChars = typeof charsParam === 'function' ? charsParam(target, index, targets) : (charsParam || 'a-zA-Z0-9!%#_'); + const characters = expandCharRanges(charSets[rawChars] || rawChars); + const totalChars = characters.length - 1; + const duration = typeof durationParam === 'function' ? durationParam(target, index, targets) : durationParam; + const revealDelay = typeof revealDelayParam === 'function' ? revealDelayParam(target, index, targets) : (revealDelayParam || 0); + const delay = typeof delayParam === 'function' ? delayParam(target, index, targets) : (delayParam || 0); + const rng = seed ? createSeededRandom(seed) : createSeededRandom(); + if (!originalTexts.has(target)) originalTexts.set(target, target.textContent); + const startingText = prevTween ? prevTween._value : target.textContent; + const targetText = text !== undefined + ? (typeof text === 'function' ? text(target, index, targets) : text) + : prevTween ? prevTween._value + : originalTexts.get(target); + const settledText = targetText === ' ' || targetText === ' ' ? ' ' : targetText; + const startLength = startingText === ' ' ? 0 : startingText.length; + const endLength = settledText.length; + const overrideChars = override === true ? characters + : typeof override === 'string' && override.length > 0 ? expandCharRanges(charSets[/** @type {String} */(override)] || /** @type {String} */(override)) + : null; + const totalOverrideChars = overrideChars ? overrideChars.length - 1 : 0; + // Space override uses   so the browser doesn't collapse consecutive spaces in innerHTML + const overrideChar = override === ' ' ? ' ' : null; + // When starting from blank, only animate the target text length to avoid padding beyond it + const animLength = override === '' ? endLength : Math.max(startLength, endLength); + // Compute total duration from interval spacing and settle time, or use the explicit duration + const animDuration = duration > 0 ? duration : (animLength - 1) * interval + settleDuration; + const computedDuration = round((animDuration + revealDelay) / globals.timeScale, 0) * globals.timeScale; + const revealDelayRatio = revealDelay > 0 ? round(revealDelay / computedDuration, 12) : 0; + // Auto-resolve reveal direction: shrinking text reveals from right, growing from left + const resolvedFrom = fromParam === undefined || fromParam === 'auto' ? (endLength < startLength ? 'right' : 'left') : fromParam; + const charOrder = new Int32Array(animLength); + if (resolvedFrom === 'random') { + for (let i = 0; i < animLength; i++) charOrder[i] = i; + for (let i = animLength - 1; i > 0; i--) { + const j = rng(0, i); + const t = charOrder[i]; charOrder[i] = charOrder[j]; charOrder[j] = t; + } + } else { + const ref = resolvedFrom === 'right' ? (override === '' || !startLength ? animLength : startLength) - 1 + : resolvedFrom === 'center' ? ((override === '' || !startLength ? animLength : startLength) - 1) / 2 + : typeof resolvedFrom === 'number' ? resolvedFrom + : 0; + const abs = Math.abs; + const indices = new Array(animLength); + for (let i = 0; i < animLength; i++) indices[i] = i; + indices.sort((a, b) => abs(a - ref) - abs(b - ref)); + for (let i = 0; i < animLength; i++) charOrder[indices[i]] = i; + } + if (reversed) { + const last = animLength - 1; + for (let i = 0; i < animLength; i++) charOrder[i] = last - charOrder[i]; + } + // settleRatio is the fraction of the animation each character spends in the active scrambling zone + const settleRatio = round(settleDuration / animDuration, 12); + // settleSpacing is the time gap between consecutive characters entering the active zone + const settleSpacing = round((1 - settleRatio) / animLength, 12); + const cursorZone = cursorLen * settleSpacing; + // stepRatio controls how often scramble characters refresh (based on settleRate) + const stepRatio = round(1000 * globals.timeScale / (settleRate * computedDuration), 12); + // Pre-compute per-character start and settle times + const charStarts = new Float32Array(animLength); + const charEnds = new Float32Array(animLength); + const scale = perturbation > 0 ? perturbation * settleRatio : 0; + for (let c = 0; c < animLength; c++) { + const so = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + const eo = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + charStarts[c] = charOrder[c] * settleSpacing + so; + charEnds[c] = Math.ceil((charStarts[c] + settleRatio + eo) / stepRatio) * stepRatio; + } + // When text shrinks with non-sequential from modes, delay target settle times past all extras + if (endLength < animLength && resolvedFrom !== 'left' && resolvedFrom !== 'right' && resolvedFrom !== 'random') { + let maxExtraEnd = 0; + for (let c = endLength; c < animLength; c++) { + if (charEnds[c] > maxExtraEnd) maxExtraEnd = charEnds[c]; + } + const targets = new Array(endLength); + for (let c = 0; c < endLength; c++) targets[c] = c; + targets.sort((a, b) => charOrder[a] - charOrder[b]); + const targetSpacing = (1 - maxExtraEnd) / endLength; + for (let i = 0; i < endLength; i++) { + const revealTime = maxExtraEnd + i * targetSpacing; + if (revealTime > charEnds[targets[i]]) { + charEnds[targets[i]] = revealTime; + } + } + } + // charCache holds the current scramble character for each position, refreshed at settleRate + const charCache = new Array(animLength); + for (let c = 0; c < animLength; c++) { + charCache[c] = characters[rng(0, totalChars)]; + } + // overrideCache holds scramble characters for the starting text (override: true or custom string) + const overrideCache = overrideChars ? (overrideChars === characters ? charCache : new Array(animLength)) : null; + if (overrideCache && overrideCache !== charCache) { + for (let c = 0; c < animLength; c++) { + overrideCache[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, overrideChars.length - 1)]; + } + } + // Build the initial display text based on override mode + let fillStartText = startingText; + if (!prevTween) { + if (override === '') { + fillStartText = ''; + } else if (overrideChars) { + fillStartText = ''; + for (let c = 0; c < startLength; c++) { + fillStartText += startingText[c] === ' ' ? ' ' : /** @type {Array} */(overrideCache)[c]; + } + } + } + + let lastValue = -1; + let lastStep = -1; + let scrambled = ''; + const hasOverride = override !== ''; + const hasOverrideChars = !!overrideChars; + const hasCursor = cursorLen > 0; + + return { + from: 0, + to: 1, + duration: computedDuration, + delay: delay, + ease: 'linear', + modifier: (v) => { + if (v === lastValue) return scrambled; + lastValue = v; + if (delay > 0 && v <= 0) { scrambled = startingText; return startingText; } + if (v <= 0) { scrambled = fillStartText; return fillStartText; } + if (v >= 1) { scrambled = settledText; return settledText; } + scrambled = ''; + // Only refresh scramble characters when we cross a settleRate step boundary + const currentStep = (v / stepRatio) | 0; + const refreshChars = currentStep !== lastStep; + if (refreshChars) lastStep = currentStep; + // Subtract delay ratio to get the effective animation progress + const linear = revealDelayRatio > 0 ? (v - revealDelayRatio) / (1 - revealDelayRatio) : v; + const t = linear > 0 ? easeFn(linear) : 0; + for (let c = 0; c < animLength; c++) { + // Each character has its own start/end window based on its reveal order + const charStart = charStarts[c]; + const charEnd = charEnds[c]; + // Settled zone: character has finished its transition + if (t >= charEnd) { + if (c < endLength) scrambled += settledText[c]; + continue; + } + // Pre-transition zone: reveal wave hasn't reached this character yet + if (t <= 0 || t < charStart) { + if (hasOverride && c < startLength) { + if (hasOverrideChars) { + if (startingText[c] === ' ') { + scrambled += ' '; + } else { + if (refreshChars) /** @type {Array} */(overrideCache)[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, totalOverrideChars)]; + scrambled += /** @type {Array} */(overrideCache)[c]; + } + } else { + // Default (override: false): show the original starting text + scrambled += startingText[c]; + } + } + continue; + } + // Active zone: character is between charStart and charEnd + const isSpace = (c < endLength && settledText[c] === ' ') || (c < startLength && startingText[c] === ' '); + if (isSpace) { + scrambled += ' '; + } else if (hasCursor && t - charStart < cursorZone) { + // Cursor sub-zone: show cursor character based on position within cursor width + scrambled += cursorChars[cursorLen - 1 - (((t - charStart) / settleSpacing) | 0)]; + } else { + // Scramble zone: show cycling random characters + if (refreshChars) charCache[c] = characters[rng(0, totalChars)]; + scrambled += charCache[c]; + } + } + if (refreshChars) onChange(scrambled, t); + return scrambled; + } + } + } +}; + +export { scrambleText }; diff --git a/dist/modules/text/split.cjs b/dist/modules/text/split.cjs index 3baa88a9b..3b36f3b3c 100644 --- a/dist/modules/text/split.cjs +++ b/dist/modules/text/split.cjs @@ -1,8 +1,8 @@ /** * Anime.js - text - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -89,12 +89,23 @@ const filterEmptyElements = $el => { /** * @param {HTMLElement} $el * @param {Number} lineIndex - * @param {Set} bin - * @returns {Set} + * @param {Set} bin + * @returns {Set} */ const filterLineElements = ($el, lineIndex, bin) => { const dataLineAttr = $el.getAttribute(dataLine); - if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') bin.add($el); + if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') { + bin.add($el); + // Also remove adjacent whitespace-only text nodes + const prev = $el.previousSibling; + const next = $el.nextSibling; + if (prev && prev.nodeType === 3 && whiteSpaceRgx.test(prev.textContent)) { + bin.add(prev); + } + if (next && next.nodeType === 3 && whiteSpaceRgx.test(next.textContent)) { + bin.add(next); + } + } let i = $el.childElementCount; while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin); return bin; @@ -186,7 +197,7 @@ const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, d */ class TextSplitter { /** - * @param {HTMLElement|NodeList|String|Array} target + * @param {Element|NodeList|String|Array} target * @param {TextSplitterParams} [parameters] */ constructor(target, parameters = {}) { @@ -258,11 +269,11 @@ class TextSplitter { } /** - * @param {(...args: any[]) => Tickable | (() => void)} effect + * @param {(...args: any[]) => Tickable | (() => void) | void} effect * @return this */ addEffect(effect) { - if (!helpers.isFnc(effect)) return console.warn('Effect must return a function.'); + if (!helpers.isFnc(effect)) { console.warn('Effect must return a function.'); return this; } const refreshableEffect = time.keepTime(effect); this.effects.push(refreshableEffect); if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this); @@ -309,7 +320,7 @@ class TextSplitter { // Only concatenate if both current and previous are non-word-like and don't contain spaces const lastWordIndex = tempWords.length - 1; const lastWord = tempWords[lastWordIndex]; - if (!lastWord.includes(' ') && !segment.includes(' ')) { + if (!whiteSpaceGroupRgx.test(lastWord) && !whiteSpaceGroupRgx.test(segment)) { tempWords[lastWordIndex] += segment; } else { tempWords.push(segment); @@ -402,7 +413,7 @@ class TextSplitter { for (let i = 0, l = elementsArray.length; i < l; i++) { const $el = elementsArray[i]; const { top, height } = $el.getBoundingClientRect(); - if (y && top - y > height * .5) linesCount++; + if (!helpers.isUnd(y) && top - y > height * .5) linesCount++; $el.setAttribute(dataLine, `${linesCount}`); const nested = $el.querySelectorAll(`[${dataLine}]`); let c = nested.length; @@ -416,9 +427,11 @@ class TextSplitter { for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) { const $clone = /** @type {HTMLElement} */($el.cloneNode(true)); filterLineElements($clone, lineIndex, new Set()).forEach($el => { - const $parent = $el.parentElement; - if ($parent) parents.add($parent); - $el.remove(); + const $parent = $el.parentNode; + if ($parent) { + if ($el.nodeType === 1) parents.add(/** @type {HTMLElement} */($parent)); + $parent.removeChild($el); + } }); clones.push($clone); } @@ -431,6 +444,7 @@ class TextSplitter { if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); if (charTemplate) this.chars = getAllTopLevelElements($el, charType); } + // Remove the word wrappers and clear the words array if lines split only if (this.linesOnly) { const words = this.words; @@ -465,7 +479,7 @@ class TextSplitter { } /** - * @param {HTMLElement|NodeList|String|Array} target + * @param {Element|NodeList|String|Array} target * @param {TextSplitterParams} [parameters] * @return {TextSplitter} */ diff --git a/dist/modules/text/split.d.ts b/dist/modules/text/split.d.ts index 110bfe3de..f18108858 100644 --- a/dist/modules/text/split.d.ts +++ b/dist/modules/text/split.d.ts @@ -4,10 +4,10 @@ */ export class TextSplitter { /** - * @param {HTMLElement|NodeList|String|Array} target + * @param {Element|NodeList|String|Array} target * @param {TextSplitterParams} [parameters] */ - constructor(target: HTMLElement | NodeList | string | Array, parameters?: TextSplitterParams); + constructor(target: Element | NodeList | string | Array, parameters?: TextSplitterParams); debug: boolean; includeSpaces: boolean; accessible: boolean; @@ -31,10 +31,10 @@ export class TextSplitter { resizeTimeout: NodeJS.Timeout; resizeObserver: ResizeObserver; /** - * @param {(...args: any[]) => Tickable | (() => void)} effect + * @param {(...args: any[]) => Tickable | (() => void) | void} effect * @return this */ - addEffect(effect: (...args: any[]) => Tickable | (() => void)): void | this; + addEffect(effect: (...args: any[]) => Tickable | (() => void) | void): this; revert(): this; /** * Recursively processes a node and its children @@ -48,7 +48,7 @@ export class TextSplitter { split(clearCache?: boolean): this; refresh(): void; } -export function splitText(target: HTMLElement | NodeList | string | Array, parameters?: TextSplitterParams): TextSplitter; +export function splitText(target: Element | NodeList | string | Array, parameters?: TextSplitterParams): TextSplitter; export function split(target: HTMLElement | NodeList | string | Array, parameters?: TextSplitterParams): TextSplitter; export type Segment = { segment: string; diff --git a/dist/modules/text/split.js b/dist/modules/text/split.js index 2924ab524..9c72a3d3a 100644 --- a/dist/modules/text/split.js +++ b/dist/modules/text/split.js @@ -1,8 +1,8 @@ /** * Anime.js - text - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { isBrowser, doc } from '../core/consts.js'; @@ -87,12 +87,23 @@ const filterEmptyElements = $el => { /** * @param {HTMLElement} $el * @param {Number} lineIndex - * @param {Set} bin - * @returns {Set} + * @param {Set} bin + * @returns {Set} */ const filterLineElements = ($el, lineIndex, bin) => { const dataLineAttr = $el.getAttribute(dataLine); - if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') bin.add($el); + if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') { + bin.add($el); + // Also remove adjacent whitespace-only text nodes + const prev = $el.previousSibling; + const next = $el.nextSibling; + if (prev && prev.nodeType === 3 && whiteSpaceRgx.test(prev.textContent)) { + bin.add(prev); + } + if (next && next.nodeType === 3 && whiteSpaceRgx.test(next.textContent)) { + bin.add(next); + } + } let i = $el.childElementCount; while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin); return bin; @@ -184,7 +195,7 @@ const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, d */ class TextSplitter { /** - * @param {HTMLElement|NodeList|String|Array} target + * @param {Element|NodeList|String|Array} target * @param {TextSplitterParams} [parameters] */ constructor(target, parameters = {}) { @@ -256,11 +267,11 @@ class TextSplitter { } /** - * @param {(...args: any[]) => Tickable | (() => void)} effect + * @param {(...args: any[]) => Tickable | (() => void) | void} effect * @return this */ addEffect(effect) { - if (!isFnc(effect)) return console.warn('Effect must return a function.'); + if (!isFnc(effect)) { console.warn('Effect must return a function.'); return this; } const refreshableEffect = keepTime(effect); this.effects.push(refreshableEffect); if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this); @@ -307,7 +318,7 @@ class TextSplitter { // Only concatenate if both current and previous are non-word-like and don't contain spaces const lastWordIndex = tempWords.length - 1; const lastWord = tempWords[lastWordIndex]; - if (!lastWord.includes(' ') && !segment.includes(' ')) { + if (!whiteSpaceGroupRgx.test(lastWord) && !whiteSpaceGroupRgx.test(segment)) { tempWords[lastWordIndex] += segment; } else { tempWords.push(segment); @@ -400,7 +411,7 @@ class TextSplitter { for (let i = 0, l = elementsArray.length; i < l; i++) { const $el = elementsArray[i]; const { top, height } = $el.getBoundingClientRect(); - if (y && top - y > height * .5) linesCount++; + if (!isUnd(y) && top - y > height * .5) linesCount++; $el.setAttribute(dataLine, `${linesCount}`); const nested = $el.querySelectorAll(`[${dataLine}]`); let c = nested.length; @@ -414,9 +425,11 @@ class TextSplitter { for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) { const $clone = /** @type {HTMLElement} */($el.cloneNode(true)); filterLineElements($clone, lineIndex, new Set()).forEach($el => { - const $parent = $el.parentElement; - if ($parent) parents.add($parent); - $el.remove(); + const $parent = $el.parentNode; + if ($parent) { + if ($el.nodeType === 1) parents.add(/** @type {HTMLElement} */($parent)); + $parent.removeChild($el); + } }); clones.push($clone); } @@ -429,6 +442,7 @@ class TextSplitter { if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); if (charTemplate) this.chars = getAllTopLevelElements($el, charType); } + // Remove the word wrappers and clear the words array if lines split only if (this.linesOnly) { const words = this.words; @@ -463,7 +477,7 @@ class TextSplitter { } /** - * @param {HTMLElement|NodeList|String|Array} target + * @param {Element|NodeList|String|Array} target * @param {TextSplitterParams} [parameters] * @return {TextSplitter} */ diff --git a/dist/modules/timeline/index.cjs b/dist/modules/timeline/index.cjs index 14ef01fa7..147fa2828 100644 --- a/dist/modules/timeline/index.cjs +++ b/dist/modules/timeline/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - timeline - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/timeline/index.js b/dist/modules/timeline/index.js index 98c29d6a9..88f383826 100644 --- a/dist/modules/timeline/index.js +++ b/dist/modules/timeline/index.js @@ -1,8 +1,8 @@ /** * Anime.js - timeline - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { Timeline, createTimeline } from './timeline.js'; diff --git a/dist/modules/timeline/position.cjs b/dist/modules/timeline/position.cjs index d1bc8beff..ba5e3578a 100644 --- a/dist/modules/timeline/position.cjs +++ b/dist/modules/timeline/position.cjs @@ -1,8 +1,8 @@ /** * Anime.js - timeline - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/timeline/position.js b/dist/modules/timeline/position.js index 5192809af..67c827ade 100644 --- a/dist/modules/timeline/position.js +++ b/dist/modules/timeline/position.js @@ -1,8 +1,8 @@ /** * Anime.js - timeline - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { relativeValuesExecRgx, minValue } from '../core/consts.js'; diff --git a/dist/modules/timeline/timeline.cjs b/dist/modules/timeline/timeline.cjs index 60a04d052..c595babb3 100644 --- a/dist/modules/timeline/timeline.cjs +++ b/dist/modules/timeline/timeline.cjs @@ -1,8 +1,8 @@ /** * Anime.js - timeline - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -14,9 +14,9 @@ var values = require('../core/values.cjs'); var targets = require('../core/targets.cjs'); var render = require('../core/render.cjs'); var styles = require('../core/styles.cjs'); +var timer = require('../timer/timer.cjs'); var composition = require('../animation/composition.cjs'); var animation = require('../animation/animation.cjs'); -var timer = require('../timer/timer.cjs'); var parser = require('../easings/eases/parser.cjs'); var position = require('./position.cjs'); @@ -33,6 +33,7 @@ var position = require('./position.cjs'); * DefaultsParams, * TimelinePosition, * StaggerFunction, + * TargetsArray, * } from '../types/index.js' */ @@ -42,6 +43,8 @@ var position = require('./position.cjs'); * } from '../waapi/waapi.js' */ +/** @import {TweakRegister} from 'tweaks' */ + /** * @param {Timeline} tl * @return {Number} @@ -63,7 +66,7 @@ function getTimelineTotalDuration(tl) { * @param {Number} timePosition * @param {TargetsParam} targets * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] * @return {Timeline} * * @param {TimerParams|AnimationParams} childParams @@ -71,17 +74,17 @@ function getTimelineTotalDuration(tl) { * @param {Number} timePosition * @param {TargetsParam} [targets] * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] */ -function addTlChild(childParams, tl, timePosition, targets, index, length) { +function addTlChild(childParams, tl, timePosition, targets, index, allTargets) { const isSetter = helpers.isNum(childParams.duration) && /** @type {Number} */(childParams.duration) <= consts.minValue; // Offset the tl position with -minValue for 0 duration animations or .set() calls in order to align their end value with the defined position const adjustedPosition = isSetter ? timePosition - consts.minValue : timePosition; - render.tick(tl, adjustedPosition, 1, 1, consts.tickModes.AUTO); + if (tl.composition) render.tick(tl, adjustedPosition, 1, 1, consts.tickModes.AUTO); const tlChild = targets ? - new animation.JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, length) : + new animation.JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, allTargets) : new timer.Timer(/** @type {TimerParams} */(childParams), tl, adjustedPosition); - tlChild.init(true); + if (tl.composition) tlChild.init(true); // TODO: Might be better to insert at a position relative to startTime? helpers.addChild(tl, tlChild); helpers.forEachChildren(tl, (/** @type {Renderable} */child) => { @@ -93,6 +96,8 @@ function addTlChild(childParams, tl, timePosition, targets, index, length) { return tl; } +let TLId = 0; + class Timeline extends timer.Timer { /** @@ -100,6 +105,9 @@ class Timeline extends timer.Timer { */ constructor(parameters = {}) { super(/** @type {TimerParams&TimelineParams} */(parameters), null, 0); + ++TLId; + /** @type {String|Number} */ + this.id = !helpers.isUnd(parameters.id) ? parameters.id : TLId; /** @type {Number} */ this.duration = 0; // TL duration starts at 0 and grows when adding children /** @type {Record} */ @@ -108,6 +116,8 @@ class Timeline extends timer.Timer { const globalDefaults = globals.globals.defaults; /** @type {DefaultsParams} */ this.defaults = defaultsParams ? helpers.mergeObjects(defaultsParams, globalDefaults) : globalDefaults; + /** @type {Boolean} */ + this.composition = values.setValue(parameters.composition, true); /** @type {Callback} */ this.onRender = parameters.onRender || globalDefaults.onRender; const tlPlaybackEase = values.setValue(parameters.playbackEase, globalDefaults.playbackEase); @@ -120,7 +130,7 @@ class Timeline extends timer.Timer { * @overload * @param {TargetsParam} a1 * @param {AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] * @return {this} * * @overload @@ -130,7 +140,7 @@ class Timeline extends timer.Timer { * * @param {TargetsParam|TimerParams} a1 * @param {TimelinePosition|AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] */ add(a1, a2, a3) { const isAnim = helpers.isObj(a2); @@ -139,9 +149,11 @@ class Timeline extends timer.Timer { this._hasChildren = true; if (isAnim) { const childParams = /** @type {AnimationParams} */(a2); - // Check for function for children stagger positions - if (helpers.isFnc(a3)) { - const staggeredPosition = a3; + const editorHook = globals.globals.editor && globals.globals.editor.addTimelineChild; + const isStaggerType = a3 && /** @type {TweakRegister} */(a3).type === 'Stagger' && globals.globals.editor; + // Check for function or Stagger type children positions + const staggeredPosition = helpers.isFnc(a3) ? a3 : null; + if (staggeredPosition || isStaggerType) { const parsedTargetsArray = targets.parseTargets(/** @type {TargetsParam} */(a1)); // Store initial duration before adding new children that will change the duration const tlDuration = this.duration; @@ -152,28 +164,36 @@ class Timeline extends timer.Timer { let i = 0; /** @type {Number} */ const parsedLength = (parsedTargetsArray.length); + // Call editor hook once for the entire stagger group instead of per target + const resolvedParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3, parsedLength) : null; + // Resolve stagger AFTER editor hook so tweaked position value (a3.defaultValue) is used + const staggerFn = staggeredPosition || globals.globals.editor.resolveStagger(/** @type {TweakRegister} */(a3).defaultValue); parsedTargetsArray.forEach((/** @type {Target} */target) => { // Create a new parameter object for each staggered children - const staggeredChildParams = { ...childParams }; + const staggeredChildParams = { ...(resolvedParams || childParams) }; // Reset the duration of the timeline iteration before each stagger to prevent wrong start value calculation this.duration = tlDuration; this.iterationDuration = tlIterationDuration; if (!helpers.isUnd(id)) staggeredChildParams.id = id + '-' + i; + const staggeredTimePosition = position.parseTimelinePosition(this, staggerFn(target, i, parsedTargetsArray, null, this)); addTlChild( staggeredChildParams, this, - position.parseTimelinePosition(this, staggeredPosition(target, i, parsedLength, this)), + staggeredTimePosition, target, i, - parsedLength + parsedTargetsArray, ); i++; }); } else { + // Call editor hook before resolving position so tweaked values are applied + const resolvedChildParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3) : childParams; + const resolvedPosition = a3 && /** @type {*} */(a3).type ? /** @type {*} */(a3).defaultValue : a3; addTlChild( - childParams, + resolvedChildParams, this, - position.parseTimelinePosition(this, a3), + position.parseTimelinePosition(this, resolvedPosition), /** @type {TargetsParam} */(a1), ); } @@ -185,7 +205,8 @@ class Timeline extends timer.Timer { position.parseTimelinePosition(this,a2), ); } - return this.init(true); + if (this.composition) this.init(true); + return this; } } @@ -212,7 +233,11 @@ class Timeline extends timer.Timer { if (helpers.isUnd(synced) || synced && helpers.isUnd(synced.pause)) return this; synced.pause(); const duration = +(/** @type {globalThis.Animation} */(synced).effect ? /** @type {globalThis.Animation} */(synced).effect.getTiming().duration : /** @type {Tickable} */(synced).duration); - return this.add(synced, { currentTime: [0, duration], duration, ease: 'linear' }, position); + // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. + if (!helpers.isUnd(synced) && !helpers.isUnd(/** @type {WAAPIAnimation} */(synced).persist)) { + /** @type {WAAPIAnimation} */(synced).persist = true; + } + return this.add(synced, { currentTime: [0, duration], duration, delay: 0, ease: 'linear', playbackEase: 'linear' }, position); } /** @@ -235,7 +260,7 @@ class Timeline extends timer.Timer { */ call(callback, position) { if (helpers.isUnd(callback) || callback && !helpers.isFnc(callback)) return this; - return this.add({ duration: 0, onComplete: () => callback(this) }, position); + return this.add({ duration: 0, delay: 0, onComplete: () => callback(this) }, position); } /** @@ -278,8 +303,8 @@ class Timeline extends timer.Timer { * @return {this} */ refresh() { - helpers.forEachChildren(this, (/** @type {JSAnimation} */child) => { - if (child.refresh) child.refresh(); + helpers.forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => { + if (/** @type {JSAnimation} */(child).refresh) /** @type {JSAnimation} */(child).refresh(); }); return this; } @@ -289,8 +314,8 @@ class Timeline extends timer.Timer { */ revert() { super.revert(); - helpers.forEachChildren(this, (/** @type {JSAnimation} */child) => child.revert, true); - return styles.cleanInlineStyles(this); + helpers.forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => child.revert, true); + return styles.revertValues(this); } /** @@ -310,7 +335,12 @@ class Timeline extends timer.Timer { * @param {TimelineParams} [parameters] * @return {Timeline} */ -const createTimeline = parameters => new Timeline(parameters).init(); +const createTimeline = parameters => { + if (globals.globals.editor) { + return /** @type {Timeline} */(/** @type {unknown} */(globals.globals.editor.addTimeline(parameters))); + } + return new Timeline(parameters).init(); +}; exports.Timeline = Timeline; exports.createTimeline = createTimeline; diff --git a/dist/modules/timeline/timeline.d.ts b/dist/modules/timeline/timeline.d.ts index 7a3f04686..774fd0793 100644 --- a/dist/modules/timeline/timeline.d.ts +++ b/dist/modules/timeline/timeline.d.ts @@ -7,6 +7,8 @@ export class Timeline extends Timer { labels: Record; /** @type {DefaultsParams} */ defaults: DefaultsParams; + /** @type {Boolean} */ + composition: boolean; /** @type {Callback} */ onRender: Callback; _ease: import("../types/index.js").EasingFunction; @@ -14,7 +16,7 @@ export class Timeline extends Timer { * @overload * @param {TargetsParam} a1 * @param {AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] * @return {this} * * @overload @@ -24,14 +26,14 @@ export class Timeline extends Timer { * * @param {TargetsParam|TimerParams} a1 * @param {TimelinePosition|AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] */ - add(a1: TargetsParam, a2: AnimationParams, a3?: TimelinePosition | StaggerFunction): this; + add(a1: TargetsParam, a2: AnimationParams, a3?: TimelinePosition | StaggerFunction | TweakRegister): this; /** * @overload * @param {TargetsParam} a1 * @param {AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] * @return {this} * * @overload @@ -41,7 +43,7 @@ export class Timeline extends Timer { * * @param {TargetsParam|TimerParams} a1 * @param {TimelinePosition|AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] */ add(a1: TimerParams, a2?: TimelinePosition): this; /** @@ -162,6 +164,7 @@ import type { TargetsParam } from '../types/index.js'; import type { AnimationParams } from '../types/index.js'; import type { TimelinePosition } from '../types/index.js'; import type { StaggerFunction } from '../types/index.js'; +import type { TweakRegister } from 'tweaks'; import type { TimerParams } from '../types/index.js'; import type { Tickable } from '../types/index.js'; import type { WAAPIAnimation } from '../waapi/waapi.js'; diff --git a/dist/modules/timeline/timeline.js b/dist/modules/timeline/timeline.js index d20c1ac78..42a38ef4c 100644 --- a/dist/modules/timeline/timeline.js +++ b/dist/modules/timeline/timeline.js @@ -1,20 +1,20 @@ /** * Anime.js - timeline - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { globals } from '../core/globals.js'; import { minValue, compositionTypes, tickModes } from '../core/consts.js'; -import { mergeObjects, isObj, isFnc, isUnd, isStr, normalizeTime, forEachChildren, isNum, addChild, clampInfinity } from '../core/helpers.js'; +import { isUnd, mergeObjects, isObj, isFnc, isStr, normalizeTime, forEachChildren, isNum, addChild, clampInfinity } from '../core/helpers.js'; import { setValue } from '../core/values.js'; import { parseTargets } from '../core/targets.js'; import { tick } from '../core/render.js'; -import { cleanInlineStyles } from '../core/styles.js'; +import { revertValues } from '../core/styles.js'; +import { Timer } from '../timer/timer.js'; import { removeTargetsFromRenderable } from '../animation/composition.js'; import { JSAnimation } from '../animation/animation.js'; -import { Timer } from '../timer/timer.js'; import { parseEase } from '../easings/eases/parser.js'; import { parseTimelinePosition } from './position.js'; @@ -31,6 +31,7 @@ import { parseTimelinePosition } from './position.js'; * DefaultsParams, * TimelinePosition, * StaggerFunction, + * TargetsArray, * } from '../types/index.js' */ @@ -40,6 +41,8 @@ import { parseTimelinePosition } from './position.js'; * } from '../waapi/waapi.js' */ +/** @import {TweakRegister} from 'tweaks' */ + /** * @param {Timeline} tl * @return {Number} @@ -61,7 +64,7 @@ function getTimelineTotalDuration(tl) { * @param {Number} timePosition * @param {TargetsParam} targets * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] * @return {Timeline} * * @param {TimerParams|AnimationParams} childParams @@ -69,17 +72,17 @@ function getTimelineTotalDuration(tl) { * @param {Number} timePosition * @param {TargetsParam} [targets] * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] */ -function addTlChild(childParams, tl, timePosition, targets, index, length) { +function addTlChild(childParams, tl, timePosition, targets, index, allTargets) { const isSetter = isNum(childParams.duration) && /** @type {Number} */(childParams.duration) <= minValue; // Offset the tl position with -minValue for 0 duration animations or .set() calls in order to align their end value with the defined position const adjustedPosition = isSetter ? timePosition - minValue : timePosition; - tick(tl, adjustedPosition, 1, 1, tickModes.AUTO); + if (tl.composition) tick(tl, adjustedPosition, 1, 1, tickModes.AUTO); const tlChild = targets ? - new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, length) : + new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, allTargets) : new Timer(/** @type {TimerParams} */(childParams), tl, adjustedPosition); - tlChild.init(true); + if (tl.composition) tlChild.init(true); // TODO: Might be better to insert at a position relative to startTime? addChild(tl, tlChild); forEachChildren(tl, (/** @type {Renderable} */child) => { @@ -91,6 +94,8 @@ function addTlChild(childParams, tl, timePosition, targets, index, length) { return tl; } +let TLId = 0; + class Timeline extends Timer { /** @@ -98,6 +103,9 @@ class Timeline extends Timer { */ constructor(parameters = {}) { super(/** @type {TimerParams&TimelineParams} */(parameters), null, 0); + ++TLId; + /** @type {String|Number} */ + this.id = !isUnd(parameters.id) ? parameters.id : TLId; /** @type {Number} */ this.duration = 0; // TL duration starts at 0 and grows when adding children /** @type {Record} */ @@ -106,6 +114,8 @@ class Timeline extends Timer { const globalDefaults = globals.defaults; /** @type {DefaultsParams} */ this.defaults = defaultsParams ? mergeObjects(defaultsParams, globalDefaults) : globalDefaults; + /** @type {Boolean} */ + this.composition = setValue(parameters.composition, true); /** @type {Callback} */ this.onRender = parameters.onRender || globalDefaults.onRender; const tlPlaybackEase = setValue(parameters.playbackEase, globalDefaults.playbackEase); @@ -118,7 +128,7 @@ class Timeline extends Timer { * @overload * @param {TargetsParam} a1 * @param {AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] * @return {this} * * @overload @@ -128,7 +138,7 @@ class Timeline extends Timer { * * @param {TargetsParam|TimerParams} a1 * @param {TimelinePosition|AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] */ add(a1, a2, a3) { const isAnim = isObj(a2); @@ -137,9 +147,11 @@ class Timeline extends Timer { this._hasChildren = true; if (isAnim) { const childParams = /** @type {AnimationParams} */(a2); - // Check for function for children stagger positions - if (isFnc(a3)) { - const staggeredPosition = a3; + const editorHook = globals.editor && globals.editor.addTimelineChild; + const isStaggerType = a3 && /** @type {TweakRegister} */(a3).type === 'Stagger' && globals.editor; + // Check for function or Stagger type children positions + const staggeredPosition = isFnc(a3) ? a3 : null; + if (staggeredPosition || isStaggerType) { const parsedTargetsArray = parseTargets(/** @type {TargetsParam} */(a1)); // Store initial duration before adding new children that will change the duration const tlDuration = this.duration; @@ -150,28 +162,36 @@ class Timeline extends Timer { let i = 0; /** @type {Number} */ const parsedLength = (parsedTargetsArray.length); + // Call editor hook once for the entire stagger group instead of per target + const resolvedParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3, parsedLength) : null; + // Resolve stagger AFTER editor hook so tweaked position value (a3.defaultValue) is used + const staggerFn = staggeredPosition || globals.editor.resolveStagger(/** @type {TweakRegister} */(a3).defaultValue); parsedTargetsArray.forEach((/** @type {Target} */target) => { // Create a new parameter object for each staggered children - const staggeredChildParams = { ...childParams }; + const staggeredChildParams = { ...(resolvedParams || childParams) }; // Reset the duration of the timeline iteration before each stagger to prevent wrong start value calculation this.duration = tlDuration; this.iterationDuration = tlIterationDuration; if (!isUnd(id)) staggeredChildParams.id = id + '-' + i; + const staggeredTimePosition = parseTimelinePosition(this, staggerFn(target, i, parsedTargetsArray, null, this)); addTlChild( staggeredChildParams, this, - parseTimelinePosition(this, staggeredPosition(target, i, parsedLength, this)), + staggeredTimePosition, target, i, - parsedLength + parsedTargetsArray, ); i++; }); } else { + // Call editor hook before resolving position so tweaked values are applied + const resolvedChildParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3) : childParams; + const resolvedPosition = a3 && /** @type {*} */(a3).type ? /** @type {*} */(a3).defaultValue : a3; addTlChild( - childParams, + resolvedChildParams, this, - parseTimelinePosition(this, a3), + parseTimelinePosition(this, resolvedPosition), /** @type {TargetsParam} */(a1), ); } @@ -183,7 +203,8 @@ class Timeline extends Timer { parseTimelinePosition(this,a2), ); } - return this.init(true); + if (this.composition) this.init(true); + return this; } } @@ -210,7 +231,11 @@ class Timeline extends Timer { if (isUnd(synced) || synced && isUnd(synced.pause)) return this; synced.pause(); const duration = +(/** @type {globalThis.Animation} */(synced).effect ? /** @type {globalThis.Animation} */(synced).effect.getTiming().duration : /** @type {Tickable} */(synced).duration); - return this.add(synced, { currentTime: [0, duration], duration, ease: 'linear' }, position); + // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. + if (!isUnd(synced) && !isUnd(/** @type {WAAPIAnimation} */(synced).persist)) { + /** @type {WAAPIAnimation} */(synced).persist = true; + } + return this.add(synced, { currentTime: [0, duration], duration, delay: 0, ease: 'linear', playbackEase: 'linear' }, position); } /** @@ -233,7 +258,7 @@ class Timeline extends Timer { */ call(callback, position) { if (isUnd(callback) || callback && !isFnc(callback)) return this; - return this.add({ duration: 0, onComplete: () => callback(this) }, position); + return this.add({ duration: 0, delay: 0, onComplete: () => callback(this) }, position); } /** @@ -276,8 +301,8 @@ class Timeline extends Timer { * @return {this} */ refresh() { - forEachChildren(this, (/** @type {JSAnimation} */child) => { - if (child.refresh) child.refresh(); + forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => { + if (/** @type {JSAnimation} */(child).refresh) /** @type {JSAnimation} */(child).refresh(); }); return this; } @@ -287,8 +312,8 @@ class Timeline extends Timer { */ revert() { super.revert(); - forEachChildren(this, (/** @type {JSAnimation} */child) => child.revert, true); - return cleanInlineStyles(this); + forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => child.revert, true); + return revertValues(this); } /** @@ -308,6 +333,11 @@ class Timeline extends Timer { * @param {TimelineParams} [parameters] * @return {Timeline} */ -const createTimeline = parameters => new Timeline(parameters).init(); +const createTimeline = parameters => { + if (globals.editor) { + return /** @type {Timeline} */(/** @type {unknown} */(globals.editor.addTimeline(parameters))); + } + return new Timeline(parameters).init(); +}; export { Timeline, createTimeline }; diff --git a/dist/modules/timer/index.cjs b/dist/modules/timer/index.cjs index 2c4954ec3..d1ed2d05b 100644 --- a/dist/modules/timer/index.cjs +++ b/dist/modules/timer/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - timer - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/timer/index.js b/dist/modules/timer/index.js index cb1bf4900..09ce2cb8a 100644 --- a/dist/modules/timer/index.js +++ b/dist/modules/timer/index.js @@ -1,8 +1,8 @@ /** * Anime.js - timer - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { Timer, createTimer } from './timer.js'; diff --git a/dist/modules/timer/timer.cjs b/dist/modules/timer/timer.cjs index 7a1c18452..f9b433b4c 100644 --- a/dist/modules/timer/timer.cjs +++ b/dist/modules/timer/timer.cjs @@ -1,8 +1,8 @@ /** * Anime.js - timer - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -69,6 +69,9 @@ const reviveTimer = timer => { let timerId = 0; +/** @param {Timer} prev @param {Timer} child */ +const sortByPriority = (prev, child) => prev._priority > child._priority; + /** * Base class used to create Timers, Animations and Timelines */ @@ -82,6 +85,8 @@ class Timer extends clock.Clock { super(0); + ++timerId; + const { id, delay, @@ -93,6 +98,7 @@ class Timer extends clock.Clock { autoplay, frameRate, playbackRate, + priority, onComplete, onLoop, onPause, @@ -103,31 +109,32 @@ class Timer extends clock.Clock { if (globals.scope.current) globals.scope.current.register(this); - const timerInitTime = parent ? 0 : engine.engine._elapsedTime; + const timerInitTime = parent ? 0 : engine.engine._lastTickTime; const timerDefaults = parent ? parent.defaults : globals.globals.defaults; const timerDelay = /** @type {Number} */(helpers.isFnc(delay) || helpers.isUnd(delay) ? timerDefaults.delay : +delay); const timerDuration = helpers.isFnc(duration) || helpers.isUnd(duration) ? Infinity : +duration; const timerLoop = values.setValue(loop, timerDefaults.loop); const timerLoopDelay = values.setValue(loopDelay, timerDefaults.loopDelay); - const timerIterationCount = timerLoop === true || - timerLoop === Infinity || - /** @type {Number} */(timerLoop) < 0 ? Infinity : - /** @type {Number} */(timerLoop) + 1; + let timerIterationCount = timerLoop === true || + timerLoop === Infinity || + /** @type {Number} */(timerLoop) < 0 ? Infinity : + /** @type {Number} */(timerLoop) + 1; let offsetPosition = 0; if (parent) { offsetPosition = parentPosition; } else { - // Make sure to tick the engine once if not currently running to get up to date engine._elapsedTime + // Make sure to tick the engine once if not currently running to get up to date engine._lastTickTime // to avoid big gaps with the following offsetPosition calculation if (!engine.engine.reqId) engine.engine.requestTick(helpers.now()); // Make sure to scale the offset position with globals.timeScale to properly handle seconds unit - offsetPosition = (engine.engine._elapsedTime - engine.engine._startTime) * globals.globals.timeScale; + offsetPosition = (engine.engine._lastTickTime - engine.engine._startTime) * globals.globals.timeScale; } // Timer's parameters - this.id = !helpers.isUnd(id) ? id : ++timerId; + /** @type {String|Number} */ + this.id = !helpers.isUnd(id) ? id : timerId; /** @type {Timeline} */ this.parent = parent; // Total duration of the timer @@ -187,7 +194,7 @@ class Timer extends clock.Clock { // Clock's parameters /** @type {Number} */ - this._elapsedTime = timerInitTime; + this._lastTickTime = timerInitTime; /** @type {Number} */ this._startTime = timerInitTime; /** @type {Number} */ @@ -196,6 +203,8 @@ class Timer extends clock.Clock { this._fps = values.setValue(frameRate, timerDefaults.frameRate); /** @type {Number} */ this._speed = values.setValue(playbackRate, timerDefaults.playbackRate); + /** @type {Number} */ + this._priority = +values.setValue(priority, 1); } get cancelled() { @@ -218,7 +227,7 @@ class Timer extends clock.Clock { } get iterationCurrentTime() { - return helpers.round(this._iterationTime, globals.globals.precision); + return helpers.clamp(helpers.round(this._iterationTime, globals.globals.precision), 0, this.iterationDuration); } set iterationCurrentTime(time) { @@ -316,9 +325,9 @@ class Timer extends clock.Clock { /** @return {this} */ resetTime() { const timeScale = 1 / (this._speed * engine.engine._speed); - // TODO: See if we can safely use engine._elapsedTime here + // TODO: See if we can safely use engine._lastTickTime here // if (!engine.reqId) engine.requestTick(now()) - // this._startTime = engine._elapsedTime - (this._currentTime + this._delay) * timeScale; + // this._startTime = engine._lastTickTime - (this._currentTime + this._delay) * timeScale; this._startTime = helpers.now() - (this._currentTime + this._delay) * timeScale; return this; } @@ -340,7 +349,7 @@ class Timer extends clock.Clock { render.tick(this, consts.minValue, 0, 0, consts.tickModes.FORCE); } else { if (!this._running) { - helpers.addChild(engine.engine, this); + helpers.addChild(engine.engine, this, sortByPriority); engine.engine._hasChildren = true; this._running = true; } @@ -450,10 +459,11 @@ class Timer extends clock.Clock { /** * Imediatly completes the timer, cancels it and triggers the onComplete callback + * @param {Boolean|Number} [muteCallbacks] * @return {this} */ - complete() { - return this.seek(this.duration).cancel(); + complete(muteCallbacks = 0) { + return this.seek(this.duration, muteCallbacks).cancel(); } /** diff --git a/dist/modules/timer/timer.d.ts b/dist/modules/timer/timer.d.ts index f2a1bfc4d..0a0047aa5 100644 --- a/dist/modules/timer/timer.d.ts +++ b/dist/modules/timer/timer.d.ts @@ -8,6 +8,7 @@ export class Timer extends Clock { * @param {Number} [parentPosition] */ constructor(parameters?: TimerParams, parent?: Timeline, parentPosition?: number); + /** @type {String|Number} */ id: string | number; /** @type {Timeline} */ parent: Timeline; @@ -64,6 +65,8 @@ export class Timer extends Clock { _prev: Renderable; /** @type {Renderable} */ _next: Renderable; + /** @type {Number} */ + _priority: number; set cancelled(cancelled: boolean); get cancelled(): boolean; set currentTime(time: number); @@ -123,9 +126,10 @@ export class Timer extends Clock { revert(): this; /** * Imediatly completes the timer, cancels it and triggers the onComplete callback + * @param {Boolean|Number} [muteCallbacks] * @return {this} */ - complete(): this; + complete(muteCallbacks?: boolean | number): this; /** * @typedef {this & {then: null}} ResolvedTimer */ diff --git a/dist/modules/timer/timer.js b/dist/modules/timer/timer.js index e987b0f13..4d2ac69cf 100644 --- a/dist/modules/timer/timer.js +++ b/dist/modules/timer/timer.js @@ -1,8 +1,8 @@ /** * Anime.js - timer - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { minValue, noop, maxValue, compositionTypes, tickModes } from '../core/consts.js'; @@ -67,6 +67,9 @@ const reviveTimer = timer => { let timerId = 0; +/** @param {Timer} prev @param {Timer} child */ +const sortByPriority = (prev, child) => prev._priority > child._priority; + /** * Base class used to create Timers, Animations and Timelines */ @@ -80,6 +83,8 @@ class Timer extends Clock { super(0); + ++timerId; + const { id, delay, @@ -91,6 +96,7 @@ class Timer extends Clock { autoplay, frameRate, playbackRate, + priority, onComplete, onLoop, onPause, @@ -101,31 +107,32 @@ class Timer extends Clock { if (scope.current) scope.current.register(this); - const timerInitTime = parent ? 0 : engine._elapsedTime; + const timerInitTime = parent ? 0 : engine._lastTickTime; const timerDefaults = parent ? parent.defaults : globals.defaults; const timerDelay = /** @type {Number} */(isFnc(delay) || isUnd(delay) ? timerDefaults.delay : +delay); const timerDuration = isFnc(duration) || isUnd(duration) ? Infinity : +duration; const timerLoop = setValue(loop, timerDefaults.loop); const timerLoopDelay = setValue(loopDelay, timerDefaults.loopDelay); - const timerIterationCount = timerLoop === true || - timerLoop === Infinity || - /** @type {Number} */(timerLoop) < 0 ? Infinity : - /** @type {Number} */(timerLoop) + 1; + let timerIterationCount = timerLoop === true || + timerLoop === Infinity || + /** @type {Number} */(timerLoop) < 0 ? Infinity : + /** @type {Number} */(timerLoop) + 1; let offsetPosition = 0; if (parent) { offsetPosition = parentPosition; } else { - // Make sure to tick the engine once if not currently running to get up to date engine._elapsedTime + // Make sure to tick the engine once if not currently running to get up to date engine._lastTickTime // to avoid big gaps with the following offsetPosition calculation if (!engine.reqId) engine.requestTick(now()); // Make sure to scale the offset position with globals.timeScale to properly handle seconds unit - offsetPosition = (engine._elapsedTime - engine._startTime) * globals.timeScale; + offsetPosition = (engine._lastTickTime - engine._startTime) * globals.timeScale; } // Timer's parameters - this.id = !isUnd(id) ? id : ++timerId; + /** @type {String|Number} */ + this.id = !isUnd(id) ? id : timerId; /** @type {Timeline} */ this.parent = parent; // Total duration of the timer @@ -185,7 +192,7 @@ class Timer extends Clock { // Clock's parameters /** @type {Number} */ - this._elapsedTime = timerInitTime; + this._lastTickTime = timerInitTime; /** @type {Number} */ this._startTime = timerInitTime; /** @type {Number} */ @@ -194,6 +201,8 @@ class Timer extends Clock { this._fps = setValue(frameRate, timerDefaults.frameRate); /** @type {Number} */ this._speed = setValue(playbackRate, timerDefaults.playbackRate); + /** @type {Number} */ + this._priority = +setValue(priority, 1); } get cancelled() { @@ -216,7 +225,7 @@ class Timer extends Clock { } get iterationCurrentTime() { - return round(this._iterationTime, globals.precision); + return clamp(round(this._iterationTime, globals.precision), 0, this.iterationDuration); } set iterationCurrentTime(time) { @@ -314,9 +323,9 @@ class Timer extends Clock { /** @return {this} */ resetTime() { const timeScale = 1 / (this._speed * engine._speed); - // TODO: See if we can safely use engine._elapsedTime here + // TODO: See if we can safely use engine._lastTickTime here // if (!engine.reqId) engine.requestTick(now()) - // this._startTime = engine._elapsedTime - (this._currentTime + this._delay) * timeScale; + // this._startTime = engine._lastTickTime - (this._currentTime + this._delay) * timeScale; this._startTime = now() - (this._currentTime + this._delay) * timeScale; return this; } @@ -338,7 +347,7 @@ class Timer extends Clock { tick(this, minValue, 0, 0, tickModes.FORCE); } else { if (!this._running) { - addChild(engine, this); + addChild(engine, this, sortByPriority); engine._hasChildren = true; this._running = true; } @@ -448,10 +457,11 @@ class Timer extends Clock { /** * Imediatly completes the timer, cancels it and triggers the onComplete callback + * @param {Boolean|Number} [muteCallbacks] * @return {this} */ - complete() { - return this.seek(this.duration).cancel(); + complete(muteCallbacks = 0) { + return this.seek(this.duration, muteCallbacks).cancel(); } /** diff --git a/dist/modules/types/index.d.ts b/dist/modules/types/index.d.ts index 5ab38aa40..93f20de56 100644 --- a/dist/modules/types/index.d.ts +++ b/dist/modules/types/index.d.ts @@ -12,7 +12,7 @@ export type DefaultsParams = { duration?: number | FunctionValue; delay?: number | FunctionValue; loopDelay?: number; - ease?: EasingParam; + ease?: EasingParam | FunctionValue; composition?: "none" | "replace" | "blend" | compositionTypes; modifier?: (v: any) => any; onBegin?: Callback; @@ -26,13 +26,13 @@ export type DefaultsParams = { export type Renderable = JSAnimation | Timeline; export type Tickable = Timer | Renderable; export type CallbackArgument = Timer & JSAnimation & Timeline; -export type Revertible = Animatable | Tickable | WAAPIAnimation | Draggable | ScrollObserver | TextSplitter | Scope; -export type StaggerFunction = (target?: Target, index?: number, length?: number, tl?: Timeline) => T; +export type Revertible = Animatable | Tickable | WAAPIAnimation | Draggable | ScrollObserver | TextSplitter | Scope | AutoLayout; +export type StaggerFunction = (target?: Target, index?: number, targets?: TargetsArray, prevTween?: Tween | null, tl?: Timeline) => T; export type StaggerParams = { start?: number | string; - from?: number | "first" | "center" | "last" | "random"; + from?: number | "first" | "center" | "last" | "random" | Array; reversed?: boolean; - grid?: Array; + grid?: Array | boolean; axis?: ("x" | "y"); use?: string | ((target: Target, i: number, length: number) => number); total?: number; @@ -57,8 +57,8 @@ export type PowerEasing = (power?: number | string) => EasingFunction; export type BackEasing = (overshoot?: number | string) => EasingFunction; export type ElasticEasing = (amplitude?: number | string, period?: number | string) => EasingFunction; export type EasingFunctionWithParams = PowerEasing | BackEasing | ElasticEasing; -export type EasingParam = (string & {}) | EaseStringParamNames | EasingFunction | Spring; -export type WAAPIEasingParam = (string & {}) | EaseStringParamNames | WAAPIEaseStringParamNames | EasingFunction | Spring; +export type EasingParam = (string & {}) | EaseStringParamNames | EasingFunction | Spring | TweakRegister; +export type WAAPIEasingParam = (string & {}) | EaseStringParamNames | WAAPIEaseStringParamNames | EasingFunction | Spring | TweakRegister; export type SpringParams = { /** * - Mass, default 1 @@ -112,9 +112,10 @@ export type TimerOptions = { autoplay?: boolean | ScrollObserver; frameRate?: number; playbackRate?: number; + priority?: number; }; export type TimerParams = TimerOptions & TickableCallbacks; -export type FunctionValue = (target: Target, index: number, length: number) => number | string | TweenObjectValue | Array; +export type FunctionValue = (target?: Target, index?: number, targets?: TargetsArray, prevTween?: Tween | null) => number | string | TweenObjectValue | EasingParam | Array; export type TweenModifier = (value: number) => number | string; export type ColorArray = [number, number, number, number]; export type Tween = { @@ -123,7 +124,8 @@ export type Tween = { property: string; target: Target; _value: string | number; - _func: Function | null; + _toFunc: Function | null; + _fromFunc: Function | null; _ease: EasingFunction; _fromNumbers: Array; _toNumbers: Array; @@ -187,13 +189,13 @@ export type TweenPropertySiblings = { export type TweenLookups = Record; export type TweenReplaceLookups = WeakMap; export type TweenAdditiveLookups = Map; -export type TweenParamValue = number | string | FunctionValue; +export type TweenParamValue = number | string | FunctionValue | EasingParam; export type TweenPropValue = TweenParamValue | [TweenParamValue, TweenParamValue]; export type TweenComposition = (string & {}) | "none" | "replace" | "blend" | compositionTypes; export type TweenParamsOptions = { duration?: TweenParamValue; delay?: TweenParamValue; - ease?: EasingParam; + ease?: EasingParam | FunctionValue; modifier?: TweenModifier; composition?: TweenComposition; }; @@ -245,14 +247,15 @@ export type TimelinePosition = number | `+=${number}` | `-=${number}` | `*=${num * - `'label'` - Label: Position animation at a named label position (e.g., `'My Label'`)
* - `stagger(String|Nummber)` - Stagger multi-elements animation positions (e.g., 10, 20, 30...) */ -export type TimelineAnimationPosition = TimelinePosition | StaggerFunction; +export type TimelineAnimationPosition = TimelinePosition | StaggerFunction | TweakRegister; export type TimelineOptions = { defaults?: DefaultsParams; playbackEase?: EasingParam; + composition?: boolean; }; export type TimelineParams = TimerOptions & TimelineOptions & TickableCallbacks & RenderableCallbacks; export type WAAPITweenValue = string | number | Array | Array; -export type WAAPIFunctionValue = (target: DOMTarget, index: number, length: number) => WAAPITweenValue; +export type WAAPIFunctionValue = (target: DOMTarget, index: number, targets: DOMTargetsArray) => WAAPITweenValue | WAAPIEasingParam; export type WAAPIKeyframeValue = WAAPITweenValue | WAAPIFunctionValue | Array; export type WAAPITweenOptions = { to?: WAAPIKeyframeValue; @@ -270,7 +273,7 @@ export type WAAPIAnimationOptions = { playbackRate?: number; duration?: number | WAAPIFunctionValue; delay?: number | WAAPIFunctionValue; - ease?: WAAPIEasingParam; + ease?: WAAPIEasingParam | WAAPIFunctionValue; composition?: CompositeOperation; persist?: boolean; onComplete?: Callback; @@ -327,6 +330,7 @@ export type ScrollObserverParams = { onEnterBackward?: Callback; onLeaveBackward?: Callback; onUpdate?: Callback; + onResize?: Callback; onSyncComplete?: Callback; }; export type DraggableAxisParam = { @@ -389,6 +393,72 @@ export type TextSplitterParams = { includeSpaces?: boolean; debug?: boolean; }; +export type ScrambleTextParams = { + /** + * - the text to transition to, otherwise uses the original text + */ + text?: string | ((arg0: Target, arg1: number, arg2: TargetsArray) => string); + /** + * - the characters used for scramble; named sets: 'lowercase', 'uppercase', 'numbers', 'symbols', 'braille', 'blocks', 'shades'; range syntax: 'A-Z', 'a-z0-9'; defaults to 'a-zA-Z0-9!%#_' + */ + chars?: string | ((arg0: Target, arg1: number, arg2: TargetsArray) => string); + /** + * - the easing applied to the scramble animation + */ + ease?: EasingParam; + /** + * - where the reveal wave starts from, 'auto' (default) uses 'left' when text grows and 'right' when it shrinks + */ + from?: number | "left" | "center" | "right" | "random" | "auto"; + /** + * - reverses the reveal order, so 'center' reveals from edges inward instead of center outward + */ + reversed?: boolean; + /** + * - characters displayed at the leading edge of the reveal wave; true uses '_', a number is a char code, a string is used directly + */ + cursor?: boolean | number | string; + /** + * - adds random timing offsets to each character's start and end, creating a more organic reveal + */ + perturbation?: number; + /** + * - a seed for the random number generator to produce reproducible scramble sequences + */ + seed?: number; + /** + * - controls the starting appearance: false shows original text, true scrambles it (default), '' starts from blank, ' ' replaces characters with spaces, a custom string (supports range syntax like 'A-Z') uses its characters as scramble set + */ + override?: boolean | string; + /** + * - characters per second entering the active zone; higher values make the reveal wave move faster (default: 60) + */ + revealRate?: number; + /** + * - time in ms each character spends scrambling before settling into its final glyph (default: 300) + */ + settleDuration?: number; + /** + * - how many times per second scramble characters cycle in the active zone (default: 30) + */ + settleRate?: number; + /** + * - if set to a value greater than 0, overrides the computed duration from interval and settle; if unset or 0, duration is calculated automatically from text length and timing parameters + */ + duration?: number | ((arg0: Target, arg1: number, arg2: TargetsArray) => number); + /** + * - delay in ms before the reveal wave starts within the scramble animation + */ + revealDelay?: number | ((arg0: Target, arg1: number, arg2: TargetsArray) => number); + /** + * - delay in ms before the entire scramble animation starts + */ + delay?: number | ((arg0: Target, arg1: number, arg2: TargetsArray) => number); + /** + * - callback fired each time a character changes during scramble; receives the current scrambled text and the eased progress (0-1) + */ + onChange?: (arg0: string, arg1: number) => void; +}; export type DrawableSVGGeometry = SVGGeometryElement & { setAttribute(name: "draw", value: `${number} ${number}`): void; draw: `${number} ${number}`; @@ -403,6 +473,8 @@ import type { WAAPIAnimation } from '../waapi/waapi.js'; import type { Draggable } from '../draggable/draggable.js'; import type { TextSplitter } from '../text/split.js'; import type { Scope } from '../scope/scope.js'; +import type { AutoLayout } from '../layout/layout.js'; import type { Spring } from '../easings/spring/index.js'; +import type { TweakRegister } from 'tweaks'; import type { tweenTypes } from '../core/consts.js'; import type { valueTypes } from '../core/consts.js'; diff --git a/dist/modules/utils/chainable.cjs b/dist/modules/utils/chainable.cjs index d7fd8357b..e79d895da 100644 --- a/dist/modules/utils/chainable.cjs +++ b/dist/modules/utils/chainable.cjs @@ -1,8 +1,8 @@ /** * Anime.js - utils - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -36,10 +36,13 @@ const chain = fn => { const result = fn(...args); return new Proxy(consts.noop, { apply: (_, __, [v]) => result(v), - get: (_, prop) => chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { - const nextResult = chainables[prop](...nextArgs); - return (/**@type {Number|String} */v) => nextResult(result(v)); - }) + get: (_, prop) => { + if (!chainables[prop]) return undefined; + return chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { + const nextResult = chainables[prop](...nextArgs); + return (/**@type {Number|String} */v) => nextResult(result(v)); + }) + } }); } }; diff --git a/dist/modules/utils/chainable.js b/dist/modules/utils/chainable.js index af6a2ec24..e54843ac1 100644 --- a/dist/modules/utils/chainable.js +++ b/dist/modules/utils/chainable.js @@ -1,8 +1,8 @@ /** * Anime.js - utils - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { noop } from '../core/consts.js'; @@ -34,10 +34,13 @@ const chain = fn => { const result = fn(...args); return new Proxy(noop, { apply: (_, __, [v]) => result(v), - get: (_, prop) => chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { - const nextResult = chainables[prop](...nextArgs); - return (/**@type {Number|String} */v) => nextResult(result(v)); - }) + get: (_, prop) => { + if (!chainables[prop]) return undefined; + return chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { + const nextResult = chainables[prop](...nextArgs); + return (/**@type {Number|String} */v) => nextResult(result(v)); + }) + } }); } }; diff --git a/dist/modules/utils/index.cjs b/dist/modules/utils/index.cjs index c653cc090..85a9f54ad 100644 --- a/dist/modules/utils/index.cjs +++ b/dist/modules/utils/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - utils - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -12,6 +12,7 @@ var random = require('./random.cjs'); var time = require('./time.cjs'); var target = require('./target.cjs'); var stagger = require('./stagger.cjs'); +var helpers = require('../core/helpers.cjs'); var styles = require('../core/styles.cjs'); var targets = require('../core/targets.cjs'); @@ -39,5 +40,8 @@ exports.get = target.get; exports.remove = target.remove; exports.set = target.set; exports.stagger = stagger.stagger; +exports.addChild = helpers.addChild; +exports.forEachChildren = helpers.forEachChildren; +exports.removeChild = helpers.removeChild; exports.cleanInlineStyles = styles.cleanInlineStyles; exports.$ = targets.registerTargets; diff --git a/dist/modules/utils/index.d.ts b/dist/modules/utils/index.d.ts index 32ed8b4f6..fd735f39e 100644 --- a/dist/modules/utils/index.d.ts +++ b/dist/modules/utils/index.d.ts @@ -3,3 +3,4 @@ export * from "./random.js"; export * from "./time.js"; export * from "./target.js"; export * from "./stagger.js"; +export { forEachChildren, addChild, removeChild } from "../core/helpers.js"; diff --git a/dist/modules/utils/index.js b/dist/modules/utils/index.js index 13c761fbc..f686d3bc4 100644 --- a/dist/modules/utils/index.js +++ b/dist/modules/utils/index.js @@ -1,8 +1,8 @@ /** * Anime.js - utils - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { clamp, damp, degToRad, lerp, mapRange, padEnd, padStart, radToDeg, round, roundPad, snap, wrap } from './chainable.js'; @@ -10,5 +10,6 @@ export { createSeededRandom, random, randomPick, shuffle } from './random.js'; export { keepTime, sync } from './time.js'; export { get, remove, set } from './target.js'; export { stagger } from './stagger.js'; +export { addChild, forEachChildren, removeChild } from '../core/helpers.js'; export { cleanInlineStyles } from '../core/styles.js'; export { registerTargets as $ } from '../core/targets.js'; diff --git a/dist/modules/utils/number.cjs b/dist/modules/utils/number.cjs index 8b8f10881..67b30e522 100644 --- a/dist/modules/utils/number.cjs +++ b/dist/modules/utils/number.cjs @@ -1,8 +1,8 @@ /** * Anime.js - utils - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/utils/number.js b/dist/modules/utils/number.js index 65529079a..3f3925654 100644 --- a/dist/modules/utils/number.js +++ b/dist/modules/utils/number.js @@ -1,8 +1,8 @@ /** * Anime.js - utils - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { lerp } from '../core/helpers.js'; diff --git a/dist/modules/utils/random.cjs b/dist/modules/utils/random.cjs index 497b618bc..86d215fe7 100644 --- a/dist/modules/utils/random.cjs +++ b/dist/modules/utils/random.cjs @@ -1,8 +1,8 @@ /** * Anime.js - utils - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/utils/random.js b/dist/modules/utils/random.js index 2d84d9ddc..fc9735562 100644 --- a/dist/modules/utils/random.js +++ b/dist/modules/utils/random.js @@ -1,8 +1,8 @@ /** * Anime.js - utils - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ /** diff --git a/dist/modules/utils/stagger.cjs b/dist/modules/utils/stagger.cjs index 07ccffef7..d44ee1916 100644 --- a/dist/modules/utils/stagger.cjs +++ b/dist/modules/utils/stagger.cjs @@ -1,8 +1,8 @@ /** * Anime.js - utils - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -19,6 +19,7 @@ var random = require('./random.cjs'); * @import { * StaggerParams, * StaggerFunction, + * JSTarget, * } from '../types/index.js' */ @@ -34,24 +35,28 @@ var random = require('./random.cjs'); * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @overload * @param {String} val * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @overload * @param {[Number, Number]} val * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @overload * @param {[String, String]} val * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @param {Number|String|[Number, Number]|[String, String]} val The staggered value or range * @param {StaggerParams} [params] The stagger parameters @@ -60,6 +65,7 @@ var random = require('./random.cjs'); const stagger = (val, params = {}) => { let values$1 = []; let maxValue = 0; + let cachedOffset; const from = params.from; const reversed = params.reversed; const ease = params.ease; @@ -67,12 +73,14 @@ const stagger = (val, params = {}) => { const hasSpring = hasEasing && !helpers.isUnd(/** @type {Spring} */(ease).ease); const staggerEase = hasSpring ? /** @type {Spring} */(ease).ease : hasEasing ? parser.parseEase(ease) : null; const grid = params.grid; + const autoGrid = grid === true; const axis = params.axis; const customTotal = params.total; const fromFirst = helpers.isUnd(from) || from === 0 || from === 'first'; const fromCenter = from === 'center'; const fromLast = from === 'last'; const fromRandom = from === 'random'; + const fromArr = helpers.isArr(from); const isRange = helpers.isArr(val); const useProp = params.use; const val1 = isRange ? helpers.parseNumber(val[0]) : helpers.parseNumber(val); @@ -80,40 +88,129 @@ const stagger = (val, params = {}) => { const unitMatch = consts.unitsExecRgx.exec((isRange ? val[1] : val) + consts.emptyString); const start = params.start || 0 + (isRange ? val1 : 0); let fromIndex = fromFirst ? 0 : helpers.isNum(from) ? from : 0; - return (target, i, t, tl) => { + return (target, i, t, _, tl) => { const [ registeredTarget ] = targets.registerTargets(target); - const total = helpers.isUnd(customTotal) ? t : customTotal; + const total = helpers.isUnd(customTotal) ? t.length : customTotal; const customIndex = !helpers.isUnd(useProp) ? helpers.isFnc(useProp) ? useProp(registeredTarget, i, total) : values.getOriginalAnimatableValue(registeredTarget, useProp) : false; const staggerIndex = helpers.isNum(customIndex) || helpers.isStr(customIndex) && helpers.isNum(+customIndex) ? +customIndex : i; if (fromCenter) fromIndex = (total - 1) / 2; if (fromLast) fromIndex = total - 1; if (!values$1.length) { - for (let index = 0; index < total; index++) { - if (!grid) { - values$1.push(helpers.abs(fromIndex - index)); + if (autoGrid) { + let hasPositions = true; + let minPosX = Infinity; + let minPosY = Infinity; + let maxPosX = -Infinity; + let maxPosY = -Infinity; + const pxArr = []; + const pyArr = []; + for (let index = 0; index < total; index++) { + const el = t[index]; + let px = 0; + let py = 0; + let found = false; + if (el && helpers.isFnc(el.getBoundingClientRect)) { + const rect = el.getBoundingClientRect(); + px = rect.left + rect.width / 2; + py = rect.top + rect.height / 2; + found = true; + } else { + const obj = /** @type {JSTarget} */(el); + if (obj && helpers.isNum(obj.x) && helpers.isNum(obj.y)) { + px = obj.x; + py = obj.y; + found = true; + } + } + if (!found) { + hasPositions = false; + break; + } + pxArr.push(px); + pyArr.push(py); + if (px < minPosX) minPosX = px; + if (py < minPosY) minPosY = py; + if (px > maxPosX) maxPosX = px; + if (py > maxPosY) maxPosY = py; + } + if (hasPositions) { + let fX = pxArr[0]; + let fY = pyArr[0]; + if (fromArr) { + fX = minPosX + from[0] * (maxPosX - minPosX); + fY = minPosY + from[1] * (maxPosY - minPosY); + } else if (fromCenter) { + fX = (minPosX + maxPosX) / 2; + fY = (minPosY + maxPosY) / 2; + } else if (fromLast) { + fX = pxArr[total - 1]; + fY = pyArr[total - 1]; + } else if (helpers.isNum(from)) { + fX = pxArr[from]; + fY = pyArr[from]; + } + for (let index = 0; index < total; index++) { + const distanceX = fX - pxArr[index]; + const distanceY = fY - pyArr[index]; + let value = helpers.sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values$1.push(value); + } + let minDist = Infinity; + for (let index = 0, l = values$1.length; index < l; index++) { + const absVal = helpers.abs(values$1[index]); + if (absVal > 0 && absVal < minDist) minDist = absVal; + } + if (minDist > 0 && minDist < Infinity) { + for (let index = 0, l = values$1.length; index < l; index++) { + values$1[index] = values$1[index] / minDist; + } + } } else { - const fromX = !fromCenter ? fromIndex % grid[0] : (grid[0] - 1) / 2; - const fromY = !fromCenter ? helpers.floor(fromIndex / grid[0]) : (grid[1] - 1) / 2; - const toX = index % grid[0]; - const toY = helpers.floor(index / grid[0]); - const distanceX = fromX - toX; - const distanceY = fromY - toY; - let value = helpers.sqrt(distanceX * distanceX + distanceY * distanceY); - if (axis === 'x') value = -distanceX; - if (axis === 'y') value = -distanceY; - values$1.push(value); + for (let index = 0; index < total; index++) { + values$1.push(helpers.abs(fromIndex - index)); + } + } + } else { + for (let index = 0; index < total; index++) { + if (!grid) { + values$1.push(helpers.abs(fromIndex - index)); + } else { + let fromX, fromY; + if (fromArr) { + fromX = from[0] * (grid[0] - 1); + fromY = from[1] * (grid[1] - 1); + } else if (fromCenter) { + fromX = (grid[0] - 1) / 2; + fromY = (grid[1] - 1) / 2; + } else { + fromX = fromIndex % grid[0]; + fromY = helpers.floor(fromIndex / grid[0]); + } + const toX = index % grid[0]; + const toY = helpers.floor(index / grid[0]); + const distanceX = fromX - toX; + const distanceY = fromY - toY; + let value = helpers.sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values$1.push(value); + } } - maxValue = helpers.max(...values$1); } + maxValue = helpers.max(...values$1); if (staggerEase) values$1 = values$1.map(val => staggerEase(val / maxValue) * maxValue); if (reversed) values$1 = values$1.map(val => axis ? (val < 0) ? val * -1 : -val : helpers.abs(maxValue - val)); if (fromRandom) values$1 = random.shuffle(values$1); } const spacing = isRange ? (val2 - val1) / maxValue : val1; - const offset = tl ? position.parseTimelinePosition(tl, helpers.isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); + if (helpers.isUnd(cachedOffset)) { + cachedOffset = tl ? position.parseTimelinePosition(tl, helpers.isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); + } /** @type {String|Number} */ - let output = offset + ((spacing * helpers.round(values$1[staggerIndex], 2)) || 0); - if (params.modifier) output = params.modifier(output); + let output = cachedOffset + ((spacing * helpers.round(values$1[staggerIndex], 2)) || 0); + if (params.modifier) output = params.modifier(/** @type {Number} */(output)); if (unitMatch) output = `${output}${unitMatch[2]}`; return output; } diff --git a/dist/modules/utils/stagger.js b/dist/modules/utils/stagger.js index 094e8c689..5691a5e8a 100644 --- a/dist/modules/utils/stagger.js +++ b/dist/modules/utils/stagger.js @@ -1,12 +1,12 @@ /** * Anime.js - utils - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { unitsExecRgx, emptyString } from '../core/consts.js'; -import { isUnd, parseNumber, isFnc, abs, floor, sqrt, round, isArr, isNum, isStr, max } from '../core/helpers.js'; +import { isUnd, parseNumber, isFnc, isNum, sqrt, abs, floor, max, round, isArr, isStr } from '../core/helpers.js'; import { parseEase } from '../easings/eases/parser.js'; import { parseTimelinePosition } from '../timeline/position.js'; import { getOriginalAnimatableValue } from '../core/values.js'; @@ -17,6 +17,7 @@ import { shuffle } from './random.js'; * @import { * StaggerParams, * StaggerFunction, + * JSTarget, * } from '../types/index.js' */ @@ -32,24 +33,28 @@ import { shuffle } from './random.js'; * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @overload * @param {String} val * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @overload * @param {[Number, Number]} val * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @overload * @param {[String, String]} val * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @param {Number|String|[Number, Number]|[String, String]} val The staggered value or range * @param {StaggerParams} [params] The stagger parameters @@ -58,6 +63,7 @@ import { shuffle } from './random.js'; const stagger = (val, params = {}) => { let values = []; let maxValue = 0; + let cachedOffset; const from = params.from; const reversed = params.reversed; const ease = params.ease; @@ -65,12 +71,14 @@ const stagger = (val, params = {}) => { const hasSpring = hasEasing && !isUnd(/** @type {Spring} */(ease).ease); const staggerEase = hasSpring ? /** @type {Spring} */(ease).ease : hasEasing ? parseEase(ease) : null; const grid = params.grid; + const autoGrid = grid === true; const axis = params.axis; const customTotal = params.total; const fromFirst = isUnd(from) || from === 0 || from === 'first'; const fromCenter = from === 'center'; const fromLast = from === 'last'; const fromRandom = from === 'random'; + const fromArr = isArr(from); const isRange = isArr(val); const useProp = params.use; const val1 = isRange ? parseNumber(val[0]) : parseNumber(val); @@ -78,40 +86,129 @@ const stagger = (val, params = {}) => { const unitMatch = unitsExecRgx.exec((isRange ? val[1] : val) + emptyString); const start = params.start || 0 + (isRange ? val1 : 0); let fromIndex = fromFirst ? 0 : isNum(from) ? from : 0; - return (target, i, t, tl) => { + return (target, i, t, _, tl) => { const [ registeredTarget ] = registerTargets(target); - const total = isUnd(customTotal) ? t : customTotal; + const total = isUnd(customTotal) ? t.length : customTotal; const customIndex = !isUnd(useProp) ? isFnc(useProp) ? useProp(registeredTarget, i, total) : getOriginalAnimatableValue(registeredTarget, useProp) : false; const staggerIndex = isNum(customIndex) || isStr(customIndex) && isNum(+customIndex) ? +customIndex : i; if (fromCenter) fromIndex = (total - 1) / 2; if (fromLast) fromIndex = total - 1; if (!values.length) { - for (let index = 0; index < total; index++) { - if (!grid) { - values.push(abs(fromIndex - index)); + if (autoGrid) { + let hasPositions = true; + let minPosX = Infinity; + let minPosY = Infinity; + let maxPosX = -Infinity; + let maxPosY = -Infinity; + const pxArr = []; + const pyArr = []; + for (let index = 0; index < total; index++) { + const el = t[index]; + let px = 0; + let py = 0; + let found = false; + if (el && isFnc(el.getBoundingClientRect)) { + const rect = el.getBoundingClientRect(); + px = rect.left + rect.width / 2; + py = rect.top + rect.height / 2; + found = true; + } else { + const obj = /** @type {JSTarget} */(el); + if (obj && isNum(obj.x) && isNum(obj.y)) { + px = obj.x; + py = obj.y; + found = true; + } + } + if (!found) { + hasPositions = false; + break; + } + pxArr.push(px); + pyArr.push(py); + if (px < minPosX) minPosX = px; + if (py < minPosY) minPosY = py; + if (px > maxPosX) maxPosX = px; + if (py > maxPosY) maxPosY = py; + } + if (hasPositions) { + let fX = pxArr[0]; + let fY = pyArr[0]; + if (fromArr) { + fX = minPosX + from[0] * (maxPosX - minPosX); + fY = minPosY + from[1] * (maxPosY - minPosY); + } else if (fromCenter) { + fX = (minPosX + maxPosX) / 2; + fY = (minPosY + maxPosY) / 2; + } else if (fromLast) { + fX = pxArr[total - 1]; + fY = pyArr[total - 1]; + } else if (isNum(from)) { + fX = pxArr[from]; + fY = pyArr[from]; + } + for (let index = 0; index < total; index++) { + const distanceX = fX - pxArr[index]; + const distanceY = fY - pyArr[index]; + let value = sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values.push(value); + } + let minDist = Infinity; + for (let index = 0, l = values.length; index < l; index++) { + const absVal = abs(values[index]); + if (absVal > 0 && absVal < minDist) minDist = absVal; + } + if (minDist > 0 && minDist < Infinity) { + for (let index = 0, l = values.length; index < l; index++) { + values[index] = values[index] / minDist; + } + } } else { - const fromX = !fromCenter ? fromIndex % grid[0] : (grid[0] - 1) / 2; - const fromY = !fromCenter ? floor(fromIndex / grid[0]) : (grid[1] - 1) / 2; - const toX = index % grid[0]; - const toY = floor(index / grid[0]); - const distanceX = fromX - toX; - const distanceY = fromY - toY; - let value = sqrt(distanceX * distanceX + distanceY * distanceY); - if (axis === 'x') value = -distanceX; - if (axis === 'y') value = -distanceY; - values.push(value); + for (let index = 0; index < total; index++) { + values.push(abs(fromIndex - index)); + } + } + } else { + for (let index = 0; index < total; index++) { + if (!grid) { + values.push(abs(fromIndex - index)); + } else { + let fromX, fromY; + if (fromArr) { + fromX = from[0] * (grid[0] - 1); + fromY = from[1] * (grid[1] - 1); + } else if (fromCenter) { + fromX = (grid[0] - 1) / 2; + fromY = (grid[1] - 1) / 2; + } else { + fromX = fromIndex % grid[0]; + fromY = floor(fromIndex / grid[0]); + } + const toX = index % grid[0]; + const toY = floor(index / grid[0]); + const distanceX = fromX - toX; + const distanceY = fromY - toY; + let value = sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values.push(value); + } } - maxValue = max(...values); } + maxValue = max(...values); if (staggerEase) values = values.map(val => staggerEase(val / maxValue) * maxValue); if (reversed) values = values.map(val => axis ? (val < 0) ? val * -1 : -val : abs(maxValue - val)); if (fromRandom) values = shuffle(values); } const spacing = isRange ? (val2 - val1) / maxValue : val1; - const offset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); + if (isUnd(cachedOffset)) { + cachedOffset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); + } /** @type {String|Number} */ - let output = offset + ((spacing * round(values[staggerIndex], 2)) || 0); - if (params.modifier) output = params.modifier(output); + let output = cachedOffset + ((spacing * round(values[staggerIndex], 2)) || 0); + if (params.modifier) output = params.modifier(/** @type {Number} */(output)); if (unitMatch) output = `${output}${unitMatch[2]}`; return output; } diff --git a/dist/modules/utils/target.cjs b/dist/modules/utils/target.cjs index cc95aa57d..e04fcc38d 100644 --- a/dist/modules/utils/target.cjs +++ b/dist/modules/utils/target.cjs @@ -1,8 +1,8 @@ /** * Anime.js - utils - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/utils/target.js b/dist/modules/utils/target.js index ecce17897..b8e1f74f5 100644 --- a/dist/modules/utils/target.js +++ b/dist/modules/utils/target.js @@ -1,8 +1,8 @@ /** * Anime.js - utils - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { globals } from '../core/globals.js'; diff --git a/dist/modules/utils/time.cjs b/dist/modules/utils/time.cjs index 9336c47c1..45caa96cc 100644 --- a/dist/modules/utils/time.cjs +++ b/dist/modules/utils/time.cjs @@ -1,8 +1,8 @@ /** * Anime.js - utils - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -28,19 +28,20 @@ const sync = (callback = consts.noop) => { }; /** - * @param {(...args: any[]) => Tickable | ((...args: any[]) => void)} constructor + * @param {(...args: any[]) => Tickable | ((...args: any[]) => void) | void} constructor * @return {(...args: any[]) => Tickable | ((...args: any[]) => void)} */ const keepTime = constructor => { /** @type {Tickable} */ let tracked; return (...args) => { - let currentIteration, currentIterationProgress, reversed, alternate; + let currentIteration, currentIterationProgress, reversed, alternate, startTime; if (tracked) { currentIteration = tracked.currentIteration; currentIterationProgress = tracked.iterationProgress; reversed = tracked.reversed; alternate = tracked._alternate; + startTime = tracked._startTime; tracked.revert(); } const cleanup = constructor(...args); @@ -48,6 +49,7 @@ const keepTime = constructor => { if (!helpers.isUnd(currentIterationProgress)) { /** @type {Tickable} */(tracked).currentIteration = currentIteration; /** @type {Tickable} */(tracked).iterationProgress = (alternate ? !(currentIteration % 2) ? reversed : !reversed : reversed) ? 1 - currentIterationProgress : currentIterationProgress; + /** @type {Tickable} */(tracked)._startTime = startTime; } return cleanup || consts.noop; } diff --git a/dist/modules/utils/time.d.ts b/dist/modules/utils/time.d.ts index ee11bb122..1e527907a 100644 --- a/dist/modules/utils/time.d.ts +++ b/dist/modules/utils/time.d.ts @@ -1,5 +1,5 @@ export function sync(callback?: Callback): Timer; -export function keepTime(constructor: (...args: any[]) => Tickable | ((...args: any[]) => void)): (...args: any[]) => Tickable | ((...args: any[]) => void); +export function keepTime(constructor: (...args: any[]) => Tickable | ((...args: any[]) => void) | void): (...args: any[]) => Tickable | ((...args: any[]) => void); import { Timer } from '../timer/timer.js'; import type { Callback } from '../types/index.js'; import type { Tickable } from '../types/index.js'; diff --git a/dist/modules/utils/time.js b/dist/modules/utils/time.js index e83deb1f3..5b3499016 100644 --- a/dist/modules/utils/time.js +++ b/dist/modules/utils/time.js @@ -1,8 +1,8 @@ /** * Anime.js - utils - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { noop } from '../core/consts.js'; @@ -26,19 +26,20 @@ const sync = (callback = noop) => { }; /** - * @param {(...args: any[]) => Tickable | ((...args: any[]) => void)} constructor + * @param {(...args: any[]) => Tickable | ((...args: any[]) => void) | void} constructor * @return {(...args: any[]) => Tickable | ((...args: any[]) => void)} */ const keepTime = constructor => { /** @type {Tickable} */ let tracked; return (...args) => { - let currentIteration, currentIterationProgress, reversed, alternate; + let currentIteration, currentIterationProgress, reversed, alternate, startTime; if (tracked) { currentIteration = tracked.currentIteration; currentIterationProgress = tracked.iterationProgress; reversed = tracked.reversed; alternate = tracked._alternate; + startTime = tracked._startTime; tracked.revert(); } const cleanup = constructor(...args); @@ -46,6 +47,7 @@ const keepTime = constructor => { if (!isUnd(currentIterationProgress)) { /** @type {Tickable} */(tracked).currentIteration = currentIteration; /** @type {Tickable} */(tracked).iterationProgress = (alternate ? !(currentIteration % 2) ? reversed : !reversed : reversed) ? 1 - currentIterationProgress : currentIterationProgress; + /** @type {Tickable} */(tracked)._startTime = startTime; } return cleanup || noop; } diff --git a/dist/modules/waapi/composition.cjs b/dist/modules/waapi/composition.cjs index 83bbc70c7..536c72fdf 100644 --- a/dist/modules/waapi/composition.cjs +++ b/dist/modules/waapi/composition.cjs @@ -1,8 +1,8 @@ /** * Anime.js - waapi - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -82,7 +82,7 @@ const addWAAPIAnimation = (parent, $el, property, keyframes, params) => { parent.animations.push(animation); removeWAAPIAnimation($el, property); helpers.addChild(WAAPIAnimationsLookups, { parent, animation, $el, property, _next: null, _prev: null }); - const handleRemove = () => { removeWAAPIAnimation($el, property, parent); }; + const handleRemove = () => removeWAAPIAnimation($el, property, parent); animation.oncancel = handleRemove; animation.onremove = handleRemove; if (!parent.persist) { diff --git a/dist/modules/waapi/composition.js b/dist/modules/waapi/composition.js index 519785e1a..bcdc7d2b5 100644 --- a/dist/modules/waapi/composition.js +++ b/dist/modules/waapi/composition.js @@ -1,8 +1,8 @@ /** * Anime.js - waapi - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ import { removeChild, addChild } from '../core/helpers.js'; @@ -80,7 +80,7 @@ const addWAAPIAnimation = (parent, $el, property, keyframes, params) => { parent.animations.push(animation); removeWAAPIAnimation($el, property); addChild(WAAPIAnimationsLookups, { parent, animation, $el, property, _next: null, _prev: null }); - const handleRemove = () => { removeWAAPIAnimation($el, property, parent); }; + const handleRemove = () => removeWAAPIAnimation($el, property, parent); animation.oncancel = handleRemove; animation.onremove = handleRemove; if (!parent.persist) { diff --git a/dist/modules/waapi/index.cjs b/dist/modules/waapi/index.cjs index 37c09d716..15fd44e71 100644 --- a/dist/modules/waapi/index.cjs +++ b/dist/modules/waapi/index.cjs @@ -1,8 +1,8 @@ /** * Anime.js - waapi - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; diff --git a/dist/modules/waapi/index.js b/dist/modules/waapi/index.js index 1848e02bc..44597f70e 100644 --- a/dist/modules/waapi/index.js +++ b/dist/modules/waapi/index.js @@ -1,8 +1,8 @@ /** * Anime.js - waapi - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ export { WAAPIAnimation, waapi } from './waapi.js'; diff --git a/dist/modules/waapi/waapi.cjs b/dist/modules/waapi/waapi.cjs index 37aa8e252..0bd3b4dcc 100644 --- a/dist/modules/waapi/waapi.cjs +++ b/dist/modules/waapi/waapi.cjs @@ -1,8 +1,8 @@ /** * Anime.js - waapi - CJS - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ 'use strict'; @@ -116,12 +116,12 @@ let transformsPropertiesRegistered = null; * @param {WAAPIKeyframeValue} value * @param {DOMTarget} $el * @param {Number} i - * @param {Number} targetsLength + * @param {DOMTargetsArray} parsedTargets * @return {String} */ -const normalizeTweenValue = (propName, value, $el, i, targetsLength) => { +const normalizeTweenValue = (propName, value, $el, i, parsedTargets) => { // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables - let v = helpers.isStr(value) ? value : values.getFunctionValue(/** @type {any} */(value), $el, i, targetsLength); + let v = helpers.isStr(value) ? value : values.getFunctionValue(/** @type {any} */(value), $el, i, parsedTargets, null, null); if (!helpers.isNum(v)) return v; if (commonDefaultPXProperties.includes(propName) || helpers.stringStartsWith(propName, 'translate')) return `${v}px`; if (helpers.stringStartsWith(propName, 'rotate') || helpers.stringStartsWith(propName, 'skew')) return `${v}deg`; @@ -134,18 +134,18 @@ const normalizeTweenValue = (propName, value, $el, i, targetsLength) => { * @param {WAAPIKeyframeValue} from * @param {WAAPIKeyframeValue} to * @param {Number} i - * @param {Number} targetsLength + * @param {DOMTargetsArray} parsedTargets * @return {WAAPITweenValue} */ -const parseIndividualTweenValue = ($el, propName, from, to, i, targetsLength) => { +const parseIndividualTweenValue = ($el, propName, from, to, i, parsedTargets) => { /** @type {WAAPITweenValue} */ let tweenValue = '0'; - const computedTo = !helpers.isUnd(to) ? normalizeTweenValue(propName, to, $el, i, targetsLength) : getComputedStyle($el)[propName]; + const computedTo = !helpers.isUnd(to) ? normalizeTweenValue(propName, to, $el, i, parsedTargets) : getComputedStyle($el)[propName]; if (!helpers.isUnd(from)) { - const computedFrom = normalizeTweenValue(propName, from, $el, i, targetsLength); + const computedFrom = normalizeTweenValue(propName, from, $el, i, parsedTargets); tweenValue = [computedFrom, computedTo]; } else { - tweenValue = helpers.isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, targetsLength)) : computedTo; + tweenValue = helpers.isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, parsedTargets)) : computedTo; } return tweenValue; }; @@ -184,14 +184,11 @@ class WAAPIAnimation { } const parsedTargets = targets.registerTargets(targets$1); - const targetsLength = parsedTargets.length; - if (!targetsLength) { + if (!parsedTargets.length) { console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`); } - const ease = values.setValue(params.ease, parseWAAPIEasing(globals.globals.defaults.ease)); - const spring = /** @type {Spring} */(ease).ease && ease; const autoplay = values.setValue(params.autoplay, globals.globals.defaults.autoplay); const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false; const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true; @@ -202,8 +199,6 @@ class WAAPIAnimation { const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal'; /** @type {FillMode} */ const fill = 'both'; // We use 'both' here because the animation can be reversed during playback - /** @type {String} */ - const easing = parseWAAPIEasing(ease); const timeScale = (globals.globals.timeScale === 1 ? 1 : consts.K); /** @type {DOMTargetsArray}] */ @@ -244,10 +239,19 @@ class WAAPIAnimation { const elStyle = $el.style; const inlineStyles = this._inlineStyles[i] = {}; + const easeToParse = values.setValue(params.ease, globals.globals.defaults.ease); + + const easeFunctionResult = values.getFunctionValue(easeToParse, $el, i, parsedTargets, null, null); + const keyEasing = helpers.isFnc(easeFunctionResult) || helpers.isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; + + const spring = /** @type {Spring} */(easeToParse).ease && easeToParse; + /** @type {String} */ + const easing = parseWAAPIEasing(keyEasing); + /** @type {Number} */ - const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : values.getFunctionValue(values.setValue(params.duration, globals.globals.defaults.duration), $el, i, targetsLength)) * timeScale; + const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : values.getFunctionValue(values.setValue(params.duration, globals.globals.defaults.duration), $el, i, parsedTargets, null, null)) * timeScale; /** @type {Number} */ - const delay = values.getFunctionValue(values.setValue(params.delay, globals.globals.defaults.delay), $el, i, targetsLength) * timeScale; + const delay = values.getFunctionValue(values.setValue(params.delay, globals.globals.defaults.delay), $el, i, parsedTargets, null, null) * timeScale; /** @type {CompositeOperation} */ const composite = /** @type {CompositeOperation} */(values.setValue(params.composition, 'replace')); @@ -268,24 +272,24 @@ class WAAPIAnimation { let parsedPropertyValue; if (helpers.isObj(propertyValue)) { const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue); - const tweenOptionsEase = values.setValue(tweenOptions.ease, ease); + const tweenOptionsEase = values.setValue(tweenOptions.ease, easing); const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase; const to = /** @type {WAAPITweenOptions} */(tweenOptions).to; const from = /** @type {WAAPITweenOptions} */(tweenOptions).from; /** @type {Number} */ - tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : values.getFunctionValue(values.setValue(tweenOptions.duration, duration), $el, i, targetsLength)) * timeScale; + tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : values.getFunctionValue(values.setValue(tweenOptions.duration, duration), $el, i, parsedTargets, null, null)) * timeScale; /** @type {Number} */ - tweenParams.delay = values.getFunctionValue(values.setValue(tweenOptions.delay, delay), $el, i, targetsLength) * timeScale; + tweenParams.delay = values.getFunctionValue(values.setValue(tweenOptions.delay, delay), $el, i, parsedTargets, null, null) * timeScale; /** @type {CompositeOperation} */ tweenParams.composite = /** @type {CompositeOperation} */(values.setValue(tweenOptions.composition, composite)); /** @type {String} */ tweenParams.easing = parseWAAPIEasing(tweenOptionsEase); - parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, targetsLength); + parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); if (individualTransformProperty) { keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; cachedTransforms[individualTransformProperty] = parsedPropertyValue; } else { - keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, targetsLength); + keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); } composition.addWAAPIAnimation(this, $el, name, keyframes, tweenParams); if (!helpers.isUnd(from)) { @@ -298,8 +302,8 @@ class WAAPIAnimation { } } else { parsedPropertyValue = helpers.isArr(propertyValue) ? - propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, targetsLength)) : - normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, targetsLength); + propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, parsedTargets)) : + normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, parsedTargets); if (individualTransformProperty) { keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; cachedTransforms[individualTransformProperty] = parsedPropertyValue; @@ -333,9 +337,10 @@ class WAAPIAnimation { * @return {this} */ forEach(callback) { - const cb = helpers.isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; - this.animations.forEach(cb); - return this; + try { + const cb = helpers.isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; + this.animations.forEach(cb); + } catch {} return this; } get speed() { @@ -431,14 +436,21 @@ class WAAPIAnimation { cancel() { this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise - return this.commitStyles().forEach('cancel'); + this.commitStyles().forEach('cancel'); + this.animations.length = 0; // Needed to release all animations from memory + requestAnimationFrame(() => { + this.targets.forEach(($el) => { // Needed to avoid unecessary inline transorms + if ($el.style.transform === 'none') $el.style.removeProperty('transform'); + }); + }); + return this; } revert() { // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted, // This means if you have multiple animations animating different transforms on the same target, // reverting one of them will also override the transform property of the other animations. - // A better approach would be to store the original custom property values is they exist instead of the entire transform value, + // A better approach would be to store the original custom property values if they exist instead of the entire transform value, // and update the CSS variables with the orignal value this.cancel().targets.forEach(($el, i) => { const targetStyle = $el.style; @@ -448,7 +460,7 @@ class WAAPIAnimation { if (helpers.isUnd(originalInlinedValue) || originalInlinedValue === consts.emptyString) { targetStyle.removeProperty(helpers.toLowerCase(name)); } else { - targetStyle[name] = originalInlinedValue; + $el.style[name] = originalInlinedValue; } } // Remove style attribute if empty diff --git a/dist/modules/waapi/waapi.js b/dist/modules/waapi/waapi.js index 7fd8c50d0..7cff71710 100644 --- a/dist/modules/waapi/waapi.js +++ b/dist/modules/waapi/waapi.js @@ -1,15 +1,15 @@ /** * Anime.js - waapi - ESM - * @version v4.2.2 + * @version v4.4.1 * @license MIT - * @copyright 2025 - Julian Garnier + * @copyright 2026 - Julian Garnier */ -import { isNil, isUnd, stringStartsWith, isKey, isObj, isArr, toLowerCase, round, isStr, isFnc, isNum } from '../core/helpers.js'; +import { isNil, isUnd, stringStartsWith, isKey, isObj, isArr, isStr, toLowerCase, round, isFnc, isNum } from '../core/helpers.js'; import { scope, globals } from '../core/globals.js'; import { registerTargets } from '../core/targets.js'; import { setValue, getFunctionValue } from '../core/values.js'; -import { isBrowser, validTransforms, noop, transformsSymbol, shortTransforms, transformsFragmentStrings, emptyString, K } from '../core/consts.js'; +import { isBrowser, validTransforms, K, noop, transformsSymbol, shortTransforms, transformsFragmentStrings, emptyString } from '../core/consts.js'; import { none } from '../easings/none.js'; import { parseEaseString } from '../easings/eases/parser.js'; import { addWAAPIAnimation } from './composition.js'; @@ -114,12 +114,12 @@ let transformsPropertiesRegistered = null; * @param {WAAPIKeyframeValue} value * @param {DOMTarget} $el * @param {Number} i - * @param {Number} targetsLength + * @param {DOMTargetsArray} parsedTargets * @return {String} */ -const normalizeTweenValue = (propName, value, $el, i, targetsLength) => { +const normalizeTweenValue = (propName, value, $el, i, parsedTargets) => { // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables - let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, targetsLength); + let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, parsedTargets, null, null); if (!isNum(v)) return v; if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`; if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`; @@ -132,18 +132,18 @@ const normalizeTweenValue = (propName, value, $el, i, targetsLength) => { * @param {WAAPIKeyframeValue} from * @param {WAAPIKeyframeValue} to * @param {Number} i - * @param {Number} targetsLength + * @param {DOMTargetsArray} parsedTargets * @return {WAAPITweenValue} */ -const parseIndividualTweenValue = ($el, propName, from, to, i, targetsLength) => { +const parseIndividualTweenValue = ($el, propName, from, to, i, parsedTargets) => { /** @type {WAAPITweenValue} */ let tweenValue = '0'; - const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, targetsLength) : getComputedStyle($el)[propName]; + const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, parsedTargets) : getComputedStyle($el)[propName]; if (!isUnd(from)) { - const computedFrom = normalizeTweenValue(propName, from, $el, i, targetsLength); + const computedFrom = normalizeTweenValue(propName, from, $el, i, parsedTargets); tweenValue = [computedFrom, computedTo]; } else { - tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, targetsLength)) : computedTo; + tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, parsedTargets)) : computedTo; } return tweenValue; }; @@ -182,14 +182,11 @@ class WAAPIAnimation { } const parsedTargets = registerTargets(targets); - const targetsLength = parsedTargets.length; - if (!targetsLength) { + if (!parsedTargets.length) { console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`); } - const ease = setValue(params.ease, parseWAAPIEasing(globals.defaults.ease)); - const spring = /** @type {Spring} */(ease).ease && ease; const autoplay = setValue(params.autoplay, globals.defaults.autoplay); const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false; const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true; @@ -200,8 +197,6 @@ class WAAPIAnimation { const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal'; /** @type {FillMode} */ const fill = 'both'; // We use 'both' here because the animation can be reversed during playback - /** @type {String} */ - const easing = parseWAAPIEasing(ease); const timeScale = (globals.timeScale === 1 ? 1 : K); /** @type {DOMTargetsArray}] */ @@ -242,10 +237,19 @@ class WAAPIAnimation { const elStyle = $el.style; const inlineStyles = this._inlineStyles[i] = {}; + const easeToParse = setValue(params.ease, globals.defaults.ease); + + const easeFunctionResult = getFunctionValue(easeToParse, $el, i, parsedTargets, null, null); + const keyEasing = isFnc(easeFunctionResult) || isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; + + const spring = /** @type {Spring} */(easeToParse).ease && easeToParse; + /** @type {String} */ + const easing = parseWAAPIEasing(keyEasing); + /** @type {Number} */ - const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, targetsLength)) * timeScale; + const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, parsedTargets, null, null)) * timeScale; /** @type {Number} */ - const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, targetsLength) * timeScale; + const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, parsedTargets, null, null) * timeScale; /** @type {CompositeOperation} */ const composite = /** @type {CompositeOperation} */(setValue(params.composition, 'replace')); @@ -266,24 +270,24 @@ class WAAPIAnimation { let parsedPropertyValue; if (isObj(propertyValue)) { const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue); - const tweenOptionsEase = setValue(tweenOptions.ease, ease); + const tweenOptionsEase = setValue(tweenOptions.ease, easing); const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase; const to = /** @type {WAAPITweenOptions} */(tweenOptions).to; const from = /** @type {WAAPITweenOptions} */(tweenOptions).from; /** @type {Number} */ - tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, targetsLength)) * timeScale; + tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, parsedTargets, null, null)) * timeScale; /** @type {Number} */ - tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, targetsLength) * timeScale; + tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, parsedTargets, null, null) * timeScale; /** @type {CompositeOperation} */ tweenParams.composite = /** @type {CompositeOperation} */(setValue(tweenOptions.composition, composite)); /** @type {String} */ tweenParams.easing = parseWAAPIEasing(tweenOptionsEase); - parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, targetsLength); + parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); if (individualTransformProperty) { keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; cachedTransforms[individualTransformProperty] = parsedPropertyValue; } else { - keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, targetsLength); + keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); } addWAAPIAnimation(this, $el, name, keyframes, tweenParams); if (!isUnd(from)) { @@ -296,8 +300,8 @@ class WAAPIAnimation { } } else { parsedPropertyValue = isArr(propertyValue) ? - propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, targetsLength)) : - normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, targetsLength); + propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, parsedTargets)) : + normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, parsedTargets); if (individualTransformProperty) { keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; cachedTransforms[individualTransformProperty] = parsedPropertyValue; @@ -331,9 +335,10 @@ class WAAPIAnimation { * @return {this} */ forEach(callback) { - const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; - this.animations.forEach(cb); - return this; + try { + const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; + this.animations.forEach(cb); + } catch {} return this; } get speed() { @@ -429,14 +434,21 @@ class WAAPIAnimation { cancel() { this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise - return this.commitStyles().forEach('cancel'); + this.commitStyles().forEach('cancel'); + this.animations.length = 0; // Needed to release all animations from memory + requestAnimationFrame(() => { + this.targets.forEach(($el) => { // Needed to avoid unecessary inline transorms + if ($el.style.transform === 'none') $el.style.removeProperty('transform'); + }); + }); + return this; } revert() { // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted, // This means if you have multiple animations animating different transforms on the same target, // reverting one of them will also override the transform property of the other animations. - // A better approach would be to store the original custom property values is they exist instead of the entire transform value, + // A better approach would be to store the original custom property values if they exist instead of the entire transform value, // and update the CSS variables with the orignal value this.cancel().targets.forEach(($el, i) => { const targetStyle = $el.style; @@ -446,7 +458,7 @@ class WAAPIAnimation { if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) { targetStyle.removeProperty(toLowerCase(name)); } else { - targetStyle[name] = originalInlinedValue; + $el.style[name] = originalInlinedValue; } } // Remove style attribute if empty diff --git a/examples/additive-creature/index.js b/examples/additive-creature/index.js index 1343c018e..4d22e3a70 100644 --- a/examples/additive-creature/index.js +++ b/examples/additive-creature/index.js @@ -29,7 +29,7 @@ utils.set(particuleEls, { modifier: v => `hsl(4, 70%, ${v}%)`, }), boxShadow: stagger([8, 1], { grid, from, - modifier: v => `0px 0px ${utils.round(v, 0)}em 0px var(--red)`, + modifier: v => `0px 0px ${utils.round(v, 0)}em 0px var(--red-1)`, }), zIndex: stagger([rows * rows, 1], { grid, from, modifier: utils.round(0) }), }); diff --git a/examples/animejs-v4-logo-animation/index.js b/examples/animejs-v4-logo-animation/index.js index f5bff0338..11abf575d 100644 --- a/examples/animejs-v4-logo-animation/index.js +++ b/examples/animejs-v4-logo-animation/index.js @@ -41,6 +41,7 @@ const tl = createTimeline({ }); tl.label('FALL') tl.add('#line-0', { + id: 'line-0 wiggle', translateY: {to: [-280, 19], ease: 'inQuart', duration: 320 }, scaleY: { to: [3, 1.75], ease: 'outElastic(1, 1.4)', duration: 300, delay: 320 }, scaleX: { to: [.8, 1], ease: 'outElastic(1, 1.4)', duration: 650, delay: 320 }, @@ -231,6 +232,7 @@ tl.label('TEXT', '<-=600') }, 'OUTRO') .add(['#a-1', '#n-1', '#i-1', '#m-1', '#e-1'], { + id: 'a n i m e', translateY: 80, duration: 300, ease: 'outQuint', diff --git a/examples/assets/css/styles.css b/examples/assets/css/styles.css index a17bec6e9..49e595b22 100644 --- a/examples/assets/css/styles.css +++ b/examples/assets/css/styles.css @@ -1,25 +1,170 @@ -:root { - --black: #252423; - --white: #F6F4F2; - --white-2: #FFF; - --grey: #b8b6b3; - --red: #FF4B4B; - --orange: #FF8F42; - --lightorange: #FFC730; - --yellow: #F6FF56; - --citrus: #A4FF4F; - --green: #18FF74; - --darkgreen: #00D672; - --turquoise: #3CFFEC; - --skyblue: #61C3FF; - --kingblue: #5A87FF; - --lavender: #8453E3; - --purple: #C26EFF; - --pink: #FB89FB; +@font-face { + font-family: 'IoskeleyMono'; + src: url('../fonts/IoskeleyMono-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'IoskeleyMono'; + src: url('../fonts/IoskeleyMono-Bold.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: swap; +} +:root { + --black-1: #252423; + --black-2: #262422; + --black-3: #262422; + --black-4: #262422; + --black-5: #272421; + --black-6: #272421; + --white-1: #f6f4f2; + --white-2: #b8b6b3; + --white-3: #9a9593; + --white-4: #524f49; + --white-5: #413d39; + --white-6: #312e2b; + --red-1: #ff4b4b; + --red-2: #bf3e3e; + --red-3: #9e3838; + --red-4: #532a29; + --red-5: #412726; + --red-6: #322523; + --corail-1: #ff8333; + --corail-2: #bf672d; + --corail-3: #9e582b; + --corail-4: #533724; + --corail-5: #412f23; + --corail-6: #322922; + --orange-1: #ffa828; + --orange-2: #bf8026; + --orange-3: #9e6d25; + --orange-4: #523e23; + --orange-5: #413422; + --orange-6: #322b21; + --yellow-1: #ffcc2a; + --yellow-2: #bf9a27; + --yellow-3: #9e8026; + --yellow-4: #524723; + --yellow-5: #413922; + --yellow-6: #322d21; + --citrus-1: #f9f640; + --citrus-2: #bab836; + --citrus-3: #9b9932; + --citrus-4: #515027; + --citrus-5: #403f24; + --citrus-6: #323022; + --lime-1: #b7ff54; + --lime-2: #8bbe44; + --lime-3: #759d3d; + --lime-4: #42522b; + --lime-5: #384027; + --lime-6: #2d3123; + --green-1: #6aff65; + --green-2: #54be50; + --green-3: #4a9e45; + --green-4: #31522e; + --green-5: #2c4029; + --green-6: #273124; + --emerald-1: #57f695; + --emerald-2: #47b873; + --emerald-3: #409962; + --emerald-4: #2d5039; + --emerald-5: #293f30; + --emerald-6: #263128; + --turquoise-1: #66ffbc; + --turquoise-2: #52be8e; + --turquoise-3: #479e77; + --turquoise-4: #305242; + --turquoise-5: #2b4035; + --turquoise-6: #26312b; + --cyan-1: #26f2d5; + --cyan-2: #25b5a0; + --cyan-3: #259686; + --cyan-4: #244e48; + --cyan-5: #233f39; + --cyan-6: #23302d; + --sega-1: #05dbe9; + --sega-2: #0fa4ae; + --sega-3: #138990; + --sega-4: #1e4a4c; + --sega-5: #203b3c; + --sega-6: #212f2f; + --sky-1: #33b3f1; + --sky-2: #2e88b4; + --sky-3: #2c7395; + --sky-4: #26424e; + --sky-5: #25363e; + --sky-6: #242c2f; + --indigo-1: #717aff; + --indigo-2: #595fbe; + --indigo-3: #4d519e; + --indigo-4: #323351; + --indigo-5: #2c2c3f; + --indigo-6: #282630; + --lavender-1: #a369ff; + --lavender-2: #7d53be; + --lavender-3: #6a489e; + --lavender-4: #3e3051; + --lavender-5: #342a3f; + --lavender-6: #2b2530; + --purple-1: #c06ddf; + --purple-2: #9356a8; + --purple-3: #7b4a8c; + --purple-4: #45314b; + --purple-5: #392b3c; + --purple-6: #2f262d; + --magenta-1: #e962bf; + --magenta-2: #af4e90; + --magenta-3: #93447a; + --magenta-4: #4e2e43; + --magenta-5: #3f2936; + --magenta-6: #31252b; + --pink-1: #ff86a7; + --pink-2: #bf687f; + --pink-3: #9f586b; + --pink-4: #53363c; + --pink-5: #412e32; + --pink-6: #322729; + --bg-1: #252423; + --bg-2: #2a2928; + --bg-3: #2f2e2d; + --bg-4: #353433; + --bg-5: #3a3938; + --fg-1: #dddcda; + --fg-2: #c6c3c1; + --fg-3: #96918f; + --fg-4: #65655e; + --fg-5: #33332e; --input-border-radius: .25rem; + --br: 1rem; + --padding: 1rem; + --border-width: 1px; } +[data-color="fg"] { --color-1: var(--fg-1); --color-2: var(--fg-2); --color-3: var(--fg-3); --color-4: var(--fg-4); --color-5: var(--fg-5);} +[data-color="bg"] { --color-1: var(--bg-1); --color-2: var(--bg-2); --color-3: var(--bg-3); --color-4: var(--bg-4); --color-5: var(--bg-5);} +[data-color="0"] { --color-1: var(--red-1); --color-2: var(--red-2); --color-3: var(--red-3); --color-4: var(--red-4); --color-5: var(--red-5);} +[data-color="1"] { --color-1: var(--corail-1); --color-2: var(--corail-2); --color-3: var(--corail-3); --color-4: var(--corail-4); --color-5: var(--corail-5);} +[data-color="2"] { --color-1: var(--orange-1); --color-2: var(--orange-2); --color-3: var(--orange-3); --color-4: var(--orange-4); --color-5: var(--orange-5);} +[data-color="3"] { --color-1: var(--yellow-1); --color-2: var(--yellow-2); --color-3: var(--yellow-3); --color-4: var(--yellow-4); --color-5: var(--yellow-5);} +[data-color="4"] { --color-1: var(--citrus-1); --color-2: var(--citrus-2); --color-3: var(--citrus-3); --color-4: var(--citrus-4); --color-5: var(--citrus-5);} +[data-color="5"] { --color-1: var(--lime-1); --color-2: var(--lime-2); --color-3: var(--lime-3); --color-4: var(--lime-4); --color-5: var(--lime-5);} +[data-color="6"] { --color-1: var(--green-1); --color-2: var(--green-2); --color-3: var(--green-3); --color-4: var(--green-4); --color-5: var(--green-5);} +[data-color="7"] { --color-1: var(--emerald-1); --color-2: var(--emerald-2); --color-3: var(--emerald-3); --color-4: var(--emerald-4); --color-5: var(--emerald-5);} +[data-color="8"] { --color-1: var(--turquoise-1); --color-2: var(--turquoise-2); --color-3: var(--turquoise-3); --color-4: var(--turquoise-4); --color-5: var(--turquoise-5);} +[data-color="9"] { --color-1: var(--cyan-1); --color-2: var(--cyan-2); --color-3: var(--cyan-3); --color-4: var(--cyan-4); --color-5: var(--cyan-5);} +[data-color="10"] { --color-1: var(--sega-1); --color-2: var(--sega-2); --color-3: var(--sega-3); --color-4: var(--sega-4); --color-5: var(--sega-5);} +[data-color="11"] { --color-1: var(--sky-1); --color-2: var(--sky-2); --color-3: var(--sky-3); --color-4: var(--sky-4); --color-5: var(--sky-5);} +[data-color="12"] { --color-1: var(--indigo-1); --color-2: var(--indigo-2); --color-3: var(--indigo-3); --color-4: var(--indigo-4); --color-5: var(--indigo-5);} +[data-color="13"] { --color-1: var(--lavender-1); --color-2: var(--lavender-2); --color-3: var(--lavender-3); --color-4: var(--lavender-4); --color-5: var(--lavender-5);} +[data-color="14"] { --color-1: var(--purple-1); --color-2: var(--purple-2); --color-3: var(--purple-3); --color-4: var(--purple-4); --color-5: var(--purple-5);} +[data-color="15"] { --color-1: var(--magenta-1); --color-2: var(--magenta-2); --color-3: var(--magenta-3); --color-4: var(--magenta-4); --color-5: var(--magenta-5);} +[data-color="16"] { --color-1: var(--pink-1); --color-2: var(--pink-2); --color-3: var(--pink-3); --color-4: var(--pink-4); --color-5: var(--pink-5);} + *, *:before, *:after { box-sizing: border-box; margin: 0; @@ -31,16 +176,22 @@ html, body { - background-color: var(--black); - color: var(--white); - font-family: sans-serif; + background-color: var(--bg-1); + color: var(--fg-1); + font-family: 'IoskeleyMono', monospace, sans-serif; } -.black { color: var(--black); } -.white { color: var(--white); } -.red { color: var(--red); } -.blue { color: var(--skyblue); } -.green { color: var(--green); } +.br { border-radius: var(--br); } +.br .br { border-radius: calc(var(--br) * .5); } +.br .br .br { border-radius: calc(var(--br) * .25); } + +.black { color: var(--bg-1); } +.white { color: var(--fg-1); } +.red { color: var(--red-1); } +.orange { color: var(--corail-1); } +.yellow { color: var(--yellow-1); } +.blue { color: var(--sky-1); } +.green { color: var(--green-1); } .grid { --one-cell: 100px; @@ -122,7 +273,7 @@ select { width: 100%; flex-shrink: .5; border: none; - background-color: var(--white); + background-color: var(--white-1); border-radius: var(--input-border-radius); white-space: nowrap; margin: 0; diff --git a/examples/assets/fonts/IoskeleyMono-Bold.woff2 b/examples/assets/fonts/IoskeleyMono-Bold.woff2 new file mode 100644 index 000000000..0d0a0e517 Binary files /dev/null and b/examples/assets/fonts/IoskeleyMono-Bold.woff2 differ diff --git a/examples/assets/fonts/IoskeleyMono-Regular.woff2 b/examples/assets/fonts/IoskeleyMono-Regular.woff2 new file mode 100644 index 000000000..e90afff90 Binary files /dev/null and b/examples/assets/fonts/IoskeleyMono-Regular.woff2 differ diff --git a/examples/auto-layout/accordion/index.html b/examples/auto-layout/accordion/index.html new file mode 100644 index 000000000..e1ab67d6b --- /dev/null +++ b/examples/auto-layout/accordion/index.html @@ -0,0 +1,119 @@ + + + + Auto layout / Accordion / Anime.js + + + + + + +
+
+ +

Circles the Sun in only eighty-eight days, so its airless surface is blasted by radiation before plunging into frigid darkness each night. Craters overlap in chaotic mosaics and vast scarps wrinkle the crust where the entire planet shrank as its iron core cooled. Despite furnace-like daytime temperatures, radar has detected bright deposits of water ice tucked inside permanently shadowed polar craters. Spacecraft measurements show that this tiny world still hosts an active magnetic field, remnant volcanism, and intriguing hollows, proving that Mercury is far more dynamic than its cratered face suggests.

+
+
+ +

Glows brilliantly in twilight because sunlight reflects off clouds packed with sulfuric acid droplets, yet those same clouds trap enough heat to bake the surface hotter than 470 °C. Atmospheric pressure equals standing nearly a kilometer under Earth’s oceans, crushing most landers within hours. Radar mapping uncovered tessera highlands, pancake domes, and channels carved by lava, revealing a world periodically resurfaced by massive eruptions. Lightning flashes inside the dense haze, and hurricane-strength winds race around the planet despite its extremely slow rotation, creating a greenhouse furnace unlike anywhere else in the Solar System.

+
+
+ +

The only known planet where liquid water, protective magnetism, and an oxygen-rich atmosphere coexist to support complex life. Plate tectonics recycle crust, drive volcanism, and sculpt mountain ranges while oceans regulate climate and shuttle nutrients. The biosphere stretches from deep-sea vents to polar ice, constantly exchanging gases with the atmosphere. Our planet’s ozone layer filters harmful ultraviolet radiation, and the magnetic field deflects solar wind particles that would otherwise strip the air away. Earth’s constantly evolving landscapes, weather systems, and ecosystems are tightly coupled, making it a rare, self-sustaining oasis in space.

+
+
+ +

A cold desert wrapped in a thin carbon-dioxide atmosphere that kicks up dust storms capable of engulfing the entire planet. Towering shield volcanoes like Olympus Mons dwarf Everest, while Valles Marineris carves a canyon system stretching the width of North America. Orbiters have mapped dry river deltas, layered sedimentary rocks, and recurring slope streaks hinting at seasonal brines. Rovers continue to drill into clay-rich outcrops, finding organic molecules and mineral evidence that ancient lakes existed for millions of years. Today, permafrost and buried ice preserve a chemical record of Mars’s more hospitable past.

+
+
+ +

A colossal gas giant composed mainly of hydrogen and helium, with storm belts powered by deep internal heat and breakneck ten-hour rotation. The Great Red Spot, a spinning cyclone wider than Earth, has raged for centuries, while lightning and ammonia blizzards illuminate the turbulent cloud decks. Beneath the pastel bands lies a mantle of metallic hydrogen that conducts electricity and generates an enormous magnetic field extending millions of kilometers. Four large moons—Io, Europa, Ganymede, and Callisto—form a miniature planetary system whose volcanic eruptions and subsurface oceans are shaped by Jupiter’s immense gravity.

+
+
+ +

Pastel atmosphere hides fierce jet streams, but its shimmering rings steal the show: billions of icy grains orbiting within a disk only tens of meters thick. Shepherd moons sculpt gaps and produce ripples, demonstrating how gravity organizes material around planets. Saturn’s low density and rapid spin flatten the poles, while a deep layer of metallic hydrogen drives a magnetic field offset from the planet’s center. Moons like Titan and Enceladus harbor methane seas and geysering plumes that feed the faint E ring, making Saturn a laboratory for understanding both planetary interiors and moon-forming disks.

+
+
+ +

An ice giant tipped almost completely on its side, so its poles alternately bask in decades of continuous sunlight before plunging into equally long winters. Methane high in the atmosphere absorbs red light, giving the planet a calm cyan appearance occasionally punctuated by bright storms and polar vortices. A set of faint dark rings and small shepherd moons circle the equator, while larger moons display tectonic canyons and resurfaced plains that hint at ancient internal heat. Uranus’s magnetic field is wildly tilted and offset, causing auroras and charged-particle belts to wander unpredictably.

+
+
+ +

Orbits so far from the Sun that daylight is nine hundred times dimmer than on Earth, yet supersonic winds power storms bigger than continents. Methane clouds and high-altitude hazes create a deep sapphire hue interrupted by transient dark vortices and bright cirrus. A complex magnetosphere, offset from the planet’s center, produces wandering auroras, while the largest moon Triton orbits backward and erupts with nitrogen geysers. Neptune’s faint rings and small shepherd moons round out a system that continues to reveal surprises, offering the clearest glimpse of what icy exoplanets might look like.

+
+
+ + + diff --git a/examples/auto-layout/accordion/index.js b/examples/auto-layout/accordion/index.js new file mode 100644 index 000000000..11d13182f --- /dev/null +++ b/examples/auto-layout/accordion/index.js @@ -0,0 +1,13 @@ +import { createLayout, spring } from '../../../dist/modules/index.js'; + +const accordion = createLayout('.accordion', { + properties: ['background-color'], + ease: spring({ bounce: .2, duration: 450 }), + added: { opacity: 0, filter: 'blur(5px)' }, + removed: { opacity: 0, filter: 'blur(5px)' } +}); + +document.addEventListener('click', event => { + const $toggled = event.target.closest('.accordion button'); + if ($toggled) accordion.update(() => $toggled.parentElement.classList.toggle('is-open')) +}); diff --git a/examples/auto-layout/cards/index.html b/examples/auto-layout/cards/index.html new file mode 100644 index 000000000..39389c5f9 --- /dev/null +++ b/examples/auto-layout/cards/index.html @@ -0,0 +1,47 @@ + + + + + + Cards + + + + +
+ + + diff --git a/examples/auto-layout/cards/index.js b/examples/auto-layout/cards/index.js new file mode 100644 index 000000000..4140b3b9f --- /dev/null +++ b/examples/auto-layout/cards/index.js @@ -0,0 +1,37 @@ +import { createLayout } from '../../../dist/modules/index.js'; + +const cards = document.getElementById('cards') + +const lorem = [ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Sed do eiusmod tempor incididunt ut labore et dolore magna.', + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco.', + 'Duis aute irure dolor in reprehenderit in voluptate velit.', + 'Excepteur sint occaecat cupidatat non proident sunt in culpa.', + 'Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut.', +] + +// Generate 12 cards +for (let i = 1; i <= 12; i++) { + cards.innerHTML += ` +
+ Photo ${i} +
+

Card Title ${i}

+

${lorem[i % lorem.length]}

+
+
+ ` +} + +const layout = createLayout('#cards', { + duration: 1000 +}); + +document.querySelectorAll('nav button').forEach(btn => { + btn.onclick = () => { + layout.update(() => { + cards.className = btn.dataset.view; + }) + } +}) diff --git a/examples/auto-layout/code/index.html b/examples/auto-layout/code/index.html new file mode 100644 index 000000000..97a64281e --- /dev/null +++ b/examples/auto-layout/code/index.html @@ -0,0 +1,97 @@ + + + + Auto layout / Code / anime.js + + + + + + +
+ +
+
+/* WAAPI */
+
+document.querySelectorAll('.circle').forEach(($el, i) => {
+  el.animate({
+    translate: '100px',
+  }, {
+    duration: 1000,
+    delay: i * 100,
+    easing: 'ease-out',
+  }).finished.then(() => {
+    $el.style.translate = '100px';
+  })
+});
+
+
+ +
+
+/* ANIMEJS */
+
+waapi.animate('.circle', {
+  translate: '100px',
+  duration: 1000,
+  delay: stagger(100),
+  ease: 'ease-out',
+});
+
+
+ +
+ + + + diff --git a/examples/auto-layout/code/index.js b/examples/auto-layout/code/index.js new file mode 100644 index 000000000..b23ebd779 --- /dev/null +++ b/examples/auto-layout/code/index.js @@ -0,0 +1,59 @@ +import { createLayout, $, stagger, random } from '../../../dist/modules/index.js'; + +const keywordSet = /^(const|let|var|function|return|if|else|for|while|new|this|true|false|null|undefined|async|await|import|export|from|class|extends)$/; +const tokenRegex = /(['"`])(?:\\.|[^\\])*?\1|[a-zA-Z_$][a-zA-Z0-9_$]*|\s+|[^a-zA-Z_$'"`\s]+/g; + +function highlightCode(el) { + const code = el.textContent; + const tokens = code.match(tokenRegex) || []; + const counts = {}; + let html = ''; + for (let i = 0; i < tokens.length; i++) { + const t = tokens[i]; + if (/^\s+$/.test(t)) { + html += t; + continue; + } + counts[t] = (counts[t] || 0) + 1; + const dataAttr = `data-layout-id="${t}-${counts[t]}"`; + if (/^['"`]/.test(t)) { + html += `${t}`; + } + else if (/^[a-zA-Z_$]/.test(t)) { + const rest = tokens.slice(i + 1).join('').trimStart(); + const isFunction = rest.startsWith('('); + const cls = keywordSet.test(t) ? 'kw' : isFunction ? 'fn' : 'var'; + html += `${t}`; + } + else { + html += `${t}`; + } + } + el.innerHTML = html; +} + +$('code').forEach(highlightCode); + +const layout = createLayout('.container', { + loop: true, + alternate: true, + loopDelay: 500, + duration: 1000, + delay: 150, + ease: 'inOutExpo', + enterFrom: { + opacity: 0, + duration: 1250, + delay: 300, + }, + leaveTo: { + opacity: 0, + // color: 'var(--white-2)', + transform: () => `translate(${random(-50, 50)}px, ${random(-200, 200)}px) rotate(${random(-30, 30)}deg)`, + duration: 750, + delay: stagger([0, 200], { from: 'random' }), + ease: 'out(3)' + } +}); + +document.fonts.ready.then(() => layout.update(({ root }) => root.classList.toggle('show-animejs'))) diff --git a/examples/auto-layout/nav/index.html b/examples/auto-layout/nav/index.html new file mode 100644 index 000000000..5326a7374 --- /dev/null +++ b/examples/auto-layout/nav/index.html @@ -0,0 +1,128 @@ + + + + Auto layout / Nav / Anime.js + + + + + + +
+ +
+
+

The smallest planet in our solar system and closest to the Sun, Mercury is only slightly larger than Earth's Moon. With extreme temperatures ranging from -290°F to 800°F, this rocky world has a cratered surface similar to our Moon.

+
+
+

Venus is the second planet from the Sun and is similar in size to Earth. Its thick atmosphere traps heat, making it the hottest planet in our solar system. The surface of Venus is a scorching 867°F.

+
+
+

Earth is the third planet from the Sun and the only known planet to harbor life. It has a diverse environment with oceans, continents, and a protective atmosphere. The average surface temperature is about 61°F.

+
+
+

Mars, the fourth planet from the Sun, is often called the "Red Planet" due to its rusty color. It has a thin atmosphere and a cold, desert-like surface with a temperature averaging -81°F.

+
+
+

Jupiter is the largest planet in our solar system, a gas giant with a powerful atmosphere. It's known for its Great Red Spot, a giant storm. The cloud-top temperature is around -234°F.

+
+
+

Saturn, the sixth planet from the Sun, is famous for its prominent rings made of ice and rock. It's a gas giant with a cloud-top temperature of about -288°F.

+
+
+

Uranus is the seventh planet from the Sun and an ice giant. It rotates on its side, giving it unique seasons. The cloud-top temperature is around -357°F.

+
+
+

Neptune is the eighth and farthest planet from the Sun. It's another ice giant with strong winds and a deep blue color. The cloud-top temperature is about -353°F.

+
+
+
+ + + diff --git a/examples/auto-layout/nav/index.js b/examples/auto-layout/nav/index.js new file mode 100644 index 000000000..c39b938ac --- /dev/null +++ b/examples/auto-layout/nav/index.js @@ -0,0 +1,32 @@ +import { createLayout, spring, $ } from '../../../dist/modules/index.js'; + +const nav = createLayout('nav ul', { + ease: spring({ bounce: .2, duration: 350 }), +}); + +const content = createLayout('.content', { + enterFrom: { + opacity: 0, + duration: 500, + delay: 200, + ease: 'inOut(3)' + }, + leaveTo: { + opacity: 0, + duration: 500, + ease: 'out(3)' + }, +}); + +document.onclick = e => { + const $button = e.target.closest('button'); + if ($button) { + nav.update(() => { + content.record(); + $button.appendChild($('.button-bg')[0]); + $('section').forEach($section => $section.classList.remove('is-active')); + $(`section[data-color="${$button.dataset.color}"]`)[0].classList.add('is-active'); + content.animate(); + }); + } +} diff --git a/examples/auto-layout/onscroll/index.html b/examples/auto-layout/onscroll/index.html new file mode 100644 index 000000000..b0a5eced6 --- /dev/null +++ b/examples/auto-layout/onscroll/index.html @@ -0,0 +1,98 @@ + + + + Auto layout / onScroll / anime.js + + + + + + +
+ +
+
+/* WAAPI */
+
+document.querySelectorAll('.circle').forEach(($el, i) => {
+  el.animate({
+    translate: '100px',
+  }, {
+    duration: 1000,
+    delay: i * 100,
+    easing: 'ease-out',
+  }).finished.then(() => {
+    $el.style.translate = '100px';
+  })
+});
+
+
+ +
+
+/* ANIMEJS */
+
+waapi.animate('.circle', {
+  translate: '100px',
+  duration: 1000,
+  delay: stagger(100),
+  ease: 'ease-out',
+});
+
+
+ +
+ + + + diff --git a/examples/auto-layout/onscroll/index.js b/examples/auto-layout/onscroll/index.js new file mode 100644 index 000000000..fd01d54c7 --- /dev/null +++ b/examples/auto-layout/onscroll/index.js @@ -0,0 +1,65 @@ +import { createLayout, $, stagger, random, onScroll } from '../../../dist/modules/index.js'; + +const keywordSet = /^(const|let|var|function|return|if|else|for|while|new|this|true|false|null|undefined|async|await|import|export|from|class|extends)$/; +const tokenRegex = /(['"`])(?:\\.|[^\\])*?\1|[a-zA-Z_$][a-zA-Z0-9_$]*|\s+|[^a-zA-Z_$'"`\s]+/g; + +function highlightCode(el) { + const code = el.textContent; + const tokens = code.match(tokenRegex) || []; + const counts = {}; + let html = ''; + for (let i = 0; i < tokens.length; i++) { + const t = tokens[i]; + if (/^\s+$/.test(t)) { + html += t; + continue; + } + counts[t] = (counts[t] || 0) + 1; + const dataAttr = `data-layout-id="${t}-${counts[t]}"`; + if (/^['"`]/.test(t)) { + html += `${t}`; + } + else if (/^[a-zA-Z_$]/.test(t)) { + const rest = tokens.slice(i + 1).join('').trimStart(); + const isFunction = rest.startsWith('('); + const cls = keywordSet.test(t) ? 'kw' : isFunction ? 'fn' : 'var'; + html += `${t}`; + } + else { + html += `${t}`; + } + } + el.innerHTML = html; +} + +$('code').forEach(highlightCode); + +const layout = createLayout('.container', { + duration: 1000, + ease: 'inOutExpo', + enterFrom: { + opacity: 0, + duration: 1250, + delay: 300, + }, + leaveTo: { + opacity: 0, + // color: 'var(--white-2)', + transform: () => `translate(${random(-50, 50)}px, ${random(-200, 200)}px) rotate(${random(-30, 30)}deg)`, + duration: 750, + delay: stagger([0, 200], { from: 'random' }), + ease: 'out(3)' + } +}); + +layout.record() + +layout.root.classList.toggle('show-animejs') + +layout.animate({ + autoplay: onScroll({ + debug: true, + sync: true, + enter: 'top top', + }) +}) diff --git a/examples/auto-layout/periodic-table/index.html b/examples/auto-layout/periodic-table/index.html new file mode 100644 index 000000000..0d5824a30 --- /dev/null +++ b/examples/auto-layout/periodic-table/index.html @@ -0,0 +1,223 @@ + + + + Auto layout / anime.js + + + + + + +
+
+ + + + +
+
+
+
+
+
+ 3D transform calculations ported from this three.js demo. +
+ + + + diff --git a/examples/auto-layout/periodic-table/index.js b/examples/auto-layout/periodic-table/index.js new file mode 100644 index 000000000..ba38485cd --- /dev/null +++ b/examples/auto-layout/periodic-table/index.js @@ -0,0 +1,318 @@ +import { createLayout, utils, stagger, spring, createTimer, createAnimatable } from '../../../dist/modules/index.js'; + +// Elements data to populate the table + +const ELEMENT_FIELDS = { + SYMBOL: 0, + NAME: 1, + COLOR: 2, + COLUMN: 3, + ROW: 4, + ATOMIC_MASS: 5, + DENSITY: 6, + MELTING_POINT: 7, + BOILING_POINT: 8, +}; +const ELEMENT_STRIDE = 9; + +const elements = [ + // symbol, name, color, column, row, atomicMass, density, meltingPoint, boilingPoint + 'H', 'Hydrogen', 0, 1, 1, 1.008, 0.08988, -259.16, -252.88, + 'He', 'Helium', 1, 18, 1, 4.0026022, 0.1786, -272.2, -268.93, + 'Li', 'Lithium', 2, 1, 2, 6.94, 0.534, 180.5, 1329.85, + 'Be', 'Beryllium', 3, 2, 2, 9.01218315, 1.85, 1286.85, 2468.85, + 'B', 'Boron', 4, 13, 2, 10.81, 2.08, 2075.85, 3926.85, + 'C', 'Carbon', 0, 14, 2, 12.011, 1.821, null, null, + 'N', 'Nitrogen', 0, 15, 2, 14.007, 1.251, -210, -195.79, + 'O', 'Oxygen', 0, 16, 2, 15.999, 1.429, -218.79, -182.96, + 'F', 'Fluorine', 0, 17, 2, 18.9984031636, 1.696, -219.67, -188.12, + 'Ne', 'Neon', 1, 18, 2, 20.17976, 0.9002, -248.59, -246.05, + 'Na', 'Sodium', 2, 1, 3, 22.989769282, 0.968, 97.79, 882.94, + 'Mg', 'Magnesium', 3, 2, 3, 24.305, 1.738, 649.85, 1089.85, + 'Al', 'Aluminium', 5, 13, 3, 26.98153857, 2.7, 660.32, 2469.85, + 'Si', 'Silicon', 4, 14, 3, 28.085, 2.329, 1413.85, 3264.85, + 'P', 'Phosphorus', 0, 15, 3, 30.9737619985, 1.823, null, null, + 'S', 'Sulfur', 0, 16, 3, 32.06, 2.07, 115.21, 444.65, + 'Cl', 'Chlorine', 0, 17, 3, 35.45, 3.2, -101.55, -34.04, + 'Ar', 'Argon', 1, 18, 3, 39.9481, 1.784, -189.34, -185.85, + 'K', 'Potassium', 2, 1, 4, 39.09831, 0.862, 63.55, 758.85, + 'Ca', 'Calcium', 3, 2, 4, 40.0784, 1.55, 841.85, 1483.85, + 'Sc', 'Scandium', 6, 3, 4, 44.9559085, 2.985, 1540.85, 2835.85, + 'Ti', 'Titanium', 6, 4, 4, 47.8671, 4.506, 1667.85, 3286.85, + 'V', 'Vanadium', 6, 5, 4, 50.94151, 6, 1909.85, 3406.85, + 'Cr', 'Chromium', 6, 6, 4, 51.99616, 7.19, 1906.85, 2670.85, + 'Mn', 'Manganese', 6, 7, 4, 54.9380443, 7.21, 1245.85, 2060.85, + 'Fe', 'Iron', 6, 8, 4, 55.8452, 7.874, 1537.85, 2860.85, + 'Co', 'Cobalt', 6, 9, 4, 58.9331944, 8.9, 1494.85, 2926.85, + 'Ni', 'Nickel', 6, 10, 4, 58.69344, 8.908, 1454.85, 2729.85, + 'Cu', 'Copper', 6, 11, 4, 63.5463, 8.96, 1084.62, 2561.85, + 'Zn', 'Zinc', 5, 12, 4, 65.382, 7.14, 419.53, 906.85, + 'Ga', 'Gallium', 5, 13, 4, 69.7231, 5.91, 29.76, 2399.85, + 'Ge', 'Germanium', 4, 14, 4, 72.6308, 5.323, 938.25, 2832.85, + 'As', 'Arsenic', 4, 15, 4, 74.9215956, 5.727, null, null, + 'Se', 'Selenium', 0, 16, 4, 78.9718, 4.81, 220.85, 684.85, + 'Br', 'Bromine', 0, 17, 4, 79.904, 3.1028, -7.35, 58.85, + 'Kr', 'Krypton', 1, 18, 4, 83.7982, 3.749, -157.37, -153.22, + 'Rb', 'Rubidium', 2, 1, 5, 85.46783, 1.532, 39.3, 687.85, + 'Sr', 'Strontium', 3, 2, 5, 87.621, 2.64, 776.85, 1376.85, + 'Y', 'Yttrium', 6, 3, 5, 88.905842, 4.472, 1525.85, 2929.85, + 'Zr', 'Zirconium', 6, 4, 5, 91.2242, 6.52, 1854.85, 4376.85, + 'Nb', 'Niobium', 6, 5, 5, 92.906372, 8.57, 2476.85, 4743.85, + 'Mo', 'Molybdenum', 6, 6, 5, 95.951, 10.28, 2622.85, 4638.85, + 'Tc', 'Technetium', 6, 7, 5, 98, 11, 2156.85, 4264.85, + 'Ru', 'Ruthenium', 6, 8, 5, 101.072, 12.45, 2333.85, 4149.85, + 'Rh', 'Rhodium', 6, 9, 5, 102.905502, 12.41, 1963.85, 3694.85, + 'Pd', 'Palladium', 6, 10, 5, 106.421, 12.023, 1554.9, 2962.85, + 'Ag', 'Silver', 6, 11, 5, 107.86822, 10.49, 961.78, 2161.85, + 'Cd', 'Cadmium', 5, 12, 5, 112.4144, 8.65, 321.07, 766.85, + 'In', 'Indium', 5, 13, 5, 114.8181, 7.31, 156.6, 2071.85, + 'Sn', 'Tin', 5, 14, 5, 118.7107, 7.365, 231.93, 2601.85, + 'Sb', 'Antimony', 4, 15, 5, 121.7601, 6.697, 630.63, 1634.85, + 'Te', 'Tellurium', 4, 16, 5, 127.603, 6.24, 449.51, 987.85, + 'I', 'Iodine', 0, 17, 5, 126.904473, 4.933, 113.7, 184.25, + 'Xe', 'Xenon', 1, 18, 5, 131.2936, 5.894, -111.75, -108.1, + 'Cs', 'Cesium', 2, 1, 6, 132.905451966, 1.93, 28.55, 670.85, + 'Ba', 'Barium', 3, 2, 6, 137.3277, 3.51, 726.85, 1844.85, + 'Hf', 'Hafnium', 6, 4, 6, 178.492, 13.31, 2232.85, 4602.85, + 'Ta', 'Tantalum', 6, 5, 6, 180.947882, 16.69, 3016.85, 5457.85, + 'W', 'Tungsten', 6, 6, 6, 183.841, 19.25, 3421.85, 5929.85, + 'Re', 'Rhenium', 6, 7, 6, 186.2071, 21.02, 3185.85, 5595.85, + 'Os', 'Osmium', 6, 8, 6, 190.233, 22.59, 3032.85, 5011.85, + 'Ir', 'Iridium', 6, 9, 6, 192.2173, 22.56, 2445.85, 4129.85, + 'Pt', 'Platinum', 6, 10, 6, 195.0849, 21.45, 1768.25, 3824.85, + 'Au', 'Gold', 6, 11, 6, 196.9665695, 19.3, 1064.18, 2969.85, + 'Hg', 'Mercury', 5, 12, 6, 200.5923, 13.534, -38.83, 356.73, + 'Tl', 'Thallium', 5, 13, 6, 204.38, 11.85, 303.85, 1472.85, + 'Pb', 'Lead', 5, 14, 6, 207.21, 11.34, 327.46, 1748.85, + 'Bi', 'Bismuth', 5, 15, 6, 208.980401, 9.78, 271.55, 1563.85, + 'Po', 'Polonium', 4, 16, 6, 209, 9.196, 253.85, 961.85, + 'At', 'Astatine', 0, 17, 6, 210, 6.35, 301.85, 336.85, + 'Rn', 'Radon', 1, 18, 6, 222, 9.73, -71.15, -61.65, + 'Fr', 'Francium', 2, 1, 7, 223, 1.87, 26.85, 676.85, + 'Ra', 'Radium', 3, 2, 7, 226, 5.5, 959.85, 1736.85, + 'Rf', 'Rutherfordium', 6, 4, 7, 267, 23.2, 2126.85, 5526.85, + 'Db', 'Dubnium', 6, 5, 7, 268, 29.3, null, null, + 'Sg', 'Seaborgium', 6, 6, 7, 269, 35, null, null, + 'Bh', 'Bohrium', 6, 7, 7, 270, 37.1, null, null, + 'Hs', 'Hassium', 6, 8, 7, 269, 40.7, -147.15, null, + 'Mt', 'Meitnerium', 9, 9, 7, 278, 37.4, null, null, + 'Ds', 'Darmstadtium', 9, 10, 7, 281, 34.8, null, null, + 'Rg', 'Roentgenium', 9, 11, 7, 282, 28.7, null, null, + 'Cn', 'Copernicium', 9, 12, 7, 285, 14, null, 3296.85, + 'Nh', 'Nihonium', 9, 13, 7, 286, 16, 426.85, 1156.85, + 'Fl', 'Flerovium', 9, 14, 7, 289, 14, 66.85, 146.85, + 'Mc', 'Moscovium', 9, 15, 7, 289, 13.5, 396.85, 1126.85, + 'Lv', 'Livermorium', 9, 16, 7, 293, 12.9, 435.85, 811.85, + 'Ts', 'Tennessine', 9, 17, 7, 294, 7.17, 449.85, 609.85, + 'Og', 'Oganesson', 9, 18, 7, 294, 4.95, null, 76.85, + 'La', 'Lanthanum', 7, 3, 8, 138.905477, 6.162, 919.85, 3463.85, + 'Ce', 'Cerium', 7, 4, 8, 140.1161, 6.77, 794.85, 3442.85, + 'Pr', 'Praseodymium', 7, 5, 8, 140.907662, 6.77, 934.85, 3129.85, + 'Nd', 'Neodymium', 7, 6, 8, 144.2423, 7.01, 1023.85, 3073.85, + 'Pm', 'Promethium', 7, 7, 8, 145, 7.26, 1041.85, 2999.85, + 'Sm', 'Samarium', 7, 8, 8, 150.362, 7.52, 1071.85, 1899.85, + 'Eu', 'Europium', 7, 9, 8, 151.9641, 5.264, 825.85, 1528.85, + 'Gd', 'Gadolinium', 7, 10, 8, 157.253, 7.9, 1311.85, 2999.85, + 'Tb', 'Terbium', 7, 11, 8, 158.925352, 8.23, 1355.85, 3122.85, + 'Dy', 'Dysprosium', 7, 12, 8, 162.5001, 8.54, 1406.85, 2566.85, + 'Ho', 'Holmium', 7, 13, 8, 164.930332, 8.79, 1460.85, 2599.85, + 'Er', 'Erbium', 7, 14, 8, 167.2593, 9.066, 1528.85, 2867.85, + 'Tm', 'Thulium', 7, 15, 8, 168.934222, 9.32, 1544.85, 1949.85, + 'Yb', 'Ytterbium', 7, 16, 8, 173.0451, 6.9, 823.85, 1195.85, + 'Lu', 'Lutetium', 7, 17, 8, 174.96681, 9.841, 1651.85, 3401.85, + 'Ac', 'Actinium', 8, 3, 9, 227, 10, 1226.85, 3226.85, + 'Th', 'Thorium', 8, 4, 9, 232.03774, 11.724, 1749.85, 4787.85, + 'Pa', 'Protactinium', 8, 5, 9, 231.035882, 15.37, 1567.85, 4026.85, + 'U', 'Uranium', 8, 6, 9, 238.028913, 19.1, 1132.15, 4130.85, + 'Np', 'Neptunium', 8, 7, 9, 237, 20.45, 638.85, 4173.85, + 'Pu', 'Plutonium', 8, 8, 9, 244, 19.816, 639.35, 3231.85, + 'Am', 'Americium', 8, 9, 9, 243, 12, 1175.85, 2606.85, + 'Cm', 'Curium', 8, 10, 9, 247, 13.51, 1339.85, 3109.85, + 'Bk', 'Berkelium', 8, 11, 9, 247, 14.78, 985.85, 2626.85, + 'Cf', 'Californium', 8, 12, 9, 251, 15.1, 899.85, 1469.85, + 'Es', 'Einsteinium', 8, 13, 9, 252, 8.84, 859.85, 995.85, + 'Fm', 'Fermium', 8, 14, 9, 257, null, 1526.85, null, + 'Md', 'Mendelevium', 8, 15, 9, 258, null, 826.85, null, + 'No', 'Nobelium', 8, 16, 9, 259, null, 826.85, null, + 'Lr', 'Lawrencium', 8, 17, 9, 266, null, 1626.85, null, +] + +// Genetate the table html + +const [ $sceneContent ] = utils.$('#scene-content'); +const [ $template ] = utils.$('#element'); + +for (let i = 0, l = elements.length / ELEMENT_STRIDE; i < l; i += 1) { + const offset = i * ELEMENT_STRIDE; + const $el = $template.content.cloneNode(true); + const $element = $el.querySelector('.element'); + const $number = $element.querySelector('.element-number'); + const $symbol = $element.querySelector('.element-symbol'); + const $title = $element.querySelector('.element-title'); + const $description = $element.querySelector('.element-description'); + $number.textContent = i + 1; + $symbol.textContent = elements[offset + ELEMENT_FIELDS.SYMBOL]; + $title.textContent = elements[offset + ELEMENT_FIELDS.NAME]; + $element.dataset.color = elements[offset + ELEMENT_FIELDS.COLOR]; + $element.style.gridColumn = elements[offset + ELEMENT_FIELDS.COLUMN]; + $element.style.gridRow = elements[offset + ELEMENT_FIELDS.ROW]; + $description.innerHTML = [ + `Atomic mass: ${elements[offset + ELEMENT_FIELDS.ATOMIC_MASS]} u`, + `Density: ${elements[offset + ELEMENT_FIELDS.DENSITY]} g/cm3`, + `Melting: ${elements[offset + ELEMENT_FIELDS.MELTING_POINT]} °C`, + `Boiling: ${elements[offset + ELEMENT_FIELDS.BOILING_POINT]} °C`, + ].join('
'); + $sceneContent.appendChild($el); +} + +const cards = utils.$('#scene-content .element'); + +// Create the cards layout and register the font-size as an extra property for animation +const elementsLayout = createLayout($sceneContent, { + properties: ['font-size'], + duration: 2000, + ease: 'inOutExpo', +}); + +// Move the scene relative to the cursor position +const sceneAnimatable = createAnimatable('#scene', { rotateX: 200, rotateY: 200 }); +const pointer = { x: 0, y: 0, rotateX: 15, rotateY: 20, rx: 0, ry: 0 }; + +createTimer({ + onUpdate: () => { + pointer.rx = utils.lerp(pointer.rx, pointer.rotateX, .01); + pointer.ry = utils.lerp(pointer.ry, pointer.rotateY, .01); + sceneAnimatable.rotateX(pointer.y * pointer.rx); + sceneAnimatable.rotateY(pointer.x * pointer.ry); + } +}); + +// The different layout tranform +const transformLayout = { + table: () => { + // The table view use CSS grid and has no special tranforms except for a selected element + pointer.rotateX = 15; + pointer.rotateY = 20; + cards.forEach($el => { + $el.style.opacity = 1; + $el.style.transform = $el.classList.contains('is-expanded') ? 'translateZ(50px)' : 'translateZ(10px)'; + }); + }, + sphere: () => { + const radius = 300; + pointer.rotateX = 40; + pointer.rotateY = 360; + cards.forEach(($el, i) => { + const offsetZ = $el.classList.contains('is-expanded') ? 20 : 0; + const phi = Math.acos(-1 + (2 * i) / cards.length); + const theta = Math.sqrt(cards.length * Math.PI) * phi; + const sinPhi = Math.sin(phi); + const x = radius * sinPhi * Math.cos(theta); + const y = radius * Math.cos(phi); + const z = radius * sinPhi * Math.sin(theta); + const yaw = Math.atan2(x, z); + const pitch = -Math.atan2(y, Math.hypot(x, z)); + $el.style.transform = `translate3d(${x}px, ${y}px, ${z}px) rotateY(${yaw}rad) rotateX(${pitch}rad) translateZ(${offsetZ}px)`; + }); + }, + helix: () => { + pointer.rotateX = 30; + pointer.rotateY = 300; + const radius = 400; + const thetaStep = 0.16; + const verticalSpacing = 3; + const yOffset = (cards.length * verticalSpacing) / 2; + cards.forEach(($el, i) => { + const offsetZ = $el.classList.contains('is-expanded') ? 20 : 0; + const theta = i * thetaStep + Math.PI; + const y = -(i * verticalSpacing) + yOffset; + const x = radius * Math.sin(theta); + const z = radius * Math.cos(theta); + const yaw = Math.atan2(x, z); + const pitch = -Math.atan2(y, Math.hypot(x, z) * 2); + $el.style.transform = `translate3d(${x}px, ${y}px, ${z}px) rotateY(${yaw}rad) rotateX(${pitch}rad) translateZ(${offsetZ}px)`; + }); + }, + grid: () => { + pointer.rotateX = 10; + pointer.rotateY = 60; + const cols = 5; + const rows = 5; + const colGap = 150; + const rowGap = 100; + const depthGap = 150; + const perLayer = cols * rows; + const layers = Math.ceil(cards.length / perLayer); + cards.forEach(($el, i) => { + const offsetZ = $el.classList.contains('is-expanded') ? 20 : 0; + const col = i % cols; + const row = Math.floor(i / cols) % rows; + const layer = Math.floor(i / perLayer); + const x = (col - (cols - 1) / 2) * colGap; + const y = ((rows - 1) / 2 - row) * rowGap; + const z = offsetZ + ((layer - (layers - 1) / 2) * depthGap); + $el.style.transform = `translate3d(${x}px, ${y}px, ${z}px)`; + }); + }, + random: () => { + // The table view use CSS grid and has no special tranforms except for a selected element + pointer.rotateX = 15; + pointer.rotateY = 20; + utils.set(cards, { x: () => utils.random(-500, 500), y: () => utils.random(-500, 500), z: () => utils.random(-500, 500)}) + }, +}; + +document.addEventListener('pointermove', event => { + const hw = window.innerWidth * .5; + const hh = window.innerHeight * .5; + pointer.x = utils.mapRange(event.clientX - hw, -hw, hw, 1, -1); + pointer.y = utils.mapRange(event.clientY - hh, -hh, hh, -1, 1); +}); + +const toggles = utils.$('.controls button.toggle'); + +document.addEventListener('click', event => { + const $toggle = event.target.closest('.controls button.toggle'); + if ($toggle) { + toggles.forEach(button => button.classList.remove('is-active')); + $toggle.classList.add('is-active'); + const layoutType = $toggle.id; + elementsLayout.update(() => { + cards.forEach($el => $el.classList.remove('is-expanded')); + $sceneContent.dataset.layout = layoutType; + transformLayout[layoutType](); + }, { + delay: stagger([0, 750], { from: 'random' }) + }); + return; + } + const $card = event.target.closest('#scene-content .element'); + const shouldExpand = $card && !$card.classList.contains('is-expanded'); + elementsLayout.update(() => { + cards.forEach($el => $el.classList.remove('is-expanded')); + if (shouldExpand) $card.classList.add('is-expanded'); + transformLayout[$sceneContent.dataset.layout](); + },{ + ease: spring({ bounce: .2, duration: 350 }), + }); +}); + +document.addEventListener('keydown', event => { + if (event.key !== 'Escape') return; + const hasExpandedCard = cards.some($el => $el.classList.contains('is-expanded')); + if (!hasExpandedCard) return; + elementsLayout.update(() => { + cards.forEach($el => $el.classList.remove('is-expanded')); + transformLayout[$sceneContent.dataset.layout](); + },{ + ease: spring({ bounce: .3, duration: 350 }), + }); +}); + +// Intro animation + +transformLayout.random(); +utils.set(cards, { opacity: 0 }); +elementsLayout.update(() => transformLayout.table(), { + delay: stagger([0, 750], { from: 'random' }) +}); \ No newline at end of file diff --git a/examples/auto-layout/planets/index.html b/examples/auto-layout/planets/index.html new file mode 100644 index 000000000..2d4b1e4b3 --- /dev/null +++ b/examples/auto-layout/planets/index.html @@ -0,0 +1,376 @@ + + + + Auto layout / Planets / Anime.js + + + + + + +
+
+ + + + + +
+
+ + + +
+
+ +
+
+ + + + diff --git a/examples/auto-layout/planets/index.js b/examples/auto-layout/planets/index.js new file mode 100644 index 000000000..aa379dc33 --- /dev/null +++ b/examples/auto-layout/planets/index.js @@ -0,0 +1,200 @@ +import { createLayout, utils, stagger } from '../../../dist/modules/index.js'; + +const planetTypes = [ + "Rocky planet", + "Gas giant", + "Ice giant" +]; + +const planets = [ + 60, "Mercury", "Smallest planet and closest to the Sun.", 0, 3, 3, 1, [], + 90, "Venus", "Slow-spinning world with runaway greenhouse heat.", 0, 4, 3, 2, [], + 100, "Earth", "It's apparently pretty nice over there.", 0, 5, 3, 6, [], + 80, "Mars", "Cold desert planet with canyons and polar ice.", 0, 6, 3, 0, [], + 250, "Jupiter", "Gigantic gas giant with a powerful magnetic field.", 1, 8, 3, 3, [], + 150, "Saturn", "Ringed gas giant with dozens of icy moons.", 1, 9, 3, 4, [180, 200, 220, 240], + 120, "Uranus", "An ice giant tipped almost completely on its side.", 2, 10, 3, 8, [150], + 110, "Neptune", "Distant ice giant with supersonic winds.", 2, 11, 3, 10, [], +]; + +const latin = [ + "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", + "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", + "dolore", "magna", "aliqua", "enim", "minim", "veniam", "quis", "nostrud", + "exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", + "commodo", "consequat", "duis", "aute", "irure", "dolor", "in", "reprehenderit", "voluptate" +] + +function loremIpsum(words = 100) { + let text = []; + for (let i = 0; i < words; i++) { + text.push(latin[Math.floor(Math.random() * latin.length)]); + } + let sentence = text.join(" "); + return sentence.charAt(0).toUpperCase() + sentence.slice(1) + "."; +} + +let nextPlanetIndex = 0; +const initialPlanets = 4; +const toggles = utils.$('.controls button.toggle'); +const [ $root ] = utils.$('#root'); +const [ $overlay ] = utils.$('#overlay'); + +const actions = { + add: () => { + const $visibleCards = utils.$('#root .card:not(.is-removed)'); + const visibleCardsLength = $visibleCards.length; + if (visibleCardsLength) utils.set($visibleCards, { '--total': visibleCardsLength + 1 }); + nextPlanetIndex = visibleCardsLength * 8; + if (nextPlanetIndex >= planets.length) return; + const template = utils.$('#card')[0]; + const $removed = utils.$('#root .card.is-removed')[0]; + let $cardTarget = $removed; + if (!$cardTarget) { + const clone = template.content.cloneNode(true); + const $card = clone.querySelector('.card'); + if (!$card) return; + const $image = $card.querySelector('.card-image'); + const $title = $card.querySelector('.card-title'); + const $type = $card.querySelector('.card-type'); + const $intro = $card.querySelector('.card-intro-description'); + const $description = $card.querySelector('.card-more-info'); + $image.innerHTML += ` + + `; + $image.innerHTML += ` + + ${planets[nextPlanetIndex + 7].map(r => ``).join('')} + + `; + utils.set($card, { '--index': visibleCardsLength + 1 }); + $card.dataset.title = $title.textContent = planets[nextPlanetIndex + 1]; + $type.textContent = planetTypes[planets[nextPlanetIndex + 3]]; + $intro.textContent = planets[nextPlanetIndex + 2]; + $card.dataset.color = planets[nextPlanetIndex + 6]; + $description.textContent = loremIpsum(utils.random(100, 150)); + $cardTarget = $card; + $root.appendChild(clone); + } + $cardTarget.classList.remove('is-removed'); + }, + remove: () => { + const $lastCard = utils.$('#root .card:not(.is-removed)').pop(); + if ($lastCard) $lastCard.classList.add('is-removed'); + }, + shuffle: () => { + utils.shuffle(utils.$('.card:not(.is-removed)')).forEach($el => $el.parentElement.appendChild($el)); + } +} + +for (let i = 0; i < initialPlanets; i++) { + actions.add(); +} + +const cardsLayout = createLayout($root, { + properties: ['font-size'], + enterFrom: { + opacity: $el => $el.classList.contains('card') ? `1` : '0', + transform: $el => $el.classList.contains('card') ? `translateY(150%) scale(.5)` : 'none', + }, + leaveTo: { + opacity: 0, + transform: ($el) => { + const i = [...$el.parentElement.children].indexOf($el); + const d = i % 2 ? -1 : 1; + return $el.classList.contains('card') ? `translate(${50 * d}%, 50%) rotate(${20 * d}deg) scale(.5)` : 'none'; + }, + }, +}); + +const modalLayout = createLayout($overlay, { + children: ['.card', '.card-image', '.card-image-grid', '.card-title', '.card-type', '.close-overlay'], + properties: ['--overlay-alpha'], + duration: 500, +}); + +const closeModal = () => { + let $card; + modalLayout.update(() => { + $overlay.close(); + $card = utils.$('#root .card.is-open')[0]; + $card.classList.remove('is-open'); + $card.focus({ preventScroll: true }); + }).then(() => { + $card.focus({ preventScroll: true }); + }) +}; + +const handleClick = e => { + const $target = e.target; + const $card = $target.closest('#root .card'); + if ($card) { + e.preventDefault(); + const $clone = document.createElement('div'); + $clone.innerHTML = $card.cloneNode(true).innerHTML; + $clone.classList = $card.classList; + $clone.dataset.layoutId = $card.dataset.layoutId; + $clone.dataset.color = $card.dataset.color; + $overlay.innerHTML = ''; + const cloneChildren = $clone.querySelectorAll('*'); + cloneChildren.forEach($el => $el.removeAttribute('style')); + $overlay.appendChild($clone); + modalLayout.update(() => { + $overlay.showModal(); + $clone.querySelector('button').disabled = false; + $card.classList.add('is-open'); + }); + } + if ($target.closest('#overlay .close-overlay') || $target.id === 'overlay') { + closeModal(); + } + const $button = $target.closest('.controls button'); + if ($button) { + const isToggle = $button.classList.contains('toggle'); + const isAction = $button.classList.contains('action'); + const buttonId = $button.id; + cardsLayout.update(() => { + if (isToggle) { + toggles.forEach(toggle => toggle.classList.remove('is-active')); + $button.classList.add('is-active'); + } else if (isAction) { + actions[buttonId](); + } + const cards = utils.$('#root .card:not(.is-removed)'); + const layoutType = utils.$('button.is-active')[0].id; + $root.dataset.layout = layoutType; + if (cards.length) { + if (layoutType === 'stack') { + utils.set(cards, { + x: 0, + y: stagger(10, { reversed: true}), + z: stagger(-20, { reversed: true}), + rotateX: 0, + rotateY: 0, + rotateZ: 0, + scale: 1, + }); + } else if (layoutType === 'chaos') { + utils.set(cards, { + x: () => utils.random(-10, 10) + 'vw', + y: () => utils.random(-10, 10) + 'vh', + z: () => utils.random(600, 750), + rotateX: () => utils.random(-45, 45), + rotateY: () => utils.random(-45, 45), + rotateZ: () => utils.random(-45, 45), + scale: () => utils.random(.2, .3, 2), + }); + } else { + utils.set(cards, { transform: 'none' }); + } + } + }, { + duration: isAction ? 350 : 450, + ease: isAction ? 'out(3)' : 'inOut(3)', + delay: isAction ? 0 : stagger([0, 350]) + }); + } +} + +document.addEventListener('click', handleClick); +$overlay.addEventListener('cancel', closeModal); diff --git a/examples/auto-layout/todo-list/index.html b/examples/auto-layout/todo-list/index.html new file mode 100644 index 000000000..b96edbb06 --- /dev/null +++ b/examples/auto-layout/todo-list/index.html @@ -0,0 +1,190 @@ + + + + Auto layout / Todo list / Anime.js + + + + + + +
+
+
+
    +
    +
    + + + +
    +
    +
    +
    +

    Pending

    +
      +
      +
      +

      Completed

      +
        +
        +
        +
        + + + + diff --git a/examples/auto-layout/todo-list/index.js b/examples/auto-layout/todo-list/index.js new file mode 100644 index 000000000..288b04b99 --- /dev/null +++ b/examples/auto-layout/todo-list/index.js @@ -0,0 +1,71 @@ +import { createLayout, spring, stagger, $ } from '../../../dist/modules/index.js'; + +const layout = createLayout($container, { + properties: ['backgroundColor', 'color', 'accent-color'], + ease: spring({ bounce: .3, duration: 450 }), + leaveTo: { + opacity: 0, + transform: 'translateY(.5rem) scale(.9)', + } +}); + +const createItem = (text) => { + const $item = /** @type {HTMLLIElement} */($todoTemplate.content.firstElementChild.cloneNode(true)); + const $checkbox = $item.querySelector('input[type="checkbox"]'); + const $labelText = $item.querySelector('.text'); + $labelText.textContent = text; + $checkbox.addEventListener('change', handleToggle); + return $item; +} + +const handleToggle = event => { + const $checkbox = /** @type {HTMLInputElement} */(event.currentTarget); + const $item = $checkbox.closest('.item'); + if (!$item) return; + const $targetList = $checkbox.checked ? $completed : $pending; + $('.list').forEach($el => $el.classList.toggle('is-active', $el === $targetList)); + $('.item').forEach($el => $el.classList.toggle('is-floating', $el === $item)); + layout.update(() => $targetList.insertBefore($item, $targetList.firstElementChild), { ease: 'inOutExpo' }); +} + +const addItem = () => { + const value = $addInput.value.trim(); + if (!value) return; + const $item = createItem(value); + // debugger; + $new.appendChild($item); + layout.update(() => $pending.insertBefore($item, $pending.firstElementChild)); + $addInput.value = ''; +} + +const handleAction = event => { + const $button = /** @type {HTMLButtonElement} */(event.target); + const $item = $button.closest('.item'); + if (!$item) return; + const $parent = $item.parentElement; + if ($parent.id === '$createForm') { + addItem(); + } else { + layout.update(() => { + $item.classList.add('is-removed'); + }, { + ease: 'out(3.5)', + onComplete: () => { + if ($parent.children.length > 1) { // If there are more than one element left remove the element directly + $item.remove(); + } else { // Otherwise wrap the removal into a layout.update() to avoid abrupt list resizing + layout.update(() => $item.remove()) + } + } + }); + } +} + +$createForm.addEventListener('submit', event => { + event.preventDefault(); + addItem(); +}); + +document.addEventListener('click', event => { + if (event.target.classList.contains('action')) handleAction(event); +}); \ No newline at end of file diff --git a/examples/draggable-playground/index.html b/examples/draggable-playground/index.html index 76e22b937..b8cc09b5a 100644 --- a/examples/draggable-playground/index.html +++ b/examples/draggable-playground/index.html @@ -21,7 +21,7 @@ top: 0; width: 100%; height: 100%; - background: linear-gradient(-90deg, rgba(255,255,255,.05) 1px, transparent 1px), linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px), linear-gradient(-90deg, rgba(255,255,255,.04) 1px, transparent 1px), linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px), linear-gradient(transparent 3px, var(--black) 3px, var(--black) calc(var(--one-cell) - 2px), transparent calc(var(--one-cell) - 2px)), linear-gradient(-90deg, var(--white) 1px, transparent 1px), linear-gradient(-90deg, transparent 3px, var(--black) 3px, var(--black) calc(var(--one-cell) - 2px), transparent calc(var(--one-cell) - 2px)), linear-gradient(var(--white) 1px, transparent 1px), var(--black); + background: linear-gradient(-90deg, rgba(255,255,255,.05) 1px, transparent 1px), linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px), linear-gradient(-90deg, rgba(255,255,255,.04) 1px, transparent 1px), linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px), linear-gradient(transparent 3px, var(--black-1) 3px, var(--black-1) calc(var(--one-cell) - 2px), transparent calc(var(--one-cell) - 2px)), linear-gradient(-90deg, var(--white-1) 1px, transparent 1px), linear-gradient(-90deg, transparent 3px, var(--black-1) 3px, var(--black-1) calc(var(--one-cell) - 2px), transparent calc(var(--one-cell) - 2px)), linear-gradient(var(--white-1) 1px, transparent 1px), var(--black-1); background-size: calc(var(--one-cell) / 10) calc(var(--one-cell) / 10), calc(var(--one-cell) / 10) calc(var(--one-cell) / 10), var(--one-cell) var(--one-cell), var(--one-cell) var(--one-cell), var(--one-cell) var(--one-cell), var(--one-cell) var(--one-cell), var(--one-cell) var(--one-cell), var(--one-cell) var(--one-cell); background-position: calc(var(--one-cell) * .25) -1px; } @@ -97,27 +97,27 @@ margin-top: -10px; background-size: 5px 5px; background-image: - radial-gradient(circle at center, var(--black) 1px, transparent 0), - radial-gradient(circle at center, var(--black) 1px, transparent 0); + radial-gradient(circle at center, var(--black-1) 1px, transparent 0), + radial-gradient(circle at center, var(--black-1) 1px, transparent 0); } .carousel .draggable { padding-top: 1rem; padding-left: 1rem; - color: var(--black); + color: var(--black-1); } .rectangle { width: calc(var(--one-cell) * 2); height: calc(var(--one-cell) * 1); - background-color: var(--red); + background-color: var(--red-1); border-radius: 20px; } .square { width: calc(var(--one-cell) * .8); height: calc(var(--one-cell) * .8); - background-color: var(--red); + background-color: var(--red-1); border-radius: 20px; } @@ -125,7 +125,7 @@ overflow: hidden; width: calc(var(--one-cell) * .1); height: calc(var(--one-cell) * .8); - background-color: var(--red); + background-color: var(--red-1); border-radius: 20px; } @@ -170,7 +170,7 @@ } .margin .draggable { - background-color: var(--red); + background-color: var(--red-1); border-radius: 10px; } @@ -306,7 +306,7 @@ .carousel-item:nth-child(even), .list-item:nth-child(even) { - background-color: var(--yellow); + background-color: var(--yellow-1); } .carousel-link { @@ -316,7 +316,7 @@ left: 20px; right: 20px; bottom: 20px; - border: 2px dotted var(--red); + border: 2px dotted var(--red-1); } #snap-carousel { @@ -372,7 +372,7 @@ width: min(25%, 105px); height: calc((var(--one-cell) * 1)); border-radius: 10px; - background: var(--yellow); + background: var(--yellow-1); backface-visibility: hidden; } @@ -383,9 +383,9 @@ .form { top: 62px; left: 60px; - background: var(--black); + background: var(--black-1); padding: 20px; - box-shadow: 0 0 0 1px var(--red); + box-shadow: 0 0 0 1px var(--red-1); } .trigger { @@ -394,7 +394,7 @@ height: calc(var(--one-cell) * 3.2); margin-left: calc(var(--one-cell) * -1.2); margin-top: calc(var(--one-cell) * -1.2); - background: var(--yellow); + background: var(--yellow-1); } #onsnap-callback { @@ -425,9 +425,9 @@ width: 100%; border-top-left-radius: .5rem; border-top-right-radius: .5rem; - background-color: var(--red); + background-color: var(--red-1); will-change: transform; - box-shadow: 0 500px 0 0 var(--red); + box-shadow: 0 500px 0 0 var(--red-1); } .drawer-content { @@ -444,7 +444,7 @@ z-index: 1; width: 100%; height: 3rem; - background-color: var(--red); + background-color: var(--red-1); border-top-left-radius: .5rem; border-top-right-radius: .5rem; } @@ -471,7 +471,7 @@ position: absolute; top: 1rem; right: 1rem; - background: var(--yellow); + background: var(--yellow-1); margin-bottom: 10px; border-radius: .5rem; } diff --git a/examples/draggable-playground/index.js b/examples/draggable-playground/index.js index e4709ebb9..43a74e077 100644 --- a/examples/draggable-playground/index.js +++ b/examples/draggable-playground/index.js @@ -18,8 +18,14 @@ const [ $log ] = utils.$('#log'); createDraggable('#fixed', { container: document.body, + releaseStiffness: 200, + releaseDamping: 8, + velocityMultiplier: 4, onDrag: self => { $log.innerHTML = `${utils.round(self.velocity, 3)}`; + }, + onUpdate: self => { + console.log(self.x); } }); @@ -205,7 +211,7 @@ const flickDraggable = createDraggable(flickData, { y: false, onGrab: () => animate(flickData, { speedX: 0, duration: 500 }), onRelease: () => animate(flickData, { speedX: 2, duration: 500 }), - releaseDamping: 10, + releaseStiffness: 10, }); createTimer({ diff --git a/examples/easings-visualizer/index.html b/examples/easings-visualizer/index.html index f9305c366..5db0dc61a 100644 --- a/examples/easings-visualizer/index.html +++ b/examples/easings-visualizer/index.html @@ -101,10 +101,10 @@ linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px), linear-gradient(-90deg, rgba(255,255,255,.04) 1px, transparent 1px), linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px), - linear-gradient(transparent 3px, var(--black) 3px, var(--black) calc(var(--one-cell) - 2px), transparent calc(var(--one-cell) - 2px)), - linear-gradient(-90deg, var(--white) 1px, transparent 1px), - linear-gradient(-90deg, transparent 3px, var(--black) 3px, var(--black) calc(var(--one-cell) - 2px), transparent calc(var(--one-cell) - 2px)), - linear-gradient(var(--white) 1px, transparent 1px), var(--black); + linear-gradient(transparent 3px, var(--black-1) 3px, var(--black-1) calc(var(--one-cell) - 2px), transparent calc(var(--one-cell) - 2px)), + linear-gradient(-90deg, var(--white-1) 1px, transparent 1px), + linear-gradient(-90deg, transparent 3px, var(--black-1) 3px, var(--black-1) calc(var(--one-cell) - 2px), transparent calc(var(--one-cell) - 2px)), + linear-gradient(var(--white-1) 1px, transparent 1px), var(--black-1); background-size: calc(var(--one-cell) / 10) calc(var(--one-cell) / 10), calc(var(--one-cell) / 10) calc(var(--one-cell) / 10), @@ -165,7 +165,7 @@ .axis-x:first-child:after { background-color: var(--red); - box-shadow: 0 0 0 1px var(--black); + box-shadow: 0 0 0 1px var(--black-1); } .axis-x:not(:first-child):after { @@ -188,8 +188,8 @@ align-items: center; font-size: .75rem; font-weight: bold; - color: var(--white); - background-color: var(--black); + color: var(--white-1); + background-color: var(--black-1); border: 1px solid rgba(255, 255, 255, .25); } @@ -202,7 +202,7 @@ width: 100%; min-height: 100dvh; margin-top: 70vw; - background: var(--black); + background: var(--black-1); } @media (min-width: 640px) { @@ -222,8 +222,8 @@ width: 100%; padding: .75rem; padding-bottom: 0; - background: var(--black); - box-shadow: 0 10px 10px 0 var(--black); + background: var(--black-1); + box-shadow: 0 10px 10px 0 var(--black-1); border-radius: 1rem 1rem 0 0; } @@ -281,7 +281,7 @@ display: block; width: 2.75rem; background-color: transparent; - color: var(--white); + color: var(--white-1); } .ease-parameter input[type="range"] { width: calc(100% - 2.75rem); @@ -322,7 +322,7 @@ border: 1px solid currentColor; width: 16px; height: 29px; - background-color: var(--black); + background-color: var(--black-1); background-image: radial-gradient(transparent 1px, rgba(0, 0, 0, .7) 0), radial-gradient(transparent 1px, currentColor 0), radial-gradient(currentColor 1px, transparent 0); background-size: 3px 3px, 3px 3px, 3px 3px; background-position: -2px -2px, -2px -2px, -2px -2px; @@ -340,7 +340,7 @@ width: 100%; min-height: 100%; padding: .75rem; - background-color: var(--black); + background-color: var(--black-1); } @media (min-width: 1280px) { @@ -361,7 +361,7 @@ margin: .125rem; padding: .5rem; background-color: rgba(255, 255, 255, .05); - color: var(--white); + color: var(--white-1); cursor: pointer; border-radius: .6rem; transition: background-color .25s ease-in-out; @@ -381,7 +381,7 @@ button.ease-button.is-active { background: var(--red); - color: var(--black); + color: var(--black-1); } button.ease-button .ease-config { @@ -411,7 +411,7 @@ } button.ease-button.is-active svg polyline { - stroke: var(--black); + stroke: var(--black-1); } diff --git a/examples/onscroll-responsive-scope/index.html b/examples/onscroll-responsive-scope/index.html index cb3b60da6..ad6639b55 100644 --- a/examples/onscroll-responsive-scope/index.html +++ b/examples/onscroll-responsive-scope/index.html @@ -92,14 +92,14 @@ z-index: 0; background-image: url(card-back.svg); transform: rotateY(180deg); - background-color: var(--white); + background-color: var(--white-1); } .front { z-index: 1; backface-visibility: hidden; background-image: url(card.svg); - background-color: var(--white); + background-color: var(--white-1); background-size: 89%; background-repeat: no-repeat; } diff --git a/examples/onscroll-sticky/index.html b/examples/onscroll-sticky/index.html index e8b364b54..275c66435 100644 --- a/examples/onscroll-sticky/index.html +++ b/examples/onscroll-sticky/index.html @@ -91,14 +91,14 @@ z-index: 0; background-image: url(card-back.svg); transform: rotateY(180deg); - background-color: var(--white); + background-color: var(--white-1); } .front { z-index: 1; backface-visibility: hidden; background-image: url(card.svg); - background-color: var(--white); + background-color: var(--white-1); background-size: 89%; background-repeat: no-repeat; } diff --git a/examples/stagger/index.html b/examples/stagger/index.html new file mode 100644 index 000000000..5b87b429a --- /dev/null +++ b/examples/stagger/index.html @@ -0,0 +1,28 @@ + + + + Stagger Grid / Anime.js + + + + + + + + + + + diff --git a/examples/stagger/index.js b/examples/stagger/index.js new file mode 100644 index 000000000..a7adf4eb9 --- /dev/null +++ b/examples/stagger/index.js @@ -0,0 +1,33 @@ +import { createTimeline, stagger, utils } from '../../dist/modules/index.js'; + +const totalColors = 14; +const totalDots = 1000; +const w = window.innerWidth; +const h = window.innerHeight; + +for (let i = 0; i < totalDots; i++) { + const el = document.createElement('div'); + el.classList.add('dot'); + el.dataset.color = String(utils.random(0, totalColors - 1)); + document.body.appendChild(el); +} + +const dots = document.querySelectorAll('.dot'); + +utils.set(dots, { + x: () => utils.random(0, w - 16), + y: () => utils.random(0, h - 16), + rotate: () => utils.random(-180, 180), + scale: () => utils.random(.2, 2, 3), +}); + +createTimeline({ composition: false }) + .add(dots, { + scale: [{ from: '-=1', to: '+=2' }], + rotate: [{ from: '-=180', to: '+=180' }], + background: [{ from: '#FFF' }], + duration: 1000, + ease: 'inOut(3)', + loop: true, + }, stagger([0, 2000], { grid: true, from: 'center', axis: 'x' })) + .init(); \ No newline at end of file diff --git a/examples/svg-line-drawing/index.js b/examples/svg-line-drawing/index.js index 48e4e8996..56b811e1d 100644 --- a/examples/svg-line-drawing/index.js +++ b/examples/svg-line-drawing/index.js @@ -51,7 +51,7 @@ document.body.innerHTML += svgLines; document.body.innerHTML += svgCircles; createTimeline({ - playbackEase: 'out(4)', + // playbackEase: 'out(4)', loop: 0, defaults: { ease: 'inOut(4)', diff --git a/examples/text/scramble-tl/index.html b/examples/text/scramble-tl/index.html new file mode 100644 index 000000000..c3647fbcd --- /dev/null +++ b/examples/text/scramble-tl/index.html @@ -0,0 +1,99 @@ + + + + Text / Scramble TL / Anime.js + + + + + + + +
        +
        +

        Introducing

        Introducing

        +

        Introducing

        Introducing

        Introducing

        +

        Introducing

        Introducing

        Introducing

        Introducing

        +

        Introducing

        Introducing

        Introducing

        Introducing

        Introducing

        +

        Introducing

        Introducing

        Introducing

        Introducing

        +

        Introducing

        Introducing

        Introducing

        +

        Introducing

        Introducing

        +
        +
        +

        Custom Easing

        Keyframe Chaining

        +

        Custom Characters

        Perturbation

        Settle Duration

        +

        Char Interval

        Text Cursor

        Seeded Random

        Text Override

        +

        Refresh Rate

        Reveal Direction

        Built-in charsets

        +

        Speed Control

        Reveal Delay

        +
        +
        +

        Learn more at animejs.com

        +
        +
        + + + diff --git a/examples/text/scramble-tl/index.js b/examples/text/scramble-tl/index.js new file mode 100644 index 000000000..068201465 --- /dev/null +++ b/examples/text/scramble-tl/index.js @@ -0,0 +1,164 @@ +import { createTimeline, scrambleText, stagger } from '../../../dist/modules/index.js'; +import { showGUI } from '../../../../../animatorkit/editor/gui/index.js'; + +showGUI(); + +const tl = createTimeline(); + +tl.add('.slide:nth-child(1)', { + opacity: {to: 1, duration: 250, ease: 'linear' }, + scale: [{from: .75, to: 1, duration: 1500, ease: 'inOut(3.5)'}], + ease: 'inOut(3)', +}); +tl.add('.slide:nth-child(1) p.center', { + scale: { from: 3 }, + color: { from: 'var(--yellow-1)', to: 'var(--orange-1)' }, + innerHTML: scrambleText({ + override: ' ', + ease: 'inQuad', + duration: 500, + from: 'center', + cursor: 'â–‘â–’â–“â–ˆ', + }), +}, '<<'); +tl.add('body', { + background: 'var(--red-5)', +}, '<<+=50'); +tl.add('.slide:nth-child(1) p:not(.center)', { + scale: { from: .75 }, + color: { to: 'var(--red-1)' }, + innerHTML: scrambleText({ + override: ' ', + from: 'center', + duration: 500, + revealDelay: 250, + cursor: 'â–‘â–’â–“', + perturbation: .25, + }), +}, stagger([250, 750], { grid: true, from: 'center', ease: 'out(3)', start: '<<' })); +tl.add('.slide:nth-child(1) p:not(.center)', { + innerHTML: scrambleText({ + text: '', + override: false, + from: 'center', + ease: 'outQuad', + reversed: true, + duration: 800, + cursor: 'â–‘â–’â–“', + }) +}, '<+=150'); +tl.add('body', { + background: 'var(--black-1)', +}, '<-=600'); +tl.add('.slide:nth-child(1) p.center', { + scale: 1.5, + color: { to: 'var(--lime-1)' }, + ease: 'inOutExpo', + duration: 1500, + innerHTML: scrambleText({ + text: 'Anime.js Scramble Text', + ease: 'inQuad', + override: false, + from: 'center', + duration: 1000, + perturbation: .25, + }), +}, '<<'); +tl.add('.slide:nth-child(1) p.center', { + scale: 1, + color: 'var(--white-1)', + ease: 'inOutExpo', + duration: 1150, + innerHTML: scrambleText({ + override: false, + text: 'Scramble text animations made easy.', + from: 'right', + duration: 950, + settleDuration: 500, + ease: 'inOut' + }), +}, '<+=250'); +tl.add('.slide:nth-child(1) p.center', { + innerHTML: scrambleText({ + text: '', + override: false, + from: 'random', + reversed: true, + duration: 850, + perturbation: .5, + }), +}, '<+=500'); +tl.set('.slide:nth-child(2)', { + opacity: 1, +}, '<<'); +tl.add('.slide:nth-child(2) p', { + innerHTML: scrambleText({ + override: ' ', + from: 'center', + duration: 500, + revealDelay: 250, + cursor: 'â–‘â–’â–“', + perturbation: .5, + }), +}, stagger([0, 1000], { grid: true, from: 'center', ease: 'out(3)', start: '<<+=250', reversed: true })); +tl.add('.slide:nth-child(2) p', { + scale: [.8, 1], +}, stagger([0, 150], { grid: true, from: 'center', ease: 'out(3)', start: '<<', reversed: true })); +tl.add('.slide:nth-child(2) p', { + innerHTML: scrambleText({ + text: ' ', + override: false, + from: 'center', + reversed: true, + duration: 500, + cursor: 'â–‘â–’â–“', + }) +}, stagger([0, 750], { grid: true, from: 'center', ease: 'out(3)', start: '<+=500' })); +tl.set('.slide:nth-child(3)', { + opacity: 1, +}, '<<'); +tl.add('.slide:nth-child(3) p', { + color: 'var(--white-1)', + scale: 1.5, + ease: 'inOutExpo', + innerHTML: scrambleText({ + override: ' ', + from: 'center', + settleDuration: 500, + revealRate: 33, + perturbation: .2, + }), +}, '-=250') +tl.add('.slide:nth-child(3) p', { + color: { to: 'var(--orange-1)', duration: 750 }, + ease: 'inOutExpo', + duration: 1250, + innerHTML: scrambleText({ + text: 'Anime.js v4.4.0', + override: false, + from: 'right', + cursor: 'â–‘â–’â–“', + duration: 750, + ease: 'inOut', + }), +}, '<+=750') +tl.add('.slide:nth-child(3) p', { + color: 'var(--yellow-1)', + scale: 2, + ease: 'inOutExpo', + duration: 750, + innerHTML: scrambleText({ + text: ' ', + chars: '#!%â–‘â–’â–“_01', + override: false, + // cursor: 'â–‘â–’â–“', + duration: 750, + ease: 'out(2)', + from: 'right', + }), +}, '<+=1000') +tl.add('body', { + background: 'var(--orange-5)', + duration: 750, +}, '<<'); +tl.init(); diff --git a/examples/text/scramble/index.html b/examples/text/scramble/index.html new file mode 100644 index 000000000..916dba1c7 --- /dev/null +++ b/examples/text/scramble/index.html @@ -0,0 +1,101 @@ + + + + Text / Scramble / Anime.js + + + + + + + +
        +

        Anime.js Scramble Text

        +

        A lightweight text scramble effect built into anime.js v4. Hover over any element on this page to see the scramble animation in action.

        +

        Features

        +
          +
        • Named character sets and range syntax for the chars parameter
        • +
        • Directional reveal with the from parameter
        • +
        • Adjustable interval between each character
        • +
        • Per-character settle duration control
        • +
        • Deterministic output with seeded random
        • +
        • Cursor pattern for sweep-style effects
        • +
        • Perturbation to randomize timing and order
        • +
        • Fill character for different-length transitions
        • +
        • Works with any easing function
        • +
        +

        How It Works

        +

        The scrambleText helper returns a function-based tween value. Each target element gets its own closure that captures the original text content and computes a per-character reveal timeline. Characters transition through random values before settling on their final position.

        +

        The animation duration is automatically calculated from the text length and timing parameters, ensuring consistent visual pacing regardless of content size, or use the duration parameter to set an exact duration that overrides the computed value.

        +
        + + + diff --git a/examples/text/scramble/index.js b/examples/text/scramble/index.js new file mode 100644 index 000000000..af577d033 --- /dev/null +++ b/examples/text/scramble/index.js @@ -0,0 +1,101 @@ +import { animate, createTimer, createTimeline, scrambleText, $ } from '../../../dist/modules/index.js'; +import { createTweaks, syncTweaks, Float, Int, Str, Bool, Select, registerType, registerTypeGUI } from 'tweaks'; +import { GUI } from 'tweaks/gui'; + +// Debug panel start + +syncTweaks('localStorage'); + +const Override = registerType('Override', false); +const overrideModes = { false: 0, true: 0, string: 0 }; + +registerTypeGUI('Override', (prop, labelKey) => { + if (!prop.list) prop.list = { mode: 'false', text: '', src: undefined }; + const s = prop.list; + if (s.src !== prop.value) { + s.src = prop.value; + if (prop.value === false) s.mode = 'false'; + else if (prop.value === true) s.mode = 'true'; + else { s.mode = 'string'; s.text = prop.value; } + } + let changed = GUI.Select(s, 'mode::"override"', overrideModes); + if (changed) { + prop.value = s.mode === 'true' ? true : s.mode === 'string' ? s.text : false; + s.src = prop.value; + } + if (s.mode === 'string') { + if (GUI.Text(s, 'text::"override chars"')) { + prop.value = s.text; + s.src = prop.value; + changed = true; + } + } + return changed; +}); + +const scrambleParams = createTweaks('Scramble', { + text: Str(), + from: Select(['auto', 'left', 'right', 'center', 'random']), + reversed: Bool(false), + ease: Select(['linear', 'in(2)', 'out(2)', 'inOut(2)', 'steps(10)']), + chars: Select(['', 'lowercase', 'uppercase', 'numbers', 'symbols', 'braille', 'blocks', 'shades'], 0), + cursor: Str('â–‘â–’â–“â–ˆ'), + override: Override(false), + perturbation: Float(0, 0, 1, 0.01), + duration: Int(500), + delay: Int(0, 0, 2000), + revealDelay: Int(0, 0, 2000), + revealRate: Int(50), + settleDuration: Int(250), + settleRate: Int(30), +}); + +const audioParams = { sound: false }; + +GUI.render(() => { + if (GUI.BeginPanel('Scramble config')) { + GUI.Tweaks(scrambleParams); + if (GUI.Checkbox(audioParams, 'sound')) ctx.resume(); + GUI.EndPanel(); + } +}); + +// Debug panel ends + +const ctx = new AudioContext(); +let allowSound = false; +const tickSound = () => { + if (!audioParams.sound || !allowSound) return; + allowSound = false; + const t = ctx.currentTime; + const o = ctx.createOscillator(); + const g = ctx.createGain(); + o.type = 'sine'; + o.frequency.setValueAtTime(4000 + Math.random() * 400, t); + g.gain.setValueAtTime(0.001, t); + g.gain.linearRampToValueAtTime(0.035, t + 0.001); + g.gain.exponentialRampToValueAtTime(0.001, t + 0.003); + o.connect(g).connect(ctx.destination); + o.start(t); + o.stop(t + 0.003); +} + +const sound = createTimer({ onUpdate: () => allowSound = true, frameRate: 30 }); +const intro = createTimeline({ delay: 500 }); + +$('.scramble').forEach($el => { + const replay = () => animate($el, { innerHTML: scrambleText({ ...scrambleParams, text: scrambleParams.text || undefined, onChange: tickSound }) }); + intro.add($el, { + innerHTML: scrambleText({ + override: '', + duration: 750, + settleDuration: 250, + perturbation: .2, + cursor: 'â–‘â–’â–“â–ˆ', + }), + }, '-=620'); + $el.addEventListener('pointerenter', replay); + $el.addEventListener('pointerdown', replay); +}); + +intro.init(); diff --git a/package-lock.json b/package-lock.json index 1d0fda886..1d6704e4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "animejs", - "version": "4.2.1", + "version": "4.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "animejs", - "version": "4.2.1", + "version": "4.4.0", "license": "MIT", "devDependencies": { "@rollup/plugin-terser": "^0.4.4", @@ -18,6 +18,7 @@ "nodemon": "^3.1.10", "rollup": "^4.9.1", "three": "^0.160.0", + "tweaks": "^0.3.4", "typescript": "^5.9.2" }, "funding": { @@ -86,6 +87,34 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.46.2", "cpu": [ @@ -98,6 +127,244 @@ "darwin" ] }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "dev": true, @@ -2265,6 +2532,16 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tweaks": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/tweaks/-/tweaks-0.3.4.tgz", + "integrity": "sha512-17F4fN8kSyio/0uYR+IzjypL402lAe2Y7QNz0BJ4Kcb856uMKbhGnOmFsuGVlA0exqj+vMHlWDn3lE0gzJi0mA==", + "dev": true, + "license": "MIT", + "bin": { + "tweaks": "cli.js" + } + }, "node_modules/type-detect": { "version": "4.1.0", "dev": true, diff --git a/package.json b/package.json index ae59cc99b..428291004 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "animejs", - "version": "4.2.2", - "homepage": "https://animejs.com", + "version": "4.4.1", "description": "JavaScript animation engine", + "homepage": "https://animejs.com", "author": "Julian Garnier ", "license": "MIT", "repository": { @@ -104,6 +104,12 @@ "import": "./dist/modules/events/index.js", "default": "./dist/modules/events/index.cjs" }, + "./layout": { + "types": "./dist/modules/layout/index.d.ts", + "require": "./dist/modules/layout/index.cjs", + "import": "./dist/modules/layout/index.js", + "default": "./dist/modules/layout/index.cjs" + }, "./easings": { "types": "./dist/modules/easings/index.d.ts", "require": "./dist/modules/easings/index.cjs", @@ -181,6 +187,7 @@ "nodemon": "^3.1.10", "rollup": "^4.9.1", "three": "^0.160.0", + "tweaks": "^0.3.4", "typescript": "^5.9.2" }, "scripts": { diff --git a/src/animation/animation.js b/src/animation/animation.js index 83e0983cd..93f3ceec5 100644 --- a/src/animation/animation.js +++ b/src/animation/animation.js @@ -4,6 +4,7 @@ import { tweenTypes, valueTypes, compositionTypes, + isDomSymbol, } from '../core/consts.js'; import { @@ -20,6 +21,8 @@ import { isNum, round, isNil, + isFnc, + isStr, } from '../core/helpers.js'; import { @@ -40,11 +43,13 @@ import { decomposeTweenValue, decomposedOriginalValue, createDecomposedValueTargetObject, + composeColorValue, + composeComplexValue, } from '../core/values.js'; import { sanitizePropertyName, - cleanInlineStyles, + revertValues, } from '../core/styles.js'; import { @@ -104,12 +109,14 @@ const fromTargetObject = createDecomposedValueTargetObject(); const toTargetObject = createDecomposedValueTargetObject(); const inlineStylesStore = {}; const toFunctionStore = { func: null }; +const fromFunctionStore = { func: null }; const keyframesTargetArray = [null]; const fastSetValuesArray = [null, null]; /** @type {TweenKeyValue} */ const keyObjectTarget = { to: null }; let tweenId = 0; +let JSAnimationId = 0; let keyframes; /** @type {TweenParamsOptions & TweenValues} */ let key; @@ -210,7 +217,7 @@ export class JSAnimation extends Timer { * @param {Number} [parentPosition] * @param {Boolean} [fastSet=false] * @param {Number} [index=0] - * @param {Number} [length=0] + * @param {TargetsArray} [allTargets] */ constructor( targets, @@ -219,11 +226,13 @@ export class JSAnimation extends Timer { parentPosition, fastSet = false, index = 0, - length = 0 + allTargets ) { super(/** @type {TimerParams & AnimationParams} */(parameters), parent, parentPosition); + ++JSAnimationId; + const parsedTargets = registerTargets(targets); const targetsLength = parsedTargets.length; @@ -233,6 +242,7 @@ export class JSAnimation extends Timer { const params = /** @type {AnimationParams} */(kfParams ? mergeObjects(generateKeyframes(/** @type {DurationKeyframes} */(kfParams), parameters), parameters) : parameters); const { + id, delay, duration, ease, @@ -243,11 +253,12 @@ export class JSAnimation extends Timer { } = params; const animDefaults = parent ? parent.defaults : globals.defaults; - const animaPlaybackEase = setValue(playbackEase, animDefaults.playbackEase); - const animEase = animaPlaybackEase ? parseEase(animaPlaybackEase) : null; - const hasSpring = !isUnd(ease) && !isUnd(/** @type {Spring} */(ease).ease); - const tEasing = hasSpring ? /** @type {Spring} */(ease).ease : setValue(ease, animEase ? 'linear' : animDefaults.ease); - const tDuration = hasSpring ? /** @type {Spring} */(ease).settlingDuration : setValue(duration, animDefaults.duration); + const animEase = setValue(ease, animDefaults.ease); + const animPlaybackEase = setValue(playbackEase, animDefaults.playbackEase); + const parsedAnimPlaybackEase = animPlaybackEase ? parseEase(animPlaybackEase) : null; + const hasSpring = !isUnd(/** @type {Spring} */(animEase).ease); + const tEasing = hasSpring ? /** @type {Spring} */(animEase).ease : setValue(ease, parsedAnimPlaybackEase ? 'linear' : animDefaults.ease); + const tDuration = hasSpring ? /** @type {Spring} */(animEase).settlingDuration : setValue(duration, animDefaults.duration); const tDelay = setValue(delay, animDefaults.delay); const tModifier = modifier || animDefaults.modifier; // If no composition is defined and the targets length is high (>= 1000) set the composition to 'none' (0) for faster tween creation @@ -255,7 +266,7 @@ export class JSAnimation extends Timer { // const absoluteOffsetTime = this._offset; const absoluteOffsetTime = this._offset + (parent ? parent._offset : 0); // This allows targeting the current animation in the spring onComplete callback - if (hasSpring) /** @type {Spring} */(ease).parent = this; + if (hasSpring) /** @type {Spring} */(animEase).parent = this; let iterationDuration = NaN; let iterationDelay = NaN; @@ -266,7 +277,7 @@ export class JSAnimation extends Timer { const target = parsedTargets[targetIndex]; const ti = index || targetIndex; - const tl = length || targetsLength; + const tl = allTargets || parsedTargets; let lastTransformGroupIndex = NaN; let lastTransformGroupLength = NaN; @@ -340,8 +351,16 @@ export class JSAnimation extends Timer { } toFunctionStore.func = null; + fromFunctionStore.func = null; - const computedToValue = getFunctionValue(key.to, target, ti, tl, toFunctionStore); + const computedComposition = getFunctionValue(setValue(key.composition, tComposition), target, ti, tl, null, null); + const tweenComposition = isNum(computedComposition) ? computedComposition : compositionTypes[computedComposition]; + if (!siblings && tweenComposition !== compositionTypes.none) siblings = getTweenSiblings(target, propName); + // Timelines pass the last sibling tween if it belongs to the same timeline + // Standalone animations only pass prevTween when the property has multiple keyframes + const tailTween = siblings ? siblings._tail : null; + const prevSiblingTween = parent && tailTween && tailTween.parent.parent === parent ? tailTween : prevTween; + const computedToValue = getFunctionValue(key.to, target, ti, tl, toFunctionStore, prevSiblingTween); let tweenToValue; // Allows function based values to return an object syntax value ({to: v}) @@ -351,17 +370,18 @@ export class JSAnimation extends Timer { } else { tweenToValue = computedToValue; } - const tweenFromValue = getFunctionValue(key.from, target, ti, tl); - const keyEasing = key.ease; + const tweenFromValue = getFunctionValue(key.from, target, ti, tl, null, prevSiblingTween); + const easeToParse = key.ease || tEasing; + + const easeFunctionResult = getFunctionValue(easeToParse, target, ti, tl, null, prevSiblingTween); + const keyEasing = isFnc(easeFunctionResult) || isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; + const hasSpring = !isUnd(keyEasing) && !isUnd(/** @type {Spring} */(keyEasing).ease); - // Easing are treated differently and don't accept function based value to prevent having to pass a function wrapper that returns an other function all the time - const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing || tEasing; + const tweenEasing = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; // Calculate default individual keyframe duration by dividing the tl of keyframes - const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(setValue(key.duration, (l > 1 ? getFunctionValue(tDuration, target, ti, tl) / l : tDuration)), target, ti, tl); + const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(setValue(key.duration, (l > 1 ? getFunctionValue(tDuration, target, ti, tl, null, prevSiblingTween) / l : tDuration)), target, ti, tl, null, prevSiblingTween); // Default delay value should only be applied to the first tween - const tweenDelay = getFunctionValue(setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl); - const computedComposition = getFunctionValue(setValue(key.composition, tComposition), target, ti, tl); - const tweenComposition = isNum(computedComposition) ? computedComposition : compositionTypes[computedComposition]; + const tweenDelay = getFunctionValue(setValue(key.delay, (!tweenIndex ? tDelay : 0)), target, ti, tl, null, prevSiblingTween); // Modifiers are treated differently and don't accept function based value to prevent having to pass a function wrapper const tweenModifier = key.modifier || tModifier; const hasFromvalue = !isUnd(tweenFromValue); @@ -378,7 +398,6 @@ export class JSAnimation extends Timer { let prevSibling = prevTween; if (tweenComposition !== compositionTypes.none) { - if (!siblings) siblings = getTweenSiblings(target, propName); let nextSibling = siblings._head; // Iterate trough all the next siblings until we find a sibling with an equal or inferior start time while (nextSibling && !nextSibling._isOverridden && nextSibling._absoluteStartTime <= absoluteStartTime) { @@ -397,8 +416,10 @@ export class JSAnimation extends Timer { // Decompose values if (isFromToValue) { - decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[0], target, ti, tl) : tweenFromValue, fromTargetObject); - decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore) : tweenToValue, toTargetObject); + decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[0], target, ti, tl, fromFunctionStore, prevSiblingTween) : tweenFromValue, fromTargetObject); + decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore, prevSiblingTween) : tweenToValue, toTargetObject); + // Needed to force an inline style registration + const originalValue = getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore); if (fromTargetObject.t === valueTypes.NUMBER) { if (prevSibling) { if (prevSibling._valueType === valueTypes.UNIT) { @@ -407,7 +428,7 @@ export class JSAnimation extends Timer { } } else { decomposeRawValue( - getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore), + originalValue, decomposedOriginalValue ); if (decomposedOriginalValue.t === valueTypes.UNIT) { @@ -512,7 +533,8 @@ export class JSAnimation extends Timer { property: propName, target: target, _value: null, - _func: toFunctionStore.func, + _toFunc: toFunctionStore.func, + _fromFunc: fromFunctionStore.func, _ease: parseEase(tweenEasing), _fromNumbers: cloneArray(fromTargetObject.d), _toNumbers: cloneArray(toTargetObject.d), @@ -549,6 +571,18 @@ export class JSAnimation extends Timer { composeTween(tween, siblings); } + // Pre-compute the tween end value for function-based value chaining (ie morphTo / scrambleText in keyframe arrays and timelines) + const vt = tween._valueType; + if (vt === valueTypes.COMPLEX) { + tween._value = composeComplexValue(tween, 1, -1); + } else if (vt === valueTypes.COLOR) { + tween._value = composeColorValue(tween, 1, -1); + } else if (vt === valueTypes.UNIT) { + tween._value = `${tweenModifier(tween._toNumber)}${tween._unit}`; + } else { + tween._value = tweenModifier(tween._toNumber); + } + if (isNaN(firstTweenChangeStartTime)) { firstTweenChangeStartTime = tween._startTime; } @@ -626,12 +660,14 @@ export class JSAnimation extends Timer { } /** @type {TargetsArray} */ this.targets = parsedTargets; + /** @type {String|Number} */ + this.id = !isUnd(id) ? id : JSAnimationId; /** @type {Number} */ this.duration = iterationDuration === minValue ? minValue : clampInfinity(((iterationDuration + this._loopDelay) * this.iterationCount) - this._loopDelay) || minValue; /** @type {Callback} */ this.onRender = onRender || animDefaults.onRender; /** @type {EasingFunction} */ - this._ease = animEase; + this._ease = parsedAnimPlaybackEase; /** @type {Number} */ this._delay = iterationDelay; // NOTE: I'm keeping delay values separated from offsets in timelines because delays can override previous tweens and it could be confusing to debug a timeline with overridden tweens and no associated visible delays. @@ -668,18 +704,29 @@ export class JSAnimation extends Timer { */ refresh() { forEachChildren(this, (/** @type {Tween} */tween) => { - const tweenFunc = tween._func; - if (tweenFunc) { - const ogValue = getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType); - decomposeRawValue(ogValue, decomposedOriginalValue); - // TODO: Check for from / to Array based values here, - decomposeRawValue(tweenFunc(), toTargetObject); - tween._fromNumbers = cloneArray(decomposedOriginalValue.d); - tween._fromNumber = decomposedOriginalValue.n; - tween._toNumbers = cloneArray(toTargetObject.d); - tween._strings = cloneArray(toTargetObject.s); - // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 - tween._toNumber = toTargetObject.o ? getRelativeValue(decomposedOriginalValue.n, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + const toFunc = tween._toFunc; + const fromFunc = tween._fromFunc; + if (toFunc || fromFunc) { + if (fromFunc) { + decomposeRawValue(fromFunc(), fromTargetObject); + if (fromTargetObject.u !== tween._unit && tween.target[isDomSymbol]) { + convertValueUnit(/** @type {DOMTarget} */(tween.target), fromTargetObject, tween._unit, true); + } + tween._fromNumbers = cloneArray(fromTargetObject.d); + tween._fromNumber = fromTargetObject.n; + } else if (toFunc) { + // When only toFunc exists, get from value from target + decomposeRawValue(getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType), decomposedOriginalValue); + tween._fromNumbers = cloneArray(decomposedOriginalValue.d); + tween._fromNumber = decomposedOriginalValue.n; + } + if (toFunc) { + decomposeRawValue(toFunc(), toTargetObject); + tween._toNumbers = cloneArray(toTargetObject.d); + tween._strings = cloneArray(toTargetObject.s); + // Make sure to apply relative operators https://github.com/juliangarnier/anime/issues/1025 + tween._toNumber = toTargetObject.o ? getRelativeValue(tween._fromNumber, toTargetObject.n, toTargetObject.o) : toTargetObject.n; + } } }); // This forces setter animations to render once @@ -693,7 +740,7 @@ export class JSAnimation extends Timer { */ revert() { super.revert(); - return cleanInlineStyles(this); + return revertValues(this); } /** @@ -715,4 +762,10 @@ export class JSAnimation extends Timer { * @param {AnimationParams} parameters * @return {JSAnimation} */ -export const animate = (targets, parameters) => new JSAnimation(targets, parameters, null, 0, false).init(); +export const animate = (targets, parameters) => { + if (globals.editor) { + return globals.editor.addAnimation(targets, parameters); + } else { + return new JSAnimation(targets, parameters, null, 0, false).init(); + } +}; diff --git a/src/core/clock.js b/src/core/clock.js index 620598bc4..0f43f93f9 100644 --- a/src/core/clock.js +++ b/src/core/clock.js @@ -6,8 +6,8 @@ import { } from './consts.js'; import { - round, -} from './helpers.js'; + defaults, +} from './globals.js'; /** * @import { @@ -29,7 +29,7 @@ export class Clock { /** @type {Number} */ this._currentTime = initTime; /** @type {Number} */ - this._elapsedTime = initTime; + this._lastTickTime = initTime; /** @type {Number} */ this._startTime = initTime; /** @type {Number} */ @@ -37,7 +37,7 @@ export class Clock { /** @type {Number} */ this._scheduledTime = 0; /** @type {Number} */ - this._frameDuration = round(K / maxFps, 0); + this._frameDuration = K / maxFps; /** @type {Number} */ this._fps = maxFps; /** @type {Number} */ @@ -58,7 +58,8 @@ export class Clock { const previousFrameDuration = this._frameDuration; const fr = +frameRate; const fps = fr < minValue ? minValue : fr; - const frameDuration = round(K / fps, 0); + const frameDuration = K / fps; + if (fps > defaults.frameRate) defaults.frameRate = fps; this._fps = fps; this._frameDuration = frameDuration; this._scheduledTime += frameDuration - previousFrameDuration; @@ -79,14 +80,13 @@ export class Clock { */ requestTick(time) { const scheduledTime = this._scheduledTime; - const elapsedTime = this._elapsedTime; - this._elapsedTime += (time - elapsedTime); - // If the elapsed time is lower than the scheduled time + this._lastTickTime = time; + // If the current time is lower than the scheduled time // this means not enough time has passed to hit one frameDuration // so skip that frame - if (elapsedTime < scheduledTime) return tickModes.NONE; + if (time < scheduledTime) return tickModes.NONE; const frameDuration = this._frameDuration; - const frameDelta = elapsedTime - scheduledTime; + const frameDelta = time - scheduledTime; // Ensures that _scheduledTime progresses in steps of at least 1 frameDuration. // Skips ahead if the actual elapsed time is higher. this._scheduledTime += frameDelta < frameDuration ? frameDuration : frameDelta; diff --git a/src/core/consts.js b/src/core/consts.js index 5fed2fd71..cf297dcb4 100644 --- a/src/core/consts.js +++ b/src/core/consts.js @@ -3,8 +3,10 @@ // TODO: Do we need to check if we're running inside a worker ? export const isBrowser = typeof window !== 'undefined'; -/** @type {Window & {AnimeJS: Array}|null} */ -export const win = isBrowser ? /** @type {Window & {AnimeJS: Array}} */(/** @type {unknown} */(window)) : null; +/** @typedef {Window & {AnimeJS: Array}|null} AnimeJSWindow + +/** @type {AnimeJSWindow} */ +export const win = isBrowser ? /** @type {AnimeJSWindow} */(/** @type {unknown} */(window)) : null; /** @type {Document|null} */ export const doc = isBrowser ? document : null; @@ -48,7 +50,6 @@ export const isRegisteredTargetSymbol = Symbol(); export const isDomSymbol = Symbol(); export const isSvgSymbol = Symbol(); export const transformsSymbol = Symbol(); -export const morphPointsSymbol = Symbol(); export const proxyTargetSymbol = Symbol(); // Numbers @@ -56,7 +57,7 @@ export const proxyTargetSymbol = Symbol(); export const minValue = 1e-11; export const maxValue = 1e12; export const K = 1e3; -export const maxFps = 120; +export const maxFps = 240; // Strings @@ -72,6 +73,7 @@ export const shortTransforms = /*#__PURE__*/ (() => { })(); export const validTransforms = [ + 'perspective', 'translateX', 'translateY', 'translateZ', @@ -86,9 +88,6 @@ export const validTransforms = [ 'skew', 'skewX', 'skewY', - 'matrix', - 'matrix3d', - 'perspective', ]; export const transformsFragmentStrings = /*#__PURE__*/ validTransforms.reduce((a, v) => ({...a, [v]: v + '('}), {}); @@ -100,6 +99,7 @@ export const noop = () => {}; // Regex +export const validRgbHslRgx = /\)\s*[-.\d]/; export const hexTestRgx = /(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i; export const rgbExecRgx = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i export const rgbaExecRgx = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i @@ -110,6 +110,5 @@ export const digitWithExponentRgx = /[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi; // export const unitsExecRgx = /^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)+([a-z]+|%)$/i; export const unitsExecRgx = /^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i export const lowerCaseRgx = /([a-z])([A-Z])/g; -export const transformsExecRgx = /(\w+)(\([^)]+\)+)/g; // Match inline transforms with cacl() values, returns the value wrapped in () export const relativeValuesExecRgx = /(\*=|\+=|-=)/; export const cssVariableMatchRgx = /var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/; diff --git a/src/core/globals.js b/src/core/globals.js index c9199eb33..730cf6310 100644 --- a/src/core/globals.js +++ b/src/core/globals.js @@ -19,6 +19,18 @@ import { * } from '../scope/index.js' */ +/** + * @typedef {Object} EditorGlobals + * @property {boolean} showPanel + * @property {boolean} synced + * @property {Function} addAnimation + * @property {Function} addTimeline + * @property {Function} addTimelineChild + * @property {Function} resolveStagger + * @property {Object|null} _head + * @property {Object|null} _tail + */ + /** @type {DefaultsParams} */ export const defaults = { id: null, @@ -62,6 +74,8 @@ export const globals = { timeScale: 1, /** @type {Number} */ tickThreshold: 200, + /** @type {EditorGlobals|null} */ + editor: null, } export const globalVersions = { version: '__packageVersion__', engine: null }; diff --git a/src/core/helpers.js b/src/core/helpers.js index 944f3c52c..0cbbdd1af 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -1,5 +1,6 @@ import { isBrowser, + validRgbHslRgx, lowerCaseRgx, hexTestRgx, maxValue, @@ -59,8 +60,8 @@ export const isHex = a => hexTestRgx.test(a); export const isRgb = a => stringStartsWith(a, 'rgb'); /**@param {any} a @return {Boolean} */ export const isHsl = a => stringStartsWith(a, 'hsl'); -/**@param {any} a @return {Boolean} */ -export const isCol = a => isHex(a) || isRgb(a) || isHsl(a); +/**@param {any} a @return {Boolean} */ // Make sure boxShadow syntax like 'rgb(255, 0, 0) 0px 0px 6px 0px' is not a valid color type +export const isCol = a => isHex(a) || ((isRgb(a) || isHsl(a)) && (a[a.length - 1] === ')' || !validRgbHslRgx.test(a))); /**@param {any} a @return {Boolean} */ export const isKey = a => !globals.defaults.hasOwnProperty(a); @@ -124,8 +125,6 @@ export const _round = Math.round; */ export const clamp = (v, min, max) => v < min ? min : v > max ? max : v; -const powCache = {}; - /** * Rounds a number to specified decimal places * @@ -133,13 +132,12 @@ const powCache = {}; * @param {Number} decimalLength - Number of decimal places * @return {Number} */ -export const round = (v, decimalLength) => { - if (decimalLength < 0) return v; - if (!decimalLength) return _round(v); - let p = powCache[decimalLength]; - if (!p) p = powCache[decimalLength] = 10 ** decimalLength; - return _round(v * p) / p; -}; + export const round = (v, decimalLength) => { + if (decimalLength < 0) return v; + if (!decimalLength) return _round(v); + const p = 10 ** decimalLength; + return _round(v * p) / p; + } /** * Snaps a value to nearest increment or array value diff --git a/src/core/render.js b/src/core/render.js index 27a8fb145..d647b71bc 100644 --- a/src/core/render.js +++ b/src/core/render.js @@ -7,8 +7,6 @@ import { valueTypes, tickModes, compositionTypes, - emptyString, - transformsFragmentStrings, transformsSymbol, minValue, } from './consts.js'; @@ -21,8 +19,17 @@ import { forEachChildren, } from './helpers.js'; +import { + buildTransformString, +} from './transforms.js'; + +import { + composeColorValue, + composeComplexValue, +} from './values.js'; + /** - * @import { + * @import { * Tickable, * Renderable, * CallbackArgument, @@ -65,7 +72,6 @@ export const render = (tickable, time, muteCallbacks, internalRender, tickMode) const _hasChildren = tickable._hasChildren; const tickableDelay = tickable._delay; const tickablePrevAbsoluteTime = tickable._currentTime; // TODO: rename ._currentTime to ._absoluteCurrentTime - const tickableEndTime = tickableDelay + iterationDuration; const tickableAbsoluteTime = time - tickableDelay; const tickablePrevTime = clamp(tickablePrevAbsoluteTime, -tickableDelay, duration); @@ -197,30 +203,9 @@ export const render = (tickable, time, muteCallbacks, internalRender, tickMode) number = /** @type {Number} */(tweenModifier(round(lerp(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision))); value = `${number}${tween._unit}`; } else if (tweenValueType === valueTypes.COLOR) { - const fn = tween._fromNumbers; - const tn = tween._toNumbers; - const r = round(clamp(/** @type {Number} */(tweenModifier(lerp(fn[0], tn[0], tweenProgress))), 0, 255), 0); - const g = round(clamp(/** @type {Number} */(tweenModifier(lerp(fn[1], tn[1], tweenProgress))), 0, 255), 0); - const b = round(clamp(/** @type {Number} */(tweenModifier(lerp(fn[2], tn[2], tweenProgress))), 0, 255), 0); - const a = clamp(/** @type {Number} */(tweenModifier(round(lerp(fn[3], tn[3], tweenProgress), tweenPrecision))), 0, 1); - value = `rgba(${r},${g},${b},${a})`; - if (tweenHasComposition) { - const ns = tween._numbers; - ns[0] = r; - ns[1] = g; - ns[2] = b; - ns[3] = a; - } + value = composeColorValue(tween, tweenProgress, tweenPrecision); } else if (tweenValueType === valueTypes.COMPLEX) { - value = tween._strings[0]; - for (let j = 0, l = tween._toNumbers.length; j < l; j++) { - const n = /** @type {Number} */(tweenModifier(round(lerp(tween._fromNumbers[j], tween._toNumbers[j], tweenProgress), tweenPrecision))); - const s = tween._strings[j + 1]; - value += `${s ? n + s : n}`; - if (tweenHasComposition) { - tween._numbers[j] = n; - } - } + value = composeComplexValue(tween, tweenProgress, tweenPrecision); } // For additive tweens and Animatables @@ -263,14 +248,8 @@ export const render = (tickable, time, muteCallbacks, internalRender, tickMode) } - // NOTE: Possible improvement: Use translate(x,y) / translate3d(x,y,z) syntax - // to reduce memory usage on string composition if (tweenTransformsNeedUpdate && tween._renderTransforms) { - let str = emptyString; - for (let key in tweenTargetTransformsProperties) { - str += `${transformsFragmentStrings[key]}${tweenTargetTransformsProperties[key]}) `; - } - tweenStyle.transform = str; + tweenStyle.transform = buildTransformString(tweenTargetTransformsProperties); tweenTransformsNeedUpdate = 0; } @@ -381,7 +360,6 @@ export const tick = (tickable, time, muteCallbacks, internalRender, tickMode) => // Renders on timeline are triggered by its children so it needs to be set after rendering the children if (!muteCallbacks && tlChildrenHasRendered) tl.onRender(/** @type {CallbackArgument} */(tl)); - // Triggers the timeline onComplete() once all chindren all completed and the current time has reached the end if ((tlChildrenHaveCompleted || tlIsRunningBackwards) && tl._currentTime >= tl.duration) { // Make sure the paused flag is false in case it has been skipped in the render function diff --git a/src/core/styles.js b/src/core/styles.js index 9c94ce7da..a2bfb50e3 100644 --- a/src/core/styles.js +++ b/src/core/styles.js @@ -4,17 +4,19 @@ import { isDomSymbol, transformsSymbol, emptyString, - transformsFragmentStrings, } from './consts.js'; import { forEachChildren, isNil, isSvg, - isUnd, toLowerCase, } from './helpers.js'; +import { + buildTransformString, +} from './transforms.js'; + /** * @import { * JSAnimation, @@ -64,56 +66,75 @@ export const sanitizePropertyName = (propertyName, target, tweenType) => { /** * @template {Renderable} T * @param {T} renderable + * @param {Boolean} [inlineStylesOnly] * @return {T} */ -export const cleanInlineStyles = renderable => { - // Allow cleanInlineStyles() to be called on timelines +export const revertValues = (renderable, inlineStylesOnly = false) => { + // Allow revertValues() to be called on timelines if (renderable._hasChildren) { - forEachChildren(renderable, cleanInlineStyles, true); + forEachChildren(renderable, (/** @type {Renderable} */child) => revertValues(child, inlineStylesOnly), true); } else { const animation = /** @type {JSAnimation} */(renderable); animation.pause(); forEachChildren(animation, (/** @type {Tween} */tween) => { const tweenProperty = tween.property; const tweenTarget = tween.target; - if (tweenTarget[isDomSymbol]) { - const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; - const originalInlinedValue = tween._inlineValue; - const tweenHadNoInlineValue = isNil(originalInlinedValue) || originalInlinedValue === emptyString; - if (tween._tweenType === tweenTypes.TRANSFORM) { - const cachedTransforms = tweenTarget[transformsSymbol]; - if (tweenHadNoInlineValue) { - delete cachedTransforms[tweenProperty]; - } else { - cachedTransforms[tweenProperty] = originalInlinedValue; - } - if (tween._renderTransforms) { - if (!Object.keys(cachedTransforms).length) { - targetStyle.removeProperty('transform'); + const tweenType = tween._tweenType; + const originalInlinedValue = tween._inlineValue; + const tweenHadNoInlineValue = isNil(originalInlinedValue) || originalInlinedValue === emptyString; + if (tweenType === tweenTypes.OBJECT) { + if (!inlineStylesOnly && !tweenHadNoInlineValue) { + tweenTarget[tweenProperty] = originalInlinedValue; + } + } else if (tweenTarget[isDomSymbol]) { + if (tweenType === tweenTypes.ATTRIBUTE) { + if (!inlineStylesOnly) { + if (tweenHadNoInlineValue) { + /** @type {DOMTarget} */(tweenTarget).removeAttribute(tweenProperty); } else { - let str = emptyString; - for (let key in cachedTransforms) { - str += transformsFragmentStrings[key] + cachedTransforms[key] + ') '; - } - targetStyle.transform = str; + /** @type {DOMTarget} */(tweenTarget).setAttribute(tweenProperty, /** @type {String} */(originalInlinedValue)); } } } else { - if (tweenHadNoInlineValue) { - targetStyle.removeProperty(toLowerCase(tweenProperty)); + const targetStyle = /** @type {DOMTarget} */(tweenTarget).style; + if (tweenType === tweenTypes.TRANSFORM) { + const cachedTransforms = tweenTarget[transformsSymbol]; + if (tweenHadNoInlineValue) { + delete cachedTransforms[tweenProperty]; + } else { + cachedTransforms[tweenProperty] = originalInlinedValue; + } + if (tween._renderTransforms) { + if (!Object.keys(cachedTransforms).length) { + targetStyle.removeProperty('transform'); + } else { + targetStyle.transform = buildTransformString(cachedTransforms); + } + } } else { - targetStyle[tweenProperty] = originalInlinedValue; + if (tweenHadNoInlineValue) { + targetStyle.removeProperty(toLowerCase(tweenProperty)); + } else { + targetStyle[tweenProperty] = originalInlinedValue; + } } } - if (animation._tail === tween) { - animation.targets.forEach(t => { - if (t.getAttribute && t.getAttribute('style') === emptyString) { - t.removeAttribute('style'); - }; - }); - } + } + if (tweenTarget[isDomSymbol] && animation._tail === tween) { + animation.targets.forEach(t => { + if (t.getAttribute && t.getAttribute('style') === emptyString) { + t.removeAttribute('style'); + }; + }); } }) } return renderable; } + +/** + * @template {Renderable} T + * @param {T} renderable + * @return {T} + */ +export const cleanInlineStyles = renderable => revertValues(renderable, true); diff --git a/src/core/transforms.js b/src/core/transforms.js index f963e299a..6a12625da 100644 --- a/src/core/transforms.js +++ b/src/core/transforms.js @@ -1,6 +1,8 @@ import { - transformsExecRgx, transformsSymbol, + validTransforms, + transformsFragmentStrings, + emptyString, } from './consts.js'; import { @@ -22,24 +24,139 @@ import { */ export const parseInlineTransforms = (target, propName, animationInlineStyles) => { const inlineTransforms = target.style.transform; - let inlinedStylesPropertyValue; if (inlineTransforms) { const cachedTransforms = target[transformsSymbol]; - let t; while (t = transformsExecRgx.exec(inlineTransforms)) { - const inlinePropertyName = t[1]; - // const inlinePropertyValue = t[2]; - const inlinePropertyValue = t[2].slice(1, -1); - cachedTransforms[inlinePropertyName] = inlinePropertyValue; - if (inlinePropertyName === propName) { - inlinedStylesPropertyValue = inlinePropertyValue; - // Store the new parsed inline styles if animationInlineStyles is provided - if (animationInlineStyles) { - animationInlineStyles[propName] = inlinePropertyValue; + let pos = 0; + const len = inlineTransforms.length; + let fullTranslateValue; + while (pos < len) { + // Skip whitespace + while (pos < len && inlineTransforms.charCodeAt(pos) === 32) pos++; + if (pos >= len) break; + // Read function name + const nameStart = pos; + while (pos < len && inlineTransforms.charCodeAt(pos) !== 40) pos++; + if (pos >= len) break; + const name = inlineTransforms.substring(nameStart, pos); + // Scan to closing paren, recording top-level comma positions + let depth = 1; + const valueStart = pos + 1; + let c1 = -1, c2 = -1; + pos++; + while (pos < len && depth > 0) { + const c = inlineTransforms.charCodeAt(pos); + if (c === 40) depth++; + else if (c === 41) depth--; + else if (c === 44 && depth === 1) { + if (c1 === -1) c1 = pos; + else if (c2 === -1) c2 = pos; } + pos++; } + const valueEnd = pos - 1; + // Decompose multi-arg functions into individual axis properties + if (name === 'translate' || name === 'translate3d') { + if (c1 === -1) { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.translateX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.translateY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.translateZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } + } + fullTranslateValue = inlineTransforms.substring(valueStart, valueEnd); + } else if (name === 'scale' || name === 'scale3d') { + if (c1 === -1) { + cachedTransforms.scale = inlineTransforms.substring(valueStart, valueEnd).trim(); + } else { + cachedTransforms.scaleX = inlineTransforms.substring(valueStart, c1).trim(); + if (c2 === -1) { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, valueEnd).trim(); + } else { + cachedTransforms.scaleY = inlineTransforms.substring(c1 + 1, c2).trim(); + cachedTransforms.scaleZ = inlineTransforms.substring(c2 + 1, valueEnd).trim(); + } + } + } else { + cachedTransforms[name] = inlineTransforms.substring(valueStart, valueEnd); + } + } + // Resolve the requested property from the cache + if (propName === 'translate3d' && fullTranslateValue) { + if (animationInlineStyles) animationInlineStyles[propName] = fullTranslateValue; + return fullTranslateValue; + } + const cached = cachedTransforms[propName]; + if (!isUnd(cached)) { + if (animationInlineStyles) animationInlineStyles[propName] = cached; + return cached; } } - return inlineTransforms && !isUnd(inlinedStylesPropertyValue) ? inlinedStylesPropertyValue : + return propName === 'translate3d' ? '0px, 0px, 0px' : + propName === 'rotate3d' ? '0, 0, 0, 0deg' : stringStartsWith(propName, 'scale') ? '1' : stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew') ? '0deg' : '0px'; } + +/** + * Builds a CSS transform string from the target's cached transform properties. + * Iterates validTransforms in order (perspective > translate > rotate > scale > skew > matrix). + * When adjacent axis properties are all present, emits a shorter shorthand (translateX + translateY -> translate(x, y)) + * The index is advanced past consumed properties so they are not emitted twice. + * Properties without a grouping partner (e.g. translateY alone, scaleZ alone) emit individually. + * + * @param {Record} props + * @return {String} + */ +export const buildTransformString = (props) => { + let str = emptyString; + for (let i = 0, l = validTransforms.length; i < l; i++) { + const key = validTransforms[i]; + const val = props[key]; + if (val !== undefined) { + // Group translateX with adjacent translateY / translateZ + if (key === 'translateX') { + const next = props.translateY; + if (next !== undefined) { + const next2 = props.translateZ; + if (next2 !== undefined) { + str += `translate3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `translate(${val},${next}) `; + i += 1; + } + continue; + } + } + // Group scaleX with adjacent scaleY / scaleZ (only when standalone scale is absent) + if (key === 'scaleX' && props.scale === undefined) { + const next = props.scaleY; + if (next !== undefined) { + const next2 = props.scaleZ; + if (next2 !== undefined) { + str += `scale3d(${val},${next},${next2}) `; + i += 2; + } else { + str += `scale(${val},${next}) `; + i += 1; + } + continue; + } + } + // All other properties: emit individually using pre-built fragment string + str += `${transformsFragmentStrings[key]}${val}) `; + } + // Preserve non-animatable rotate3d in correct position (after rotateZ, before scale) + if (key === 'rotateZ') { + if (props.rotate3d !== undefined) str += `rotate3d(${props.rotate3d}) `; + } + } + // Preserve non-animatable matrix/matrix3d from inline styles + if (props.matrix !== undefined) str += `matrix(${props.matrix}) `; + if (props.matrix3d !== undefined) str += `matrix3d(${props.matrix3d}) `; + return str; +} diff --git a/src/core/values.js b/src/core/values.js index aa3f01743..d669d53ed 100644 --- a/src/core/values.js +++ b/src/core/values.js @@ -3,6 +3,7 @@ import { validTransforms, tweenTypes, valueTypes, + compositionTypes, digitWithExponentRgx, unitsExecRgx, isDomSymbol, @@ -21,6 +22,9 @@ import { isCol, isValidSVGAttribute, isStr, + round, + lerp, + clamp, } from './helpers.js'; import { @@ -38,6 +42,7 @@ import { * Tween, * TweenPropValue, * TweenDecomposedValue, +* TargetsArray, * } from '../types/index.js' */ @@ -55,15 +60,16 @@ export const setValue = (targetValue, defaultValue) => { * @param {TweenPropValue} value * @param {Target} target * @param {Number} index - * @param {Number} total - * @param {Object} [store] + * @param {TargetsArray} targets + * @param {Object|null} store + * @param {Tween|null} prevTween * @return {any} */ -export const getFunctionValue = (value, target, index, total, store) => { +export const getFunctionValue = (value, target, index, targets, store, prevTween) => { let func; if (isFnc(value)) { func = () => { - const computed = /** @type {Function} */(value)(target, index, total); + const computed = /** @type {Function} */(value)(target, index, targets, prevTween); // Fallback to 0 if the function returns undefined / NaN / null / false / 0 return !isNaN(+computed) ? +computed : computed || 0; } @@ -130,9 +136,17 @@ const getCSSValue = (target, propName, animationInlineStyles) => { */ export const getOriginalAnimatableValue = (target, propName, tweenType, animationInlineStyles) => { const type = !isUnd(tweenType) ? tweenType : getTweenType(target, propName); - return type === tweenTypes.OBJECT ? target[propName] || 0 : - type === tweenTypes.ATTRIBUTE ? /** @type {DOMTarget} */(target).getAttribute(propName) : - type === tweenTypes.TRANSFORM ? parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : + if (type === tweenTypes.OBJECT) { + const value = target[propName]; + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value || 0; + } + if (type === tweenTypes.ATTRIBUTE) { + const value = /** @type {DOMTarget} */(target).getAttribute(propName); + if (value && animationInlineStyles) animationInlineStyles[propName] = value; + return value; + } + return type === tweenTypes.TRANSFORM ? parseInlineTransforms(/** @type {DOMTarget} */(target), propName, animationInlineStyles) : type === tweenTypes.CSS_VAR ? getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles).trimStart() : getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles); } @@ -233,3 +247,51 @@ export const decomposeTweenValue = (tween, targetObject) => { } export const decomposedOriginalValue = createDecomposedValueTargetObject(); + +/** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ +export const composeColorValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const r = round(clamp(/** @type {Number} */(mod(lerp(fn[0], tn[0], progress))), 0, 255), 0); + const g = round(clamp(/** @type {Number} */(mod(lerp(fn[1], tn[1], progress))), 0, 255), 0); + const b = round(clamp(/** @type {Number} */(mod(lerp(fn[2], tn[2], progress))), 0, 255), 0); + const a = clamp(/** @type {Number} */(mod(round(lerp(fn[3], tn[3], progress), precision))), 0, 1); + if (tween._composition !== compositionTypes.none) { + const ns = tween._numbers; + ns[0] = r; + ns[1] = g; + ns[2] = b; + ns[3] = a; + } + return `rgba(${r},${g},${b},${a})`; +} + +/** + * @param {Tween} tween + * @param {Number} progress + * @param {Number} precision + * @return {String} + */ +export const composeComplexValue = (tween, progress, precision) => { + const mod = tween._modifier; + const fn = tween._fromNumbers; + const tn = tween._toNumbers; + const ts = tween._strings; + const hasComposition = tween._composition !== compositionTypes.none; + let v = ts[0]; + for (let j = 0, l = tn.length; j < l; j++) { + const n = /** @type {Number} */(mod(round(lerp(fn[j], tn[j], progress), precision))); + const s = ts[j + 1]; + v += `${s ? n + s : n}`; + if (hasComposition) { + tween._numbers[j] = n; + } + } + return v; +} diff --git a/src/engine/engine.js b/src/engine/engine.js index 4bcbe429c..be23ef3f8 100644 --- a/src/engine/engine.js +++ b/src/engine/engine.js @@ -92,7 +92,7 @@ class Engine extends Clock { wake() { if (this.useDefaultMainLoop && !this.reqId) { - // Imediatly request a tick to update engine._elapsedTime and get accurate offsetPosition calculation in timer.js + // Imediatly request a tick to update engine._lastTickTime and get accurate offsetPosition calculation in timer.js this.requestTick(now()); this.reqId = engineTickMethod(tickEngine); } diff --git a/src/events/scroll.js b/src/events/scroll.js index 951a00bf7..4d6ad5cba 100644 --- a/src/events/scroll.js +++ b/src/events/scroll.js @@ -265,6 +265,7 @@ class ScrollContainer { this.updateBounds(); forEachChildren(this, (/** @type {ScrollObserver} */child) => { child.refresh(); + child.onResize(child); if (child._debug) { child.debug(); } @@ -476,6 +477,8 @@ export class ScrollObserver { /** @type {Callback} */ this.onUpdate = parameters.onUpdate || noop; /** @type {Callback} */ + this.onResize = parameters.onResize || noop; + /** @type {Callback} */ this.onSyncComplete = parameters.onSyncComplete || noop; /** @type {Boolean} */ this.reverted = false; @@ -539,7 +542,9 @@ export class ScrollObserver { linked.pause(); this.linked = linked; // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. - if (!isUnd(/** @type {WAAPIAnimation} */(linked))) /** @type {WAAPIAnimation} */(linked).persist = true; + if (!isUnd(linked) && !isUnd(/** @type {WAAPIAnimation} */(linked).persist)) { + /** @type {WAAPIAnimation} */(linked).persist = true; + } // Try to use a target of the linked object if no target parameters specified if (!this._params.target) { /** @type {HTMLElement} */ @@ -741,12 +746,11 @@ export class ScrollObserver { // let offsetX = 0; // let offsetY = 0; // let $offsetParent = $el; - /** @type {Element} */ if (linked) { linkedTime = linked.currentTime; linked.seek(0, true); } - /* Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 + // Old implementation to get offset and targetSize before fixing https://github.com/juliangarnier/anime/issues/1021 // const isContainerStatic = get(container.element, 'position') === 'static' ? set(container.element, { position: 'relative '}) : false; // while ($el && $el !== container.element && $el !== doc.body) { // const isSticky = get($el, 'position') === 'sticky' ? diff --git a/src/index.js b/src/index.js index f331d7092..c4d32c983 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ export * from './scope/index.js'; export * from './events/index.js'; export * from './engine/index.js'; export * from './easings/index.js'; +export * from './layout/index.js'; export * as easings from './easings/index.js'; export * from './utils/index.js'; export * as utils from './utils/index.js'; @@ -15,4 +16,5 @@ export * as svg from './svg/index.js'; export * from './text/index.js'; export * as text from './text/index.js'; export * from './waapi/index.js'; -export * from './types/index.js'; \ No newline at end of file +export * from './types/index.js'; +export { globals } from './core/globals.js'; \ No newline at end of file diff --git a/src/layout/index.js b/src/layout/index.js new file mode 100644 index 000000000..23780baf7 --- /dev/null +++ b/src/layout/index.js @@ -0,0 +1 @@ +export * from './layout.js'; \ No newline at end of file diff --git a/src/layout/layout.js b/src/layout/layout.js new file mode 100644 index 000000000..5cd83b326 --- /dev/null +++ b/src/layout/layout.js @@ -0,0 +1,1614 @@ +import { + isStr, + isArr, + isUnd, + isFnc, + isSvg, + mergeObjects, +} from '../core/helpers.js'; + +import { + registerTargets, +} from '../core/targets.js'; + +import { + parseEase, +} from '../easings/eases/parser.js'; + +import { + getFunctionValue, + setValue, +} from '../core/values.js'; + +import { + createTimeline, +} from '../timeline/timeline.js'; + +import { + waapi, +} from '../waapi/waapi.js'; + +import { + globals, + defaults, + scope, +} from '../core/globals.js'; + +/** + * @import { + * AnimationParams, + * RenderableCallbacks, + * TickableCallbacks, + * TimelineParams, + * TimerParams, + * } from '../types/index.js' +*/ + +/** + * @import { + * ScrollObserver, + * } from '../events/scroll.js' +*/ + +/** + * @import { + * Timeline, + * } from '../timeline/timeline.js' +*/ + +/** + * @import { + * WAAPIAnimation + * } from '../waapi/waapi.js' +*/ + +/** + * @import { + * Spring, + } from '../easings/spring/index.js' +*/ + +/** + * @import { + * DOMTarget, + * DOMTargetSelector, + * FunctionValue, + * EasingParam, + } from '../types/index.js' +*/ + +/** + * @typedef {DOMTargetSelector|Array} LayoutChildrenParam + */ + +/** + * @typedef {Object} LayoutAnimationTimingsParams + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] + */ + +/** + * @typedef {Record} LayoutStateAnimationProperties + */ + +/** + * @typedef {LayoutStateAnimationProperties & LayoutAnimationTimingsParams} LayoutStateParams + */ + +/** + * @typedef {Object} LayoutSpecificAnimationParams + * @property {Number|String} [id] + * @property {Number|FunctionValue} [delay] + * @property {Number|FunctionValue} [duration] + * @property {EasingParam|FunctionValue} [ease] + * @property {EasingParam} [playbackEase] + * @property {LayoutStateParams} [swapAt] + * @property {LayoutStateParams} [enterFrom] + * @property {LayoutStateParams} [leaveTo] + */ + +/** + * @typedef {LayoutSpecificAnimationParams & TimerParams & TickableCallbacks & RenderableCallbacks} LayoutAnimationParams + */ + +/** + * @typedef {Object} LayoutOptions + * @property {LayoutChildrenParam} [children] + * @property {Array} [properties] + */ + +/** + * @typedef {LayoutAnimationParams & LayoutOptions} AutoLayoutParams + */ + +/** + * @typedef {Record & { + * transform: String, + * x: Number, + * y: Number, + * left: Number, + * top: Number, + * clientLeft: Number, + * clientTop: Number, + * width: Number, + * height: Number, + * }} LayoutNodeProperties + */ + +/** + * @typedef {Object} LayoutNode + * @property {String} id + * @property {DOMTarget} $el + * @property {Number} index + * @property {Array} targets + * @property {Number} delay + * @property {Number} duration + * @property {EasingParam} ease + * @property {DOMTarget} $measure + * @property {LayoutSnapshot} state + * @property {AutoLayout} layout + * @property {LayoutNode|null} parentNode + * @property {Boolean} isTarget + * @property {Boolean} isEntering + * @property {Boolean} isLeaving + * @property {Boolean} hasTransform + * @property {Array} inlineStyles + * @property {String|null} inlineTransforms + * @property {String|null} inlineTransition + * @property {Boolean} branchAdded + * @property {Boolean} branchRemoved + * @property {Boolean} branchNotRendered + * @property {Boolean} sizeChanged + * @property {Boolean} isInlined + * @property {Boolean} hasVisibilitySwap + * @property {Boolean} hasDisplayNone + * @property {Boolean} hasVisibilityHidden + * @property {String|null} measuredInlineTransform + * @property {String|null} measuredInlineTransition + * @property {String|null} measuredDisplay + * @property {String|null} measuredVisibility + * @property {String|null} measuredPosition + * @property {Boolean} measuredHasDisplayNone + * @property {Boolean} measuredHasVisibilityHidden + * @property {Boolean} measuredIsVisible + * @property {Boolean} measuredIsRemoved + * @property {Boolean} measuredIsInsideRoot + * @property {LayoutNodeProperties} properties + * @property {LayoutNode|null} _head + * @property {LayoutNode|null} _tail + * @property {LayoutNode|null} _prev + * @property {LayoutNode|null} _next + */ + +/** + * @callback LayoutNodeIterator + * @param {LayoutNode} node + * @param {Number} index + * @return {void} + */ + +let layoutId = 0; +let nodeId = 0; + +/** + * @param {DOMTarget} root + * @param {DOMTarget} $el + * @return {Boolean} + */ +const isElementInRoot = (root, $el) => { + if (!root || !$el) return false; + return root === $el || root.contains($el); +} + +/** + * @param {DOMTarget|null} $el + * @return {String|null} + */ +const muteElementTransition = $el => { + if (!$el) return null; + const style = $el.style; + const transition = style.transition || ''; + style.setProperty('transition', 'none', 'important'); + return transition; +} + +/** + * @param {DOMTarget|null} $el + * @param {String|null} transition + */ +const restoreElementTransition = ($el, transition) => { + if (!$el) return; + const style = $el.style; + if (transition) { + style.transition = transition; + } else { + style.removeProperty('transition'); + } +} + +/** + * @param {LayoutNode} node + */ +const muteNodeTransition = node => { + const store = node.layout.transitionMuteStore; + const $el = node.$el; + const $measure = node.$measure; + if ($el && !store.has($el)) store.set($el, muteElementTransition($el)); + if ($measure && !store.has($measure)) store.set($measure, muteElementTransition($measure)); +} + +/** + * @param {Map} store + */ +const restoreLayoutTransition = store => { + store.forEach((value, $el) => restoreElementTransition($el, value)); + store.clear(); +} + +const hiddenComputedStyle = /** @type {CSSStyleDeclaration} */({ + display: 'none', + visibility: 'hidden', + opacity: '0', + transform: 'none', + position: 'static', +}); + +/** + * @param {LayoutNode|null} node + */ +const detachNode = node => { + if (!node) return; + const parent = node.parentNode; + if (!parent) return; + if (parent._head === node) parent._head = node._next; + if (parent._tail === node) parent._tail = node._prev; + if (node._prev) node._prev._next = node._next; + if (node._next) node._next._prev = node._prev; + node._prev = null; + node._next = null; + node.parentNode = null; +} + +/** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @param {LayoutSnapshot} state + * @param {LayoutNode} recycledNode + * @return {LayoutNode} + */ +const createNode = ($el, parentNode, state, recycledNode) => { + let dataId = $el.dataset.layoutId; + if (!dataId) dataId = $el.dataset.layoutId = `node-${nodeId++}`; + const node = recycledNode ? recycledNode : /** @type {LayoutNode} */({}); + node.$el = $el; + node.$measure = $el; + node.id = dataId; + node.index = 0; + node.targets = null; + node.delay = 0; + node.duration = 0; + node.ease = null; + node.state = state; + node.layout = state.layout; + node.parentNode = parentNode || null; + node.isTarget = false; + node.isEntering = false; + node.isLeaving = false; + node.isInlined = false; + node.hasTransform = false; + node.inlineStyles = []; + node.inlineTransforms = null; + node.inlineTransition = null; + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.sizeChanged = false; + node.hasVisibilitySwap = false; + node.hasDisplayNone = false; + node.hasVisibilityHidden = false; + node.measuredInlineTransform = null; + node.measuredInlineTransition = null; + node.measuredDisplay = null; + node.measuredVisibility = null; + node.measuredPosition = null; + node.measuredHasDisplayNone = false; + node.measuredHasVisibilityHidden = false; + node.measuredIsVisible = false; + node.measuredIsRemoved = false; + node.measuredIsInsideRoot = false; + node.properties = /** @type {LayoutNodeProperties} */({ + transform: 'none', + x: 0, + y: 0, + left: 0, + top: 0, + clientLeft: 0, + clientTop: 0, + width: 0, + height: 0, + }); + node.layout.properties.forEach(prop => node.properties[prop] = 0); + node._head = null; + node._tail = null; + node._prev = null; + node._next = null; + return node; +} + +/** + * @param {LayoutNode} node + * @param {DOMTarget} $measure + * @param {CSSStyleDeclaration} computedStyle + * @param {Boolean} skipMeasurements + * @return {LayoutNode} + */ +const recordNodeState = (node, $measure, computedStyle, skipMeasurements) => { + const $el = node.$el; + const root = node.layout.root; + const isRoot = root === $el; + const properties = node.properties; + const rootNode = node.state.rootNode; + const parentNode = node.parentNode; + const computedTransforms = computedStyle.transform; + const inlineTransforms = $el.style.transform; + const parentNotRendered = parentNode ? parentNode.measuredIsRemoved : false; + const position = computedStyle.position; + if (isRoot) node.layout.absoluteCoords = position === 'fixed' || position === 'absolute'; + node.$measure = $measure; + node.inlineTransforms = inlineTransforms; + node.hasTransform = computedTransforms && computedTransforms !== 'none'; + node.measuredIsInsideRoot = isElementInRoot(root, $measure); + node.measuredInlineTransform = null; + node.measuredDisplay = computedStyle.display; + node.measuredVisibility = computedStyle.visibility; + node.measuredPosition = position; + node.measuredHasDisplayNone = computedStyle.display === 'none'; + node.measuredHasVisibilityHidden = computedStyle.visibility === 'hidden'; + node.measuredIsVisible = !(node.measuredHasDisplayNone || node.measuredHasVisibilityHidden); + node.measuredIsRemoved = node.measuredHasDisplayNone || node.measuredHasVisibilityHidden || parentNotRendered; + // Check if element has adjacent text that would reflow when taken out of flow + let hasAdjacentText = false; + let s = $el.previousSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.previousSibling; + if (s && s.nodeType === Node.TEXT_NODE) { + hasAdjacentText = true; + } else { + s = $el.nextSibling; + while (s && (s.nodeType === Node.COMMENT_NODE || (s.nodeType === Node.TEXT_NODE && !s.textContent.trim()))) s = s.nextSibling; + hasAdjacentText = s !== null && s.nodeType === Node.TEXT_NODE; + } + node.isInlined = hasAdjacentText; + + // Mute transforms (and transition to avoid triggering an animation) before the position calculation + if (node.hasTransform && !skipMeasurements) { + const transitionMuteStore = node.layout.transitionMuteStore; + if (!transitionMuteStore.get($el)) node.inlineTransition = muteElementTransition($el); + if ($measure === $el) { + $el.style.transform = 'none'; + } else { + if (!transitionMuteStore.get($measure)) node.measuredInlineTransition = muteElementTransition($measure); + node.measuredInlineTransform = $measure.style.transform; + $measure.style.transform = 'none'; + } + } + + let left = 0; + let top = 0; + let width = 0; + let height = 0; + + if (!skipMeasurements) { + const rect = $measure.getBoundingClientRect(); + left = rect.left; + top = rect.top; + width = rect.width; + height = rect.height; + } + + for (let name in properties) { + const computedProp = name === 'transform' ? computedTransforms : computedStyle[name] || (computedStyle.getPropertyValue && computedStyle.getPropertyValue(name)); + if (!isUnd(computedProp)) properties[name] = computedProp; + } + + properties.left = left; + properties.top = top; + properties.clientLeft = skipMeasurements ? 0 : $measure.clientLeft; + properties.clientTop = skipMeasurements ? 0 : $measure.clientTop; + // Compute local x/y relative to parent + let absoluteLeft, absoluteTop; + if (isRoot) { + if (!node.layout.absoluteCoords) { + absoluteLeft = 0; + absoluteTop = 0; + } else { + absoluteLeft = left; + absoluteTop = top; + } + } else { + const p = parentNode || rootNode; + const parentLeft = p.properties.left; + const parentTop = p.properties.top; + const borderLeft = p.properties.clientLeft; + const borderTop = p.properties.clientTop; + if (!node.layout.absoluteCoords) { + if (p === rootNode) { + const rootLeft = rootNode.properties.left; + const rootTop = rootNode.properties.top; + const rootBorderLeft = rootNode.properties.clientLeft; + const rootBorderTop = rootNode.properties.clientTop; + absoluteLeft = left - rootLeft - rootBorderLeft; + absoluteTop = top - rootTop - rootBorderTop; + } else { + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; + } + } else { + absoluteLeft = left - parentLeft - borderLeft; + absoluteTop = top - parentTop - borderTop; + } + } + properties.x = absoluteLeft; + properties.y = absoluteTop; + properties.width = width; + properties.height = height; + return node; +} + +/** + * @param {LayoutNode} node + * @param {LayoutStateAnimationProperties} [props] + */ +const updateNodeProperties = (node, props) => { + if (!props) return; + for (let name in props) { + if (name === '__proto__' || name === 'constructor' || name === 'prototype') continue; + node.properties[name] = props[name]; + } +} + +/** + * @param {LayoutNode} node + * @param {LayoutAnimationTimingsParams} params + */ +const updateNodeTimingParams = (node, params) => { + const easeFunctionResult = getFunctionValue(params.ease, node.$el, node.index, node.targets, null, null); + const keyEasing = isFnc(easeFunctionResult) ? easeFunctionResult : params.ease; + const hasSpring = !isUnd(keyEasing) && !isUnd(/** @type {Spring} */(keyEasing).ease); + node.ease = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing; + node.duration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(params.duration, node.$el, node.index, node.targets, null, null); + node.delay = getFunctionValue(params.delay, node.$el, node.index, node.targets, null, null); +} + +/** + * @param {LayoutNode} node + */ +const recordNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + stylesStore.length = 0; + node.layout.recordedProperties.forEach(prop => { + stylesStore.push(prop, style[prop] || ''); + }); +} + +/** + * @param {LayoutNode} node + */ +const restoreNodeInlineStyles = node => { + const style = node.$el.style; + const stylesStore = node.inlineStyles; + for (let i = 0, l = stylesStore.length; i < l; i += 2) { + const property = stylesStore[i]; + const styleValue = stylesStore[i + 1]; + if (styleValue && styleValue !== '') { + style[property] = styleValue; + } else { + style[property] = ''; + style.removeProperty(property); + } + } +} + +/** + * @param {LayoutNode} node + */ +const restoreNodeTransform = node => { + const inlineTransforms = node.inlineTransforms; + const nodeStyle = node.$el.style; + if (!node.hasTransform || !inlineTransforms || (node.hasTransform && nodeStyle.transform === 'none') || (inlineTransforms && inlineTransforms === 'none')) { + nodeStyle.removeProperty('transform'); + } else if (inlineTransforms) { + nodeStyle.transform = inlineTransforms; + } + const $measure = node.$measure; + if (node.hasTransform && $measure !== node.$el) { + const measuredStyle = $measure.style; + const measuredInline = node.measuredInlineTransform; + if (measuredInline && measuredInline !== '') { + measuredStyle.transform = measuredInline; + } else { + measuredStyle.removeProperty('transform'); + } + } + node.measuredInlineTransform = null; + if (node.inlineTransition !== null) { + restoreElementTransition(node.$el, node.inlineTransition); + node.inlineTransition = null; + } + if ($measure !== node.$el && node.measuredInlineTransition !== null) { + restoreElementTransition($measure, node.measuredInlineTransition); + node.measuredInlineTransition = null; + } +} + +/** + * @param {LayoutNode} node + */ +const restoreNodeVisualState = node => { + if (node.measuredIsRemoved || node.hasVisibilitySwap) { + node.$el.style.removeProperty('display'); + node.$el.style.removeProperty('visibility'); + if (node.hasVisibilitySwap) { + node.$measure.style.removeProperty('display'); + node.$measure.style.removeProperty('visibility'); + } + } + // if (node.measuredIsRemoved) { + node.layout.pendingRemoval.delete(node.$el); + // } +} + +/** + * @param {LayoutNode} node + * @param {LayoutNode} targetNode + * @param {LayoutSnapshot} newState + * @return {LayoutNode} + */ +const cloneNodeProperties = (node, targetNode, newState) => { + targetNode.properties = /** @type {LayoutNodeProperties} */({ ...node.properties }); + targetNode.state = newState; + targetNode.isTarget = node.isTarget; + targetNode.hasTransform = node.hasTransform; + targetNode.inlineTransforms = node.inlineTransforms; + targetNode.measuredIsVisible = node.measuredIsVisible; + targetNode.measuredDisplay = node.measuredDisplay; + targetNode.measuredIsRemoved = node.measuredIsRemoved; + targetNode.measuredHasDisplayNone = node.measuredHasDisplayNone; + targetNode.measuredHasVisibilityHidden = node.measuredHasVisibilityHidden; + targetNode.hasDisplayNone = node.hasDisplayNone; + targetNode.isInlined = node.isInlined; + targetNode.hasVisibilityHidden = node.hasVisibilityHidden; + return targetNode; +} + +class LayoutSnapshot { + /** + * @param {AutoLayout} layout + */ + constructor(layout) { + /** @type {AutoLayout} */ + this.layout = layout; + /** @type {LayoutNode|null} */ + this.rootNode = null; + /** @type {Set} */ + this.rootNodes = new Set(); + /** @type {Map} */ + this.nodes = new Map(); + /** @type {Number} */ + this.scrollX = 0; + /** @type {Number} */ + this.scrollY = 0; + } + + /** + * @return {this} + */ + revert() { + this.forEachNode(node => { + this.layout.pendingRemoval.delete(node.$el); + node.$el.removeAttribute('data-layout-id'); + node.$measure.removeAttribute('data-layout-id'); + }); + this.rootNode = null; + this.rootNodes.clear(); + this.nodes.clear(); + return this; + } + + /** + * @param {DOMTarget} $el + * @return {LayoutNode} + */ + getNode($el) { + if (!$el || !$el.dataset) return; + return this.nodes.get($el.dataset.layoutId); + } + + /** + * @param {DOMTarget} $el + * @param {String} prop + * @return {Number|String} + */ + getComputedValue($el, prop) { + const node = this.getNode($el); + if (!node) return; + return /** @type {Number|String} */(node.properties[prop]); + } + + /** + * @param {LayoutNode|null} rootNode + * @param {LayoutNodeIterator} cb + */ + forEach(rootNode, cb) { + let node = rootNode; + let i = 0; + while (node) { + cb(node, i++); + if (node._head) { + node = node._head; + } else if (node._next) { + node = node._next; + } else { + while (node && !node._next) { + node = node.parentNode; + } + if (node) node = node._next; + } + } + } + + /** + * @param {LayoutNodeIterator} cb + */ + forEachRootNode(cb) { + this.forEach(this.rootNode, cb); + } + + /** + * @param {LayoutNodeIterator} cb + */ + forEachNode(cb) { + for (const rootNode of this.rootNodes) { + this.forEach(rootNode, cb); + } + } + + /** + * @param {DOMTarget} $el + * @param {LayoutNode|null} parentNode + * @return {LayoutNode|null} + */ + registerElement($el, parentNode) { + if (!$el || $el.nodeType !== 1) return null; + + if (!this.layout.transitionMuteStore.has($el)) this.layout.transitionMuteStore.set($el, muteElementTransition($el)); + + /** @type {Array} */ + const stack = [$el, parentNode]; + const root = this.layout.root; + let firstNode = null; + + while (stack.length) { + /** @type {LayoutNode|null} */ + const $parent = /** @type {LayoutNode|null} */(stack.pop()); + /** @type {DOMTarget|null} */ + const $current = /** @type {DOMTarget|null} */(stack.pop()); + + if (!$current || $current.nodeType !== 1 || isSvg($current)) continue; + + const skipMeasurements = $parent ? $parent.measuredIsRemoved : false; + const computedStyle = skipMeasurements ? hiddenComputedStyle : getComputedStyle($current); + const hasDisplayNone = skipMeasurements ? true : computedStyle.display === 'none'; + const hasVisibilityHidden = skipMeasurements ? true : computedStyle.visibility === 'hidden'; + const isVisible = !hasDisplayNone && !hasVisibilityHidden; + const existingId = $current.dataset.layoutId; + const isInsideRoot = isElementInRoot(root, $current); + + let node = existingId ? this.nodes.get(existingId) : null; + + if (node && node.$el !== $current) { + const nodeInsideRoot = isElementInRoot(root, node.$el); + const measuredVisible = node.measuredIsVisible; + const shouldReassignNode = !nodeInsideRoot && (isInsideRoot || (!isInsideRoot && !measuredVisible && isVisible)); + const shouldReuseMeasurements = nodeInsideRoot && !measuredVisible && isVisible; + // Rebind nodes that move into the root or whose detached twin just became visible + if (shouldReassignNode) { + detachNode(node); + node = createNode($current, $parent, this, node); + // for hidden element with in-root sibling, keep the hidden node but borrow measurements from its visible in-root twin element + } else if (shouldReuseMeasurements) { + recordNodeState(node, $current, computedStyle, skipMeasurements); + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; + } + if (!firstNode) firstNode = node; + continue; + // No reassignment needed so keep walking descendants under the current parent + } else { + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), $parent); + $child = $child.previousElementSibling; + } + if (!firstNode) firstNode = node; + continue; + } + } else { + node = createNode($current, $parent, this, node); + } + + node.branchAdded = false; + node.branchRemoved = false; + node.branchNotRendered = false; + node.isTarget = false; + node.sizeChanged = false; + node.hasVisibilityHidden = hasVisibilityHidden; + node.hasDisplayNone = hasDisplayNone; + node.hasVisibilitySwap = (hasVisibilityHidden && !node.measuredHasVisibilityHidden) || (hasDisplayNone && !node.measuredHasDisplayNone); + + this.nodes.set(node.id, node); + + node.parentNode = $parent || null; + node._prev = null; + node._next = null; + + if ($parent) { + this.rootNodes.delete(node); + if (!$parent._head) { + $parent._head = node; + $parent._tail = node; + } else { + $parent._tail._next = node; + node._prev = $parent._tail; + $parent._tail = node; + } + } else { + // Each disconnected subtree becomes its own root in the snapshot graph + this.rootNodes.add(node); + } + + recordNodeState(node, node.$el, computedStyle, skipMeasurements); + + let $child = $current.lastElementChild; + while ($child) { + stack.push(/** @type {DOMTarget} */($child), node); + $child = $child.previousElementSibling; + } + + if (!firstNode) firstNode = node; + } + + return firstNode; + } + + /** + * @param {DOMTarget} $el + * @param {Set} candidates + * @return {LayoutNode|null} + */ + ensureDetachedNode($el, candidates) { + if (!$el || $el === this.layout.root) return null; + const existingId = $el.dataset.layoutId; + const existingNode = existingId ? this.nodes.get(existingId) : null; + if (existingNode && existingNode.$el === $el) return existingNode; + let parentNode = null; + let $ancestor = $el.parentElement; + while ($ancestor && $ancestor !== this.layout.root) { + if (candidates.has($ancestor)) { + parentNode = this.ensureDetachedNode($ancestor, candidates); + break; + } + $ancestor = $ancestor.parentElement; + } + return this.registerElement($el, parentNode); + } + + /** + * @return {this} + */ + record() { + const layout = this.layout; + const children = layout.children; + const root = layout.root; + const toParse = isArr(children) ? children : [children]; + const scoped = []; + const scopeRoot = children === '*' ? root : scope.root; + + // Mute transition and transforms of root ancestors before recording the state + + /** @type {Array} */ + const rootAncestorTransformStore = []; + let $ancestor = root.parentElement; + while ($ancestor && $ancestor.nodeType === 1) { + const computedStyle = getComputedStyle($ancestor); + if (computedStyle.transform && computedStyle.transform !== 'none') { + const inlineTransform = $ancestor.style.transform || ''; + const inlineTransition = muteElementTransition($ancestor); + rootAncestorTransformStore.push($ancestor, inlineTransform, inlineTransition); + $ancestor.style.transform = 'none'; + } + $ancestor = $ancestor.parentElement; + } + + for (let i = 0, l = toParse.length; i < l; i++) { + const child = toParse[i]; + scoped[i] = isStr(child) ? scopeRoot.querySelectorAll(child) : child; + } + + const parsedChildren = registerTargets(scoped); + + this.nodes.clear(); + this.rootNodes.clear(); + + const rootNode = this.registerElement(root, null); + // Root node are always targets + rootNode.isTarget = true; + this.rootNode = rootNode; + + const inRootNodeIds = new Set(); + // Update index and total for inital timing calculation + let index = 0; + const allNodeTargets = []; + this.nodes.forEach((node) => { allNodeTargets.push(node.$el); }); + this.nodes.forEach((node, id) => { + node.index = index++; + node.targets = allNodeTargets; + // Track ids of nodes that belong to the current root to filter detached matches + if (node && node.measuredIsInsideRoot) { + inRootNodeIds.add(id); + } + }); + + // Elements with a layout id outside the root that match the children selector + const detachedElementsLookup = new Set(); + const orderedDetachedElements = []; + + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + if (!$el || $el.nodeType !== 1 || $el === root) continue; + const insideRoot = isElementInRoot(root, $el); + if (!insideRoot) { + const layoutNodeId = $el.dataset.layoutId; + if (!layoutNodeId || !inRootNodeIds.has(layoutNodeId)) continue; + } + if (!detachedElementsLookup.has($el)) { + detachedElementsLookup.add($el); + orderedDetachedElements.push($el); + } + } + + for (let i = 0, l = orderedDetachedElements.length; i < l; i++) { + this.ensureDetachedNode(orderedDetachedElements[i], detachedElementsLookup); + } + + for (let i = 0, l = parsedChildren.length; i < l; i++) { + const $el = parsedChildren[i]; + const node = this.getNode($el); + if (node) { + let cur = node; + while (cur) { + if (cur.isTarget) break; + cur.isTarget = true; + cur = cur.parentNode; + } + } + } + + this.scrollX = window.scrollX; + this.scrollY = window.scrollY; + + this.forEachNode(restoreNodeTransform); + + // Restore transition and transforms of root ancestors + + for (let i = 0, l = rootAncestorTransformStore.length; i < l; i += 3) { + const $el = /** @type {DOMTarget} */(rootAncestorTransformStore[i]); + const inlineTransform = /** @type {String} */(rootAncestorTransformStore[i + 1]); + const inlineTransition = /** @type {String|null} */(rootAncestorTransformStore[i + 2]); + if (inlineTransform && inlineTransform !== '') { + $el.style.transform = inlineTransform; + } else { + $el.style.removeProperty('transform'); + } + restoreElementTransition($el, inlineTransition); + } + + return this; + } +} + +/** + * @param {LayoutStateParams} params + * @return {[LayoutStateAnimationProperties, LayoutAnimationTimingsParams]} + */ +function splitPropertiesFromParams(params) { + /** @type {LayoutStateAnimationProperties} */ + const properties = {}; + /** @type {LayoutAnimationTimingsParams} */ + const parameters = {}; + for (let name in params) { + if (name === '__proto__' || name === 'constructor' || name === 'prototype') continue; + const value = params[name]; + const isEase = name === 'ease'; + const isTiming = name === 'duration' || name === 'delay'; + if (isTiming || isEase) { + if (isEase) { + parameters[name] = /** @type {EasingParam} */(value); + } else { + parameters[name] = /** @type {Number|FunctionValue} */(value); + } + } else { + properties[name] = /** @type {Number|String} */(value); + } + } + return [properties, parameters]; +} + +export class AutoLayout { + /** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + */ + constructor(root, params = {}) { + if (scope.current) scope.current.register(this); + const swapAtSplitParams = splitPropertiesFromParams(params.swapAt); + const enterFromSplitParams = splitPropertiesFromParams(params.enterFrom); + const leaveToSplitParams = splitPropertiesFromParams(params.leaveTo); + const transitionProperties = params.properties; + /** @type {Number|FunctionValue} */ + params.duration = setValue(params.duration, 350); + /** @type {Number|FunctionValue} */ + params.delay = setValue(params.delay, 0); + /** @type {EasingParam|FunctionValue} */ + params.ease = setValue(params.ease, 'inOut(3.5)'); + /** @type {AutoLayoutParams} */ + this.params = params; + /** @type {DOMTarget} */ + this.root = /** @type {DOMTarget} */(registerTargets(root)[0]); + /** @type {Number|String} */ + this.id = params.id || layoutId++; + /** @type {LayoutChildrenParam} */ + this.children = params.children || '*'; + /** @type {Boolean} */ + this.absoluteCoords = false; + /** @type {LayoutStateParams} */ + this.swapAtParams = mergeObjects(params.swapAt || { opacity: 0 }, { ease: 'inOut(1.75)' }); + /** @type {LayoutStateParams} */ + this.enterFromParams = params.enterFrom || { opacity: 0 }; + /** @type {LayoutStateParams} */ + this.leaveToParams = params.leaveTo || { opacity: 0 }; + /** @type {Set} */ + this.properties = new Set([ + 'opacity', + 'fontSize', + 'color', + 'backgroundColor', + 'borderRadius', + 'border', + 'filter', + 'clipPath', + ]); + if (swapAtSplitParams[0]) for (let name in swapAtSplitParams[0]) this.properties.add(name); + if (enterFromSplitParams[0]) for (let name in enterFromSplitParams[0]) this.properties.add(name); + if (leaveToSplitParams[0]) for (let name in leaveToSplitParams[0]) this.properties.add(name); + if (transitionProperties) for (let i = 0, l = transitionProperties.length; i < l; i++) this.properties.add(transitionProperties[i]); + /** @type {Set} */ + this.recordedProperties = new Set([ + 'display', + 'visibility', + 'translate', + 'position', + 'left', + 'top', + 'marginLeft', + 'marginTop', + 'width', + 'height', + 'maxWidth', + 'maxHeight', + 'minWidth', + 'minHeight', + ]); + this.properties.forEach(prop => this.recordedProperties.add(prop)); + /** @type {WeakSet} */ + this.pendingRemoval = new WeakSet(); + /** @type {Map} */ + this.transitionMuteStore = new Map(); + /** @type {LayoutSnapshot} */ + this.oldState = new LayoutSnapshot(this); + /** @type {LayoutSnapshot} */ + this.newState = new LayoutSnapshot(this); + /** @type {Timeline} */ + this.timeline = null; + /** @type {WAAPIAnimation} */ + this.transformAnimation = null; + /** @type {Array} */ + this.animating = []; + /** @type {Array} */ + this.swapping = []; + /** @type {Array} */ + this.leaving = []; + /** @type {Array} */ + this.entering = []; + // Record the current state as the old state to init the data attributes and allow imediate .animate() + this.oldState.record(); + // And all layout transition muted during the record + restoreLayoutTransition(this.transitionMuteStore); + } + + /** + * @return {this} + */ + revert() { + this.root.classList.remove('is-animated'); + if (this.timeline) { + this.timeline.complete(); + this.timeline = null; + } + if (this.transformAnimation) { + this.transformAnimation.complete(); + this.transformAnimation = null; + } + this.animating.length = this.swapping.length = this.leaving.length = this.entering.length = 0; + this.oldState.revert(); + this.newState.revert(); + requestAnimationFrame(() => restoreLayoutTransition(this.transitionMuteStore)); + return this; + } + + /** + * @return {this} + */ + record() { + // Commit transforms before measuring + if (this.transformAnimation) { + this.transformAnimation.cancel(); + this.transformAnimation = null; + } + // Record the old state + this.oldState.record(); + // Cancel any running timeline + if (this.timeline) { + this.timeline.cancel(); + this.timeline = null; + } + // Restore previously captured inline styles + this.newState.forEachRootNode(restoreNodeInlineStyles); + return this; + } + + /** + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + animate(params = {}) { + /** @type { LayoutAnimationTimingsParams } */ + const animationTimings = { + ease: setValue(params.ease, this.params.ease), + delay: setValue(params.delay, this.params.delay), + duration: setValue(params.duration, this.params.duration), + } + /** @type {TimelineParams} */ + const tlParams = { + id: this.id + } + const onComplete = setValue(params.onComplete, this.params.onComplete); + const onPause = setValue(params.onPause, this.params.onPause); + for (let name in defaults) { + if (name !== 'ease' && name !== 'duration' && name !== 'delay') { + if (!isUnd(params[name])) { + tlParams[name] = params[name]; + } else if (!isUnd(this.params[name])) { + tlParams[name] = this.params[name]; + } + } + } + tlParams.onComplete = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const ed = globals.editor; + const isScrollControled = (ap && ap.linked) || (ed && ed.showPanel); + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + return; + } + // Make sure to call .cancel() after restoreNodeInlineStyles(node); otehrwise the commited styles get reverted + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(node => { + restoreNodeVisualState(node); + restoreNodeInlineStyles(node); + }); + for (let i = 0, l = transformed.length; i < l; i++) { + const $el = transformed[i]; + $el.style.transform = newState.getComputedValue($el, 'transform'); + } + if (this.root.classList.contains('is-animated')) { + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + } + // Avoid CSS transitions at the end of the animation by restoring them on the next frame + requestAnimationFrame(() => { + if (this.root.classList.contains('is-animated')) return; + restoreLayoutTransition(this.transitionMuteStore); + }); + }; + tlParams.onPause = () => { + const ap = /** @type {ScrollObserver} */(params.autoplay); + const isScrollControled = ap && ap.linked; + if (isScrollControled) { + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + return; + } + if (!this.root.classList.contains('is-animated')) return; + if (this.transformAnimation) this.transformAnimation.cancel(); + newState.forEachRootNode(restoreNodeVisualState); + this.root.classList.remove('is-animated'); + if (onComplete) onComplete(this.timeline); + if (onPause) onPause(this.timeline); + }; + tlParams.composition = false; + + const swapAtParams = mergeObjects(mergeObjects(params.swapAt || {}, this.swapAtParams), animationTimings); + const enterFromParams = mergeObjects(mergeObjects(params.enterFrom || {}, this.enterFromParams), animationTimings); + const leaveToParams = mergeObjects(mergeObjects(params.leaveTo || {}, this.leaveToParams), animationTimings); + const [ swapAtProps, swapAtTimings ] = splitPropertiesFromParams(swapAtParams); + const [ enterFromProps, enterFromTimings ] = splitPropertiesFromParams(enterFromParams); + const [ leaveToProps, leaveToTimings ] = splitPropertiesFromParams(leaveToParams); + + const oldState = this.oldState; + const newState = this.newState; + const animating = this.animating; + const swapping = this.swapping; + const entering = this.entering; + const leaving = this.leaving; + const pendingRemoval = this.pendingRemoval; + + animating.length = swapping.length = entering.length = leaving.length = 0; + + // Mute old state CSS transitions to prevent wrong properties calculation + oldState.forEachRootNode(muteNodeTransition); + // Capture the new state before animation + newState.record(); + newState.forEachRootNode(recordNodeInlineStyles); + + const targets = []; + const animated = []; + const transformed = []; + const animatedSwap = []; + const rootNode = newState.rootNode; + const $root = rootNode.$el; + + newState.forEachRootNode(node => { + + const $el = node.$el; + const id = node.id; + const parent = node.parentNode; + const parentAdded = parent ? parent.branchAdded : false; + const parentRemoved = parent ? parent.branchRemoved : false; + const parentNotRendered = parent ? parent.branchNotRendered : false; + + let oldStateNode = oldState.nodes.get(id); + + const hasNoOldState = !oldStateNode; + + if (hasNoOldState) { + oldStateNode = cloneNodeProperties(node, /** @type {LayoutNode} */({}), oldState); + oldState.nodes.set(id, oldStateNode); + oldStateNode.measuredIsRemoved = true; + } else if (oldStateNode.measuredIsRemoved && !node.measuredIsRemoved) { + cloneNodeProperties(node, oldStateNode, oldState); + oldStateNode.measuredIsRemoved = true; + } + + const oldParentNode = oldStateNode.parentNode; + const oldParentId = oldParentNode ? oldParentNode.id : null; + const newParentId = parent ? parent.id : null; + const parentChanged = oldParentId !== newParentId; + const elementChanged = oldStateNode.$el !== node.$el; + const wasRemovedBefore = oldStateNode.measuredIsRemoved; + const isRemovedNow = node.measuredIsRemoved; + + // Recalculate postion relative to their parent for elements that have been moved + if (!oldStateNode.measuredIsRemoved && !isRemovedNow && !hasNoOldState && (parentChanged || elementChanged)) { + const oldAbsoluteLeft = oldStateNode.properties.left; + const oldAbsoluteTop = oldStateNode.properties.top; + const newParent = parent || newState.rootNode; + const oldParent = newParent.id ? oldState.nodes.get(newParent.id) : null; + const parentLeft = oldParent ? oldParent.properties.left : newParent.properties.left; + const parentTop = oldParent ? oldParent.properties.top : newParent.properties.top; + const borderLeft = oldParent ? oldParent.properties.clientLeft : newParent.properties.clientLeft; + const borderTop = oldParent ? oldParent.properties.clientTop : newParent.properties.clientTop; + oldStateNode.properties.x = oldAbsoluteLeft - parentLeft - borderLeft; + oldStateNode.properties.y = oldAbsoluteTop - parentTop - borderTop; + } + + if (node.hasVisibilitySwap) { + if (node.hasVisibilityHidden) { + node.$el.style.visibility = 'visible'; + node.$measure.style.visibility = 'hidden'; + } + if (node.hasDisplayNone) { + node.$el.style.display = oldStateNode.measuredDisplay || node.measuredDisplay || ''; + // Setting visibility 'hidden' instead of display none to avoid calculation issues + node.$measure.style.visibility = 'hidden'; + // @TODO: check why setting display here can cause calculation issues + // node.$measure.style.display = 'none'; + } + } + + const wasPendingRemoval = pendingRemoval.has($el); + const wasVisibleBefore = oldStateNode.measuredIsVisible; + const isVisibleNow = node.measuredIsVisible; + const becomeVisible = !wasVisibleBefore && isVisibleNow && !parentNotRendered; + const topLevelAdded = !isRemovedNow && (wasRemovedBefore || wasPendingRemoval) && !parentAdded; + const newlyRemoved = isRemovedNow && !wasRemovedBefore && !parentRemoved; + const topLevelRemoved = newlyRemoved || isRemovedNow && wasPendingRemoval && !parentRemoved; + + node.branchAdded = parentAdded || topLevelAdded; + node.branchRemoved = parentRemoved || topLevelRemoved; + node.branchNotRendered = parentNotRendered || isRemovedNow; + + if (isRemovedNow && wasVisibleBefore) { + node.$el.style.display = oldStateNode.measuredDisplay; + node.$el.style.visibility = 'visible'; + cloneNodeProperties(oldStateNode, node, newState); + } + + // Node is leaving + if (newlyRemoved) { + if (node.isTarget) { + leaving.push($el); + node.isLeaving = true; + } + pendingRemoval.add($el); + } else if (!isRemovedNow && wasPendingRemoval) { + pendingRemoval.delete($el); + } + + // Node is entering + if ((topLevelAdded && !parentNotRendered) || becomeVisible) { + updateNodeProperties(oldStateNode, enterFromProps); + if (node.isTarget) { + entering.push($el); + node.isEntering = true; + } + // Node is leaving + } else if (topLevelRemoved && !parentNotRendered) { + updateNodeProperties(node, leaveToProps); + } + + // Node is animating + // The animating array is used only to calculate delays and duration on root children + if (node !== rootNode && node.isTarget && !node.isEntering && !node.isLeaving) { + animating.push($el); + } + + targets.push($el); + + }); + + let enteringIndex = 0; + let leavingIndex = 0; + let animatingIndex = 0; + + newState.forEachRootNode(node => { + + const $el = node.$el; + const parent = node.parentNode; + const oldStateNode = oldState.nodes.get(node.id); + const nodeProperties = node.properties; + const oldStateNodeProperties = oldStateNode.properties; + + // Use closest animated parent index and total values so that children staggered delays are in sync with their parent + let animatedParent = parent !== rootNode && parent; + while (animatedParent && !animatedParent.isTarget && animatedParent !== rootNode) { + animatedParent = animatedParent.parentNode; + } + + // Root is always animated first in sync with the first child (animating.length is the total of children) + if (node === rootNode) { + node.index = 0; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); + } else if (node.isEntering) { + node.index = animatedParent ? animatedParent.index : enteringIndex; + node.targets = animatedParent ? animating : entering; + updateNodeTimingParams(node, enterFromTimings); + enteringIndex++; + } else if (node.isLeaving) { + node.index = animatedParent ? animatedParent.index : leavingIndex; + node.targets = animatedParent ? animating : leaving; + leavingIndex++; + updateNodeTimingParams(node, leaveToTimings); + } else if (node.isTarget) { + node.index = animatingIndex++; + node.targets = animating; + updateNodeTimingParams(node, animationTimings); + } else { + node.index = animatedParent ? animatedParent.index : 0; + node.targets = animating; + updateNodeTimingParams(node, swapAtTimings); + } + + // Make sure the old state node has its inex and total values up to date for valid "from" function values calculation + oldStateNode.index = node.index; + oldStateNode.targets = node.targets; + + // Computes all values up front so we can check for changes and we don't have to re-compute them inside the animation props + for (let prop in nodeProperties) { + nodeProperties[prop] = getFunctionValue(nodeProperties[prop], $el, node.index, node.targets, null, null); + oldStateNodeProperties[prop] = getFunctionValue(oldStateNodeProperties[prop], $el, oldStateNode.index, oldStateNode.targets, null, null); + } + + // Use a 1px tolerance to detect dimensions changes to prevent width / height animations on barelly visible elements + const sizeTolerance = 1; + const widthChanged = Math.abs(nodeProperties.width - oldStateNodeProperties.width) > sizeTolerance; + const heightChanged = Math.abs(nodeProperties.height - oldStateNodeProperties.height) > sizeTolerance; + + node.sizeChanged = (widthChanged || heightChanged); + + // const hiddenStateChanged = (topLevelAdded || newlyRemoved) && wasRemovedBefore !== isRemovedNow; + + if (node.isTarget && (!node.measuredIsRemoved && oldStateNode.measuredIsVisible || node.measuredIsRemoved && node.measuredIsVisible)) { + if (nodeProperties.transform !== 'none' || oldStateNodeProperties.transform !== 'none') { + node.hasTransform = true; + transformed.push($el); + } + for (let prop in nodeProperties) { + // if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop] || hiddenStateChanged)) { + if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop])) { + animated.push($el); + break; + } + } + } + + if (!node.isTarget) { + swapping.push($el); + if (node.sizeChanged && parent && parent.isTarget && parent.sizeChanged) { + if (swapAtProps.transform) { + node.hasTransform = true; + transformed.push($el); + } + animatedSwap.push($el); + } + } + + }); + + const timingParams = { + delay: (/** @type {HTMLElement} */$el) => newState.getNode($el).delay, + duration: (/** @type {HTMLElement} */$el) => newState.getNode($el).duration, + ease: (/** @type {HTMLElement} */$el) => newState.getNode($el).ease, + } + + tlParams.defaults = timingParams; + + this.timeline = createTimeline(tlParams); + + // Imediatly return the timeline if no layout changes detected + if (!animated.length && !transformed.length && !swapping.length) { + // Make sure to restore all CSS transition if no animation + restoreLayoutTransition(this.transitionMuteStore); + return this.timeline.complete(); + } + + if (targets.length) { + + this.root.classList.add('is-animated'); + + for (let i = 0, l = targets.length; i < l; i++) { + const $el = targets[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; + + // muteNodeTransition(newNode); + + // Don't animate positions of inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + // Display grid can mess with the absolute positioning, so set it to block during transition + if (oldNode.measuredDisplay === 'grid' || newNode.measuredDisplay === 'grid') $el.style.setProperty('display', 'block', 'important'); + // All children must be in position absolute or fixed + if ($el !== $root || this.absoluteCoords) { + $el.style.position = this.absoluteCoords ? 'fixed' : 'absolute'; + $el.style.left = '0px'; + $el.style.top = '0px'; + $el.style.marginLeft = '0px'; + $el.style.marginTop = '0px'; + $el.style.translate = `${oldNodeState.x}px ${oldNodeState.y}px`; + } + if ($el === $root && newNode.measuredPosition === 'static') { + $el.style.position = 'relative'; + // Cancel left / trop in case the static element had muted values now activated by potision relative + $el.style.left = '0px'; + $el.style.top = '0px'; + } + } + // Animate dimensions for all elements (including inlined) + $el.style.width = `${oldNodeState.width}px`; + $el.style.height = `${oldNodeState.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + } + + // Restore the scroll position if the oldState differs from the current state + if (oldState.scrollX !== window.scrollX || oldState.scrollY !== window.scrollY) { + // Restoring in the next frame avoids race conditions if for example a waapi animation commit styles that affect the root height + requestAnimationFrame(() => window.scrollTo(oldState.scrollX, oldState.scrollY)); + } + + for (let i = 0, l = animated.length; i < l; i++) { + const $el = animated[i]; + const id = $el.dataset.layoutId; + const oldNode = oldState.nodes.get(id); + const newNode = newState.nodes.get(id); + const oldNodeState = oldNode.properties; + const newNodeState = newNode.properties; + let nodeHasChanged = false; + /** @type {AnimationParams} */ + const animatedProps = { + composition: 'none', + } + if (oldNodeState.width !== newNodeState.width) { + animatedProps.width = [oldNodeState.width, newNodeState.width]; + nodeHasChanged = true; + } + if (oldNodeState.height !== newNodeState.height) { + animatedProps.height = [oldNodeState.height, newNodeState.height]; + nodeHasChanged = true; + } + // If the node has transforms we handle the translate animation in waapi otherwise translate and other transforms can be out of sync + // And we don't animate the position of inlined elements + if (!newNode.hasTransform && !newNode.isInlined) { + animatedProps.translate = [`${oldNodeState.x}px ${oldNodeState.y}px`, `${newNodeState.x}px ${newNodeState.y}px`]; + nodeHasChanged = true; + } + this.properties.forEach(prop => { + const oldVal = oldNodeState[prop]; + const newVal = newNodeState[prop]; + if (prop !== 'transform' && oldVal !== newVal) { + animatedProps[prop] = [oldVal, newVal]; + nodeHasChanged = true; + } + }); + if (nodeHasChanged) { + this.timeline.add($el, animatedProps, 0); + } + } + + } + + if (swapping.length) { + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const oldNode = oldState.getNode($el); + const oldNodeProps = oldNode.properties; + $el.style.width = `${oldNodeProps.width}px`; + $el.style.height = `${oldNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // We don't animate the position of inlined elements + if (!oldNode.isInlined) { + $el.style.translate = `${oldNodeProps.x}px ${oldNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${oldState.getComputedValue($el, prop)}`; + } + }); + } + + for (let i = 0, l = swapping.length; i < l; i++) { + const $el = swapping[i]; + const newNode = newState.getNode($el); + const newNodeProps = newNode.properties; + this.timeline.call(() => { + $el.style.width = `${newNodeProps.width}px`; + $el.style.height = `${newNodeProps.height}px`; + // Overrides user defined min and max to prevents width and height clamping + $el.style.minWidth = `auto`; + $el.style.minHeight = `auto`; + $el.style.maxWidth = `none`; + $el.style.maxHeight = `none`; + // Don't set translate for inlined elements (to avoid text reflow) + if (!newNode.isInlined) { + $el.style.translate = `${newNodeProps.x}px ${newNodeProps.y}px`; + } + this.properties.forEach(prop => { + if (prop !== 'transform') { + $el.style[prop] = `${newState.getComputedValue($el, prop)}`; + } + }); + }, newNode.delay + newNode.duration / 2); + } + + if (animatedSwap.length) { + const ease = parseEase(newState.nodes.get(animatedSwap[0].dataset.layoutId).ease); + const inverseEased = t => 1 - ease(1 - t); + const animatedSwapParams = /** @type {AnimationParams} */({}); + if (swapAtProps) { + for (let prop in swapAtProps) { + if (prop !== 'transform') { + animatedSwapParams[prop] = [ + { from: (/** @type {HTMLElement} */$el) => oldState.getComputedValue($el, prop), to: swapAtProps[prop] }, + { from: swapAtProps[prop], to: (/** @type {HTMLElement} */$el) => newState.getComputedValue($el, prop), ease: inverseEased } + ] + } + } + } + this.timeline.add(animatedSwap, animatedSwapParams, 0); + } + + } + + const transformedLength = transformed.length; + + if (transformedLength) { + // We only need to set the transform property here since translate is already defined in the targets loop + for (let i = 0; i < transformedLength; i++) { + const $el = transformed[i]; + const node = newState.getNode($el); + // Don't set translate for inlined elements (to avoid text reflow) + if (!node.isInlined) { + $el.style.translate = `${oldState.getComputedValue($el, 'x')}px ${oldState.getComputedValue($el, 'y')}px`; + } + $el.style.transform = oldState.getComputedValue($el, 'transform'); + if (animatedSwap.includes($el)) { + node.ease = getFunctionValue(swapAtParams.ease, $el, node.index, node.targets, null, null); + node.duration = getFunctionValue(swapAtParams.duration, $el, node.index, node.targets, null, null); + } + } + this.transformAnimation = waapi.animate(transformed, { + translate: (/** @type {HTMLElement} */$el) => { + const node = newState.getNode($el); + // Don't animate translate for inlined elements (to avoid text reflow) + if (node.isInlined) return '0px 0px'; + return `${newState.getComputedValue($el, 'x')}px ${newState.getComputedValue($el, 'y')}px`; + }, + transform: (/** @type {HTMLElement} */$el) => { + const newValue = newState.getComputedValue($el, 'transform'); + if (!animatedSwap.includes($el)) return newValue; + const oldValue = oldState.getComputedValue($el, 'transform'); + const node = newState.getNode($el); + return [oldValue, getFunctionValue(swapAtProps.transform, $el, node.index, node.targets, null, null), newValue] + }, + autoplay: false, + // persist: true, + ...timingParams, + }); + this.timeline.sync(this.transformAnimation, 0); + } + + return this.timeline.init(); + } + + /** + * @param {(layout: this) => void} callback + * @param {LayoutAnimationParams} [params] + * @return {Timeline} + */ + update(callback, params = {}) { + this.record(); + callback(this); + return this.animate(params); + } +} + +/** + * @param {DOMTargetSelector} root + * @param {AutoLayoutParams} [params] + * @return {AutoLayout} + */ +export const createLayout = (root, params) => new AutoLayout(root, params); diff --git a/src/svg/morphto.js b/src/svg/morphto.js index e2d907208..e3b731b30 100644 --- a/src/svg/morphto.js +++ b/src/svg/morphto.js @@ -1,7 +1,3 @@ -import { - morphPointsSymbol, -} from '../core/consts.js'; - import { round, } from '../core/helpers.js'; @@ -22,7 +18,7 @@ import { * @param {Number} [precision] * @return {FunctionValue} */ -export const morphTo = (path2, precision = .33) => ($path1) => { +export const morphTo = (path2, precision = .33) => ($path1, index, total, prevTween) => { const tagName1 = ($path1.tagName || '').toLowerCase(); if (!tagName1.match(/^(path|polygon|polyline)$/)) { throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use , or .`); @@ -37,7 +33,7 @@ export const morphTo = (path2, precision = .33) => ($path1) => { } const isPath = $path1.tagName === 'path'; const separator = isPath ? ' ' : ','; - const previousPoints = $path1[morphPointsSymbol]; + const previousPoints = prevTween ? prevTween._value : null; if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints); let v1 = '', v2 = ''; @@ -59,7 +55,5 @@ export const morphTo = (path2, precision = .33) => ($path1) => { } } - $path1[morphPointsSymbol] = v2; - return [v1, v2]; } diff --git a/src/text/index.js b/src/text/index.js index 5e8c649dc..c613935c1 100644 --- a/src/text/index.js +++ b/src/text/index.js @@ -1 +1,2 @@ -export * from './split.js'; \ No newline at end of file +export * from './split.js'; +export * from './scramble.js'; \ No newline at end of file diff --git a/src/text/scramble.js b/src/text/scramble.js new file mode 100644 index 000000000..e690b6f25 --- /dev/null +++ b/src/text/scramble.js @@ -0,0 +1,276 @@ +import { + createSeededRandom, +} from '../utils/random.js'; + +import { + globals, +} from '../core/globals.js'; + +import { + round, +} from '../core/helpers.js'; + +import { + parseEase, +} from '../easings/eases/parser.js'; + +import { + noop, +} from '../core/consts.js'; + + +/** + * @import { + * ScrambleTextParams, + * FunctionValue, + * } from '../types/index.js' +*/ + +/** + * '-' is the range operator; place it at the start or end of the string to use it as a literal (e.g. '-abc' or 'abc-') + * @param {String} str + * @return {String} + */ +const expandCharRanges = (str) => { + let result = ''; + for (let i = 0, l = str.length; i < l; i++) { + if (i + 2 < l && str[i + 1] === '-' && str.charCodeAt(i) < str.charCodeAt(i + 2)) { + const start = str.charCodeAt(i); + const end = str.charCodeAt(i + 2); + for (let c = start; c <= end; c++) result += String.fromCharCode(c); + i += 2; + } else { + result += str[i]; + } + } + return result; +} + +const charSets = { + lowercase: 'a-z', + uppercase: 'A-Z', + numbers: '0-9', + symbols: '!%#_|*+=', + braille: 'â €-⣿', + blocks: 'â–€-â–Ÿ', + shades: 'â–‘-â–“', +} + +const originalTexts = new WeakMap(); + +/** + * Returns a function-based tween value that scrambles the target's text content, + * progressively revealing the original text. + * + * @param {ScrambleTextParams} [params] + * @return {FunctionValue} + */ +export const scrambleText = (params = {}) => { + if (!params) params = {}; + const charsParam = params.chars; + const easeFn = parseEase(params.ease || 'linear'); + const text = params.text; + const fromParam = params.from; + const reversed = params.reversed || false; + const perturbation = params.perturbation || 0; + const cursorParam = params.cursor; + const cursorChars = cursorParam === true ? '_' + : typeof cursorParam === 'number' ? String.fromCharCode(cursorParam) + : typeof cursorParam === 'string' ? cursorParam + : ''; + const cursorLen = cursorChars.length; + const seed = params.seed || 0; + const override = params.override !== undefined ? params.override : true; + const revealRate = params.revealRate || 60; + const interval = 1000 * globals.timeScale / revealRate; + const settleDuration = params.settleDuration || 300 * globals.timeScale; + const settleRate = params.settleRate || 30; + const durationParam = params.duration; + const revealDelayParam = params.revealDelay; + const delayParam = params.delay; + const onChange = params.onChange || noop; + + return (target, index, targets, prevTween) => { + const rawChars = typeof charsParam === 'function' ? charsParam(target, index, targets) : (charsParam || 'a-zA-Z0-9!%#_'); + const characters = expandCharRanges(charSets[rawChars] || rawChars); + const totalChars = characters.length - 1; + const duration = typeof durationParam === 'function' ? durationParam(target, index, targets) : durationParam; + const revealDelay = typeof revealDelayParam === 'function' ? revealDelayParam(target, index, targets) : (revealDelayParam || 0); + const delay = typeof delayParam === 'function' ? delayParam(target, index, targets) : (delayParam || 0); + const rng = seed ? createSeededRandom(seed) : createSeededRandom(); + if (!originalTexts.has(target)) originalTexts.set(target, target.textContent); + const startingText = prevTween ? prevTween._value : target.textContent; + const targetText = text !== undefined + ? (typeof text === 'function' ? text(target, index, targets) : text) + : prevTween ? prevTween._value + : originalTexts.get(target); + const settledText = targetText === ' ' || targetText === ' ' ? ' ' : targetText; + const startLength = startingText === ' ' ? 0 : startingText.length; + const endLength = settledText.length; + const overrideChars = override === true ? characters + : typeof override === 'string' && override.length > 0 ? expandCharRanges(charSets[/** @type {String} */(override)] || /** @type {String} */(override)) + : null; + const totalOverrideChars = overrideChars ? overrideChars.length - 1 : 0; + // Space override uses   so the browser doesn't collapse consecutive spaces in innerHTML + const overrideChar = override === ' ' ? ' ' : null; + // When starting from blank, only animate the target text length to avoid padding beyond it + const animLength = override === '' ? endLength : Math.max(startLength, endLength); + // Compute total duration from interval spacing and settle time, or use the explicit duration + const animDuration = duration > 0 ? duration : (animLength - 1) * interval + settleDuration; + const computedDuration = round((animDuration + revealDelay) / globals.timeScale, 0) * globals.timeScale; + const revealDelayRatio = revealDelay > 0 ? round(revealDelay / computedDuration, 12) : 0; + // Auto-resolve reveal direction: shrinking text reveals from right, growing from left + const resolvedFrom = fromParam === undefined || fromParam === 'auto' ? (endLength < startLength ? 'right' : 'left') : fromParam; + const charOrder = new Int32Array(animLength); + if (resolvedFrom === 'random') { + for (let i = 0; i < animLength; i++) charOrder[i] = i; + for (let i = animLength - 1; i > 0; i--) { + const j = rng(0, i); + const t = charOrder[i]; charOrder[i] = charOrder[j]; charOrder[j] = t; + } + } else { + const ref = resolvedFrom === 'right' ? (override === '' || !startLength ? animLength : startLength) - 1 + : resolvedFrom === 'center' ? ((override === '' || !startLength ? animLength : startLength) - 1) / 2 + : typeof resolvedFrom === 'number' ? resolvedFrom + : 0; + const abs = Math.abs; + const indices = new Array(animLength); + for (let i = 0; i < animLength; i++) indices[i] = i; + indices.sort((a, b) => abs(a - ref) - abs(b - ref)); + for (let i = 0; i < animLength; i++) charOrder[indices[i]] = i; + } + if (reversed) { + const last = animLength - 1; + for (let i = 0; i < animLength; i++) charOrder[i] = last - charOrder[i]; + } + // settleRatio is the fraction of the animation each character spends in the active scrambling zone + const settleRatio = round(settleDuration / animDuration, 12); + // settleSpacing is the time gap between consecutive characters entering the active zone + const settleSpacing = round((1 - settleRatio) / animLength, 12); + const cursorZone = cursorLen * settleSpacing; + // stepRatio controls how often scramble characters refresh (based on settleRate) + const stepRatio = round(1000 * globals.timeScale / (settleRate * computedDuration), 12); + // Pre-compute per-character start and settle times + const charStarts = new Float32Array(animLength); + const charEnds = new Float32Array(animLength); + const scale = perturbation > 0 ? perturbation * settleRatio : 0; + for (let c = 0; c < animLength; c++) { + const so = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + const eo = scale > 0 ? (rng(0, 2000) - 1000) / 1000 * scale : 0; + charStarts[c] = charOrder[c] * settleSpacing + so; + charEnds[c] = Math.ceil((charStarts[c] + settleRatio + eo) / stepRatio) * stepRatio; + } + // When text shrinks with non-sequential from modes, delay target settle times past all extras + if (endLength < animLength && resolvedFrom !== 'left' && resolvedFrom !== 'right' && resolvedFrom !== 'random') { + let maxExtraEnd = 0; + for (let c = endLength; c < animLength; c++) { + if (charEnds[c] > maxExtraEnd) maxExtraEnd = charEnds[c]; + } + const targets = new Array(endLength); + for (let c = 0; c < endLength; c++) targets[c] = c; + targets.sort((a, b) => charOrder[a] - charOrder[b]); + const targetSpacing = (1 - maxExtraEnd) / endLength; + for (let i = 0; i < endLength; i++) { + const revealTime = maxExtraEnd + i * targetSpacing; + if (revealTime > charEnds[targets[i]]) { + charEnds[targets[i]] = revealTime; + } + } + } + // charCache holds the current scramble character for each position, refreshed at settleRate + const charCache = new Array(animLength); + for (let c = 0; c < animLength; c++) { + charCache[c] = characters[rng(0, totalChars)]; + } + // overrideCache holds scramble characters for the starting text (override: true or custom string) + const overrideCache = overrideChars ? (overrideChars === characters ? charCache : new Array(animLength)) : null; + if (overrideCache && overrideCache !== charCache) { + for (let c = 0; c < animLength; c++) { + overrideCache[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, overrideChars.length - 1)]; + } + } + // Build the initial display text based on override mode + let fillStartText = startingText; + if (!prevTween) { + if (override === '') { + fillStartText = ''; + } else if (overrideChars) { + fillStartText = ''; + for (let c = 0; c < startLength; c++) { + fillStartText += startingText[c] === ' ' ? ' ' : /** @type {Array} */(overrideCache)[c]; + } + } + } + + let lastValue = -1; + let lastStep = -1; + let scrambled = ''; + const hasOverride = override !== ''; + const hasOverrideChars = !!overrideChars; + const hasCursor = cursorLen > 0; + + return { + from: 0, + to: 1, + duration: computedDuration, + delay: delay, + ease: 'linear', + modifier: (v) => { + if (v === lastValue) return scrambled; + lastValue = v; + if (delay > 0 && v <= 0) { scrambled = startingText; return startingText; } + if (v <= 0) { scrambled = fillStartText; return fillStartText; } + if (v >= 1) { scrambled = settledText; return settledText; } + scrambled = ''; + // Only refresh scramble characters when we cross a settleRate step boundary + const currentStep = (v / stepRatio) | 0; + const refreshChars = currentStep !== lastStep; + if (refreshChars) lastStep = currentStep; + // Subtract delay ratio to get the effective animation progress + const linear = revealDelayRatio > 0 ? (v - revealDelayRatio) / (1 - revealDelayRatio) : v; + const t = linear > 0 ? easeFn(linear) : 0; + for (let c = 0; c < animLength; c++) { + // Each character has its own start/end window based on its reveal order + const charStart = charStarts[c]; + const charEnd = charEnds[c]; + // Settled zone: character has finished its transition + if (t >= charEnd) { + if (c < endLength) scrambled += settledText[c]; + continue; + } + // Pre-transition zone: reveal wave hasn't reached this character yet + if (t <= 0 || t < charStart) { + if (hasOverride && c < startLength) { + if (hasOverrideChars) { + if (startingText[c] === ' ') { + scrambled += ' '; + } else { + if (refreshChars) /** @type {Array} */(overrideCache)[c] = overrideChar || /** @type {String} */(overrideChars)[rng(0, totalOverrideChars)]; + scrambled += /** @type {Array} */(overrideCache)[c]; + } + } else { + // Default (override: false): show the original starting text + scrambled += startingText[c]; + } + } + continue; + } + // Active zone: character is between charStart and charEnd + const isSpace = (c < endLength && settledText[c] === ' ') || (c < startLength && startingText[c] === ' '); + if (isSpace) { + scrambled += ' '; + } else if (hasCursor && t - charStart < cursorZone) { + // Cursor sub-zone: show cursor character based on position within cursor width + scrambled += cursorChars[cursorLen - 1 - (((t - charStart) / settleSpacing) | 0)]; + } else { + // Scramble zone: show cycling random characters + if (refreshChars) charCache[c] = characters[rng(0, totalChars)]; + scrambled += charCache[c]; + } + } + if (refreshChars) onChange(scrambled, t); + return scrambled; + } + } + } +} diff --git a/src/text/split.js b/src/text/split.js index 60cd016f6..c397e9ff3 100644 --- a/src/text/split.js +++ b/src/text/split.js @@ -103,12 +103,23 @@ const filterEmptyElements = $el => { /** * @param {HTMLElement} $el * @param {Number} lineIndex - * @param {Set} bin - * @returns {Set} + * @param {Set} bin + * @returns {Set} */ const filterLineElements = ($el, lineIndex, bin) => { const dataLineAttr = $el.getAttribute(dataLine); - if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') bin.add($el); + if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') { + bin.add($el); + // Also remove adjacent whitespace-only text nodes + const prev = $el.previousSibling; + const next = $el.nextSibling; + if (prev && prev.nodeType === 3 && whiteSpaceRgx.test(prev.textContent)) { + bin.add(prev); + } + if (next && next.nodeType === 3 && whiteSpaceRgx.test(next.textContent)) { + bin.add(next); + } + } let i = $el.childElementCount; while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin); return bin; @@ -200,7 +211,7 @@ const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, d */ export class TextSplitter { /** - * @param {HTMLElement|NodeList|String|Array} target + * @param {Element|NodeList|String|Array} target * @param {TextSplitterParams} [parameters] */ constructor(target, parameters = {}) { @@ -272,11 +283,11 @@ export class TextSplitter { } /** - * @param {(...args: any[]) => Tickable | (() => void)} effect + * @param {(...args: any[]) => Tickable | (() => void) | void} effect * @return this */ addEffect(effect) { - if (!isFnc(effect)) return console.warn('Effect must return a function.'); + if (!isFnc(effect)) { console.warn('Effect must return a function.'); return this; } const refreshableEffect = keepTime(effect); this.effects.push(refreshableEffect); if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this); @@ -323,7 +334,7 @@ export class TextSplitter { // Only concatenate if both current and previous are non-word-like and don't contain spaces const lastWordIndex = tempWords.length - 1; const lastWord = tempWords[lastWordIndex]; - if (!lastWord.includes(' ') && !segment.includes(' ')) { + if (!whiteSpaceGroupRgx.test(lastWord) && !whiteSpaceGroupRgx.test(segment)) { tempWords[lastWordIndex] += segment; } else { tempWords.push(segment); @@ -416,7 +427,7 @@ export class TextSplitter { for (let i = 0, l = elementsArray.length; i < l; i++) { const $el = elementsArray[i]; const { top, height } = $el.getBoundingClientRect(); - if (y && top - y > height * .5) linesCount++; + if (!isUnd(y) && top - y > height * .5) linesCount++; $el.setAttribute(dataLine, `${linesCount}`); const nested = $el.querySelectorAll(`[${dataLine}]`); let c = nested.length; @@ -430,9 +441,11 @@ export class TextSplitter { for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) { const $clone = /** @type {HTMLElement} */($el.cloneNode(true)); filterLineElements($clone, lineIndex, new Set()).forEach($el => { - const $parent = $el.parentElement; - if ($parent) parents.add($parent); - $el.remove(); + const $parent = $el.parentNode; + if ($parent) { + if ($el.nodeType === 1) parents.add(/** @type {HTMLElement} */($parent)); + $parent.removeChild($el); + } }); clones.push($clone); } @@ -445,6 +458,7 @@ export class TextSplitter { if (wordTemplate) this.words = getAllTopLevelElements($el, wordType); if (charTemplate) this.chars = getAllTopLevelElements($el, charType); } + // Remove the word wrappers and clear the words array if lines split only if (this.linesOnly) { const words = this.words; @@ -479,7 +493,7 @@ export class TextSplitter { } /** - * @param {HTMLElement|NodeList|String|Array} target + * @param {Element|NodeList|String|Array} target * @param {TextSplitterParams} [parameters] * @return {TextSplitter} */ diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index ef62d7d2f..5c23d5fe9 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -34,8 +34,8 @@ import { } from '../core/render.js'; import { - cleanInlineStyles, -} from '../utils/target.js'; + revertValues, +} from '../core/styles.js'; import { Timer, @@ -70,6 +70,7 @@ import { * DefaultsParams, * TimelinePosition, * StaggerFunction, + * TargetsArray, * } from '../types/index.js' */ @@ -79,6 +80,8 @@ import { * } from '../waapi/waapi.js' */ +/** @import {TweakRegister} from 'tweaks' */ + /** * @param {Timeline} tl * @return {Number} @@ -100,7 +103,7 @@ function getTimelineTotalDuration(tl) { * @param {Number} timePosition * @param {TargetsParam} targets * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] * @return {Timeline} * * @param {TimerParams|AnimationParams} childParams @@ -108,17 +111,17 @@ function getTimelineTotalDuration(tl) { * @param {Number} timePosition * @param {TargetsParam} [targets] * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [allTargets] */ -function addTlChild(childParams, tl, timePosition, targets, index, length) { +function addTlChild(childParams, tl, timePosition, targets, index, allTargets) { const isSetter = isNum(childParams.duration) && /** @type {Number} */(childParams.duration) <= minValue; // Offset the tl position with -minValue for 0 duration animations or .set() calls in order to align their end value with the defined position const adjustedPosition = isSetter ? timePosition - minValue : timePosition; - tick(tl, adjustedPosition, 1, 1, tickModes.AUTO); + if (tl.composition) tick(tl, adjustedPosition, 1, 1, tickModes.AUTO); const tlChild = targets ? - new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, length) : + new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, allTargets) : new Timer(/** @type {TimerParams} */(childParams), tl, adjustedPosition); - tlChild.init(true); + if (tl.composition) tlChild.init(true); // TODO: Might be better to insert at a position relative to startTime? addChild(tl, tlChild); forEachChildren(tl, (/** @type {Renderable} */child) => { @@ -130,6 +133,8 @@ function addTlChild(childParams, tl, timePosition, targets, index, length) { return tl; } +let TLId = 0; + export class Timeline extends Timer { /** @@ -137,6 +142,9 @@ export class Timeline extends Timer { */ constructor(parameters = {}) { super(/** @type {TimerParams&TimelineParams} */(parameters), null, 0); + ++TLId; + /** @type {String|Number} */ + this.id = !isUnd(parameters.id) ? parameters.id : TLId; /** @type {Number} */ this.duration = 0; // TL duration starts at 0 and grows when adding children /** @type {Record} */ @@ -145,6 +153,8 @@ export class Timeline extends Timer { const globalDefaults = globals.defaults; /** @type {DefaultsParams} */ this.defaults = defaultsParams ? mergeObjects(defaultsParams, globalDefaults) : globalDefaults; + /** @type {Boolean} */ + this.composition = setValue(parameters.composition, true); /** @type {Callback} */ this.onRender = parameters.onRender || globalDefaults.onRender; const tlPlaybackEase = setValue(parameters.playbackEase, globalDefaults.playbackEase); @@ -157,7 +167,7 @@ export class Timeline extends Timer { * @overload * @param {TargetsParam} a1 * @param {AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] * @return {this} * * @overload @@ -167,7 +177,7 @@ export class Timeline extends Timer { * * @param {TargetsParam|TimerParams} a1 * @param {TimelinePosition|AnimationParams} a2 - * @param {TimelinePosition|StaggerFunction} [a3] + * @param {TimelinePosition|StaggerFunction|TweakRegister} [a3] */ add(a1, a2, a3) { const isAnim = isObj(a2); @@ -176,9 +186,11 @@ export class Timeline extends Timer { this._hasChildren = true; if (isAnim) { const childParams = /** @type {AnimationParams} */(a2); - // Check for function for children stagger positions - if (isFnc(a3)) { - const staggeredPosition = a3; + const editorHook = globals.editor && globals.editor.addTimelineChild; + const isStaggerType = a3 && /** @type {TweakRegister} */(a3).type === 'Stagger' && globals.editor; + // Check for function or Stagger type children positions + const staggeredPosition = isFnc(a3) ? a3 : null; + if (staggeredPosition || isStaggerType) { const parsedTargetsArray = parseTargets(/** @type {TargetsParam} */(a1)); // Store initial duration before adding new children that will change the duration const tlDuration = this.duration; @@ -189,28 +201,36 @@ export class Timeline extends Timer { let i = 0; /** @type {Number} */ const parsedLength = (parsedTargetsArray.length); + // Call editor hook once for the entire stagger group instead of per target + const resolvedParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3, parsedLength) : null; + // Resolve stagger AFTER editor hook so tweaked position value (a3.defaultValue) is used + const staggerFn = staggeredPosition || globals.editor.resolveStagger(/** @type {TweakRegister} */(a3).defaultValue); parsedTargetsArray.forEach((/** @type {Target} */target) => { // Create a new parameter object for each staggered children - const staggeredChildParams = { ...childParams }; + const staggeredChildParams = { ...(resolvedParams || childParams) }; // Reset the duration of the timeline iteration before each stagger to prevent wrong start value calculation this.duration = tlDuration; this.iterationDuration = tlIterationDuration; if (!isUnd(id)) staggeredChildParams.id = id + '-' + i; + const staggeredTimePosition = parseTimelinePosition(this, staggerFn(target, i, parsedTargetsArray, null, this)); addTlChild( staggeredChildParams, this, - parseTimelinePosition(this, staggeredPosition(target, i, parsedLength, this)), + staggeredTimePosition, target, i, - parsedLength + parsedTargetsArray, ); i++; }); } else { + // Call editor hook before resolving position so tweaked values are applied + const resolvedChildParams = editorHook ? editorHook(/** @type {TargetsParam} */(a1), childParams, this.id, a3) : childParams; + const resolvedPosition = a3 && /** @type {*} */(a3).type ? /** @type {*} */(a3).defaultValue : a3; addTlChild( - childParams, + resolvedChildParams, this, - parseTimelinePosition(this, a3), + parseTimelinePosition(this, resolvedPosition), /** @type {TargetsParam} */(a1), ); } @@ -222,7 +242,8 @@ export class Timeline extends Timer { parseTimelinePosition(this,a2), ); } - return this.init(true); + if (this.composition) this.init(true); + return this; } } @@ -249,7 +270,11 @@ export class Timeline extends Timer { if (isUnd(synced) || synced && isUnd(synced.pause)) return this; synced.pause(); const duration = +(/** @type {globalThis.Animation} */(synced).effect ? /** @type {globalThis.Animation} */(synced).effect.getTiming().duration : /** @type {Tickable} */(synced).duration); - return this.add(synced, { currentTime: [0, duration], duration, ease: 'linear' }, position); + // Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish. + if (!isUnd(synced) && !isUnd(/** @type {WAAPIAnimation} */(synced).persist)) { + /** @type {WAAPIAnimation} */(synced).persist = true; + } + return this.add(synced, { currentTime: [0, duration], duration, delay: 0, ease: 'linear', playbackEase: 'linear' }, position); } /** @@ -272,7 +297,7 @@ export class Timeline extends Timer { */ call(callback, position) { if (isUnd(callback) || callback && !isFnc(callback)) return this; - return this.add({ duration: 0, onComplete: () => callback(this) }, position); + return this.add({ duration: 0, delay: 0, onComplete: () => callback(this) }, position); } /** @@ -315,8 +340,8 @@ export class Timeline extends Timer { * @return {this} */ refresh() { - forEachChildren(this, (/** @type {JSAnimation} */child) => { - if (child.refresh) child.refresh(); + forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => { + if (/** @type {JSAnimation} */(child).refresh) /** @type {JSAnimation} */(child).refresh(); }); return this; } @@ -326,8 +351,8 @@ export class Timeline extends Timer { */ revert() { super.revert(); - forEachChildren(this, (/** @type {JSAnimation} */child) => child.revert, true); - return cleanInlineStyles(this); + forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => child.revert, true); + return revertValues(this); } /** @@ -347,4 +372,9 @@ export class Timeline extends Timer { * @param {TimelineParams} [parameters] * @return {Timeline} */ -export const createTimeline = parameters => new Timeline(parameters).init(); +export const createTimeline = parameters => { + if (globals.editor) { + return /** @type {Timeline} */(/** @type {unknown} */(globals.editor.addTimeline(parameters))); + } + return new Timeline(parameters).init(); +}; diff --git a/src/timer/timer.js b/src/timer/timer.js index 87fda73f9..ae7756000 100644 --- a/src/timer/timer.js +++ b/src/timer/timer.js @@ -99,6 +99,9 @@ const reviveTimer = timer => { let timerId = 0; +/** @param {Timer} prev @param {Timer} child */ +const sortByPriority = (prev, child) => prev._priority > child._priority; + /** * Base class used to create Timers, Animations and Timelines */ @@ -112,6 +115,8 @@ export class Timer extends Clock { super(0); + ++timerId; + const { id, delay, @@ -123,6 +128,7 @@ export class Timer extends Clock { autoplay, frameRate, playbackRate, + priority, onComplete, onLoop, onPause, @@ -133,31 +139,32 @@ export class Timer extends Clock { if (scope.current) scope.current.register(this); - const timerInitTime = parent ? 0 : engine._elapsedTime; + const timerInitTime = parent ? 0 : engine._lastTickTime; const timerDefaults = parent ? parent.defaults : globals.defaults; const timerDelay = /** @type {Number} */(isFnc(delay) || isUnd(delay) ? timerDefaults.delay : +delay); const timerDuration = isFnc(duration) || isUnd(duration) ? Infinity : +duration; const timerLoop = setValue(loop, timerDefaults.loop); const timerLoopDelay = setValue(loopDelay, timerDefaults.loopDelay); - const timerIterationCount = timerLoop === true || - timerLoop === Infinity || - /** @type {Number} */(timerLoop) < 0 ? Infinity : - /** @type {Number} */(timerLoop) + 1; + let timerIterationCount = timerLoop === true || + timerLoop === Infinity || + /** @type {Number} */(timerLoop) < 0 ? Infinity : + /** @type {Number} */(timerLoop) + 1; let offsetPosition = 0; if (parent) { offsetPosition = parentPosition; } else { - // Make sure to tick the engine once if not currently running to get up to date engine._elapsedTime + // Make sure to tick the engine once if not currently running to get up to date engine._lastTickTime // to avoid big gaps with the following offsetPosition calculation if (!engine.reqId) engine.requestTick(now()); // Make sure to scale the offset position with globals.timeScale to properly handle seconds unit - offsetPosition = (engine._elapsedTime - engine._startTime) * globals.timeScale; + offsetPosition = (engine._lastTickTime - engine._startTime) * globals.timeScale; } // Timer's parameters - this.id = !isUnd(id) ? id : ++timerId; + /** @type {String|Number} */ + this.id = !isUnd(id) ? id : timerId; /** @type {Timeline} */ this.parent = parent; // Total duration of the timer @@ -217,7 +224,7 @@ export class Timer extends Clock { // Clock's parameters /** @type {Number} */ - this._elapsedTime = timerInitTime; + this._lastTickTime = timerInitTime; /** @type {Number} */ this._startTime = timerInitTime; /** @type {Number} */ @@ -226,6 +233,8 @@ export class Timer extends Clock { this._fps = setValue(frameRate, timerDefaults.frameRate); /** @type {Number} */ this._speed = setValue(playbackRate, timerDefaults.playbackRate); + /** @type {Number} */ + this._priority = +setValue(priority, 1); } get cancelled() { @@ -248,7 +257,7 @@ export class Timer extends Clock { } get iterationCurrentTime() { - return round(this._iterationTime, globals.precision); + return clamp(round(this._iterationTime, globals.precision), 0, this.iterationDuration); } set iterationCurrentTime(time) { @@ -346,9 +355,9 @@ export class Timer extends Clock { /** @return {this} */ resetTime() { const timeScale = 1 / (this._speed * engine._speed); - // TODO: See if we can safely use engine._elapsedTime here + // TODO: See if we can safely use engine._lastTickTime here // if (!engine.reqId) engine.requestTick(now()) - // this._startTime = engine._elapsedTime - (this._currentTime + this._delay) * timeScale; + // this._startTime = engine._lastTickTime - (this._currentTime + this._delay) * timeScale; this._startTime = now() - (this._currentTime + this._delay) * timeScale; return this; } @@ -370,7 +379,7 @@ export class Timer extends Clock { tick(this, minValue, 0, 0, tickModes.FORCE); } else { if (!this._running) { - addChild(engine, this); + addChild(engine, this, sortByPriority); engine._hasChildren = true; this._running = true; } @@ -480,10 +489,11 @@ export class Timer extends Clock { /** * Imediatly completes the timer, cancels it and triggers the onComplete callback + * @param {Boolean|Number} [muteCallbacks] * @return {this} */ - complete() { - return this.seek(this.duration).cancel(); + complete(muteCallbacks = 0) { + return this.seek(this.duration, muteCallbacks).cancel(); } /** diff --git a/src/types/index.js b/src/types/index.js index d7c44928a..71def3ab2 100644 --- a/src/types/index.js +++ b/src/types/index.js @@ -5,6 +5,7 @@ export {} // Private types /** + * @import { TweakRegister } from 'tweaks'; * @import { ScrollObserver } from '../events/scroll.js'; * @import { JSAnimation } from '../animation/animation.js'; * @import { Animatable } from '../animatable/animatable.js'; @@ -14,6 +15,7 @@ export {} * @import { Draggable } from '../draggable/draggable.js'; * @import { TextSplitter } from '../text/split.js'; * @import { Scope } from '../scope/scope.js'; + * @import { AutoLayout } from '../layout/layout.js'; * @import { Spring } from '../easings/spring/index.js'; * @import { compositionTypes, tweenTypes, valueTypes } from '../core/consts.js'; */ @@ -37,7 +39,7 @@ export {} * @property {Number|FunctionValue} [duration] * @property {Number|FunctionValue} [delay] * @property {Number} [loopDelay] - * @property {EasingParam} [ease] + * @property {EasingParam|FunctionValue} [ease] * @property {'none'|'replace'|'blend'|compositionTypes} [composition] * @property {(v: any) => any} [modifier] * @property {Callback} [onBegin] @@ -52,7 +54,7 @@ export {} /** @typedef {JSAnimation|Timeline} Renderable */ /** @typedef {Timer|Renderable} Tickable */ /** @typedef {Timer&JSAnimation&Timeline} CallbackArgument */ -/** @typedef {Animatable|Tickable|WAAPIAnimation|Draggable|ScrollObserver|TextSplitter|Scope} Revertible */ +/** @typedef {Animatable|Tickable|WAAPIAnimation|Draggable|ScrollObserver|TextSplitter|Scope|AutoLayout} Revertible */ // Stagger types @@ -61,7 +63,8 @@ export {} * @callback StaggerFunction * @param {Target} [target] * @param {Number} [index] - * @param {Number} [length] + * @param {TargetsArray} [targets] + * @param {Tween|null} [prevTween] * @param {Timeline} [tl] * @return {T} */ @@ -69,9 +72,9 @@ export {} /** * @typedef {Object} StaggerParams * @property {Number|String} [start] - * @property {Number|'first'|'center'|'last'|'random'} [from] + * @property {Number|'first'|'center'|'last'|'random'|Array.} [from] * @property {Boolean} [reversed] - * @property {Array.} [grid] + * @property {Array.|Boolean} [grid] * @property {('x'|'y')} [axis] * @property {String|((target: Target, i: Number, length: Number) => Number)} [use] * @property {Number} [total] @@ -132,8 +135,8 @@ export {} // A hack to get both ease names suggestions AND allow any strings // https://github.com/microsoft/TypeScript/issues/29729#issuecomment-460346421 -/** @typedef {(String & {})|EaseStringParamNames|EasingFunction|Spring} EasingParam */ -/** @typedef {(String & {})|EaseStringParamNames|WAAPIEaseStringParamNames|EasingFunction|Spring} WAAPIEasingParam */ +/** @typedef {(String & {})|EaseStringParamNames|EasingFunction|Spring|TweakRegister} EasingParam */ +/** @typedef {(String & {})|EaseStringParamNames|WAAPIEaseStringParamNames|EasingFunction|Spring|TweakRegister} WAAPIEasingParam */ // Spring types @@ -189,6 +192,7 @@ export {} * @property {Boolean|ScrollObserver} [autoplay] * @property {Number} [frameRate] * @property {Number} [playbackRate] + * @property {Number} [priority] */ /** @@ -199,10 +203,11 @@ export {} /** * @callback FunctionValue - * @param {Target} target - The animated target - * @param {Number} index - The target index - * @param {Number} length - The total number of animated targets - * @return {Number|String|TweenObjectValue|Array.} + * @param {Target} [target] - The animated target + * @param {Number} [index] - The target index + * @param {TargetsArray} [targets] - The array of all animated targets + * @param {Tween|null} [prevTween] - The previous sibling tween for the same target and property + * @return {Number|String|TweenObjectValue|EasingParam|Array.} */ /** @@ -220,7 +225,8 @@ export {} * @property {String} property * @property {Target} target * @property {String|Number} _value - * @property {Function|null} _func + * @property {Function|null} _toFunc + * @property {Function|null} _fromFunc * @property {EasingFunction} _ease * @property {Array.} _fromNumbers * @property {Array.} _toNumbers @@ -270,7 +276,7 @@ export {} // JSAnimation types /** - * @typedef {Number|String|FunctionValue} TweenParamValue + * @typedef {Number|String|FunctionValue|EasingParam} TweenParamValue */ /** @@ -285,7 +291,7 @@ export {} * @typedef {Object} TweenParamsOptions * @property {TweenParamValue} [duration] * @property {TweenParamValue} [delay] - * @property {EasingParam} [ease] + * @property {EasingParam|FunctionValue} [ease] * @property {TweenModifier} [modifier] * @property {TweenComposition} [composition] */ @@ -369,13 +375,14 @@ export {} * - `'label'` - Label: Position animation at a named label position (e.g., `'My Label'`)
        * - `stagger(String|Nummber)` - Stagger multi-elements animation positions (e.g., 10, 20, 30...) * - * @typedef {TimelinePosition | StaggerFunction} TimelineAnimationPosition + * @typedef {TimelinePosition | StaggerFunction | TweakRegister} TimelineAnimationPosition */ /** * @typedef {Object} TimelineOptions * @property {DefaultsParams} [defaults] * @property {EasingParam} [playbackEase] + * @property {Boolean} [composition] */ /** @@ -392,8 +399,8 @@ export {} * @callback WAAPIFunctionValue * @param {DOMTarget} target - The animated target * @param {Number} index - The target index - * @param {Number} length - The total number of animated targets - * @return {WAAPITweenValue} + * @param {DOMTargetsArray} targets - The array of all animated targets + * @return {WAAPITweenValue|WAAPIEasingParam} */ /** @@ -419,7 +426,7 @@ export {} * @property {Number} [playbackRate] * @property {Number|WAAPIFunctionValue} [duration] * @property {Number|WAAPIFunctionValue} [delay] - * @property {WAAPIEasingParam} [ease] + * @property {WAAPIEasingParam|WAAPIFunctionValue} [ease] * @property {CompositeOperation} [composition] * @property {Boolean} [persist] * @property {Callback} [onComplete] @@ -550,6 +557,7 @@ export {} * @property {Callback} [onEnterBackward] * @property {Callback} [onLeaveBackward] * @property {Callback} [onUpdate] + * @property {Callback} [onResize] * @property {Callback} [onSyncComplete] */ @@ -638,6 +646,26 @@ export {} * @property {Boolean} [debug] */ +/** + * @typedef {Object} ScrambleTextParams + * @property {String|function(Target, Number, TargetsArray): String} [text] - the text to transition to, otherwise uses the original text + * @property {String|function(Target, Number, TargetsArray): String} [chars] - the characters used for scramble; named sets: 'lowercase', 'uppercase', 'numbers', 'symbols', 'braille', 'blocks', 'shades'; range syntax: 'A-Z', 'a-z0-9'; defaults to 'a-zA-Z0-9!%#_' + * @property {EasingParam} [ease] - the easing applied to the scramble animation + * @property {Number|'left'|'center'|'right'|'random'|'auto'} [from] - where the reveal wave starts from, 'auto' (default) uses 'left' when text grows and 'right' when it shrinks + * @property {Boolean} [reversed] - reverses the reveal order, so 'center' reveals from edges inward instead of center outward + * @property {Boolean|Number|String} [cursor] - characters displayed at the leading edge of the reveal wave; true uses '_', a number is a char code, a string is used directly + * @property {Number} [perturbation] - adds random timing offsets to each character's start and end, creating a more organic reveal + * @property {Number} [seed] - a seed for the random number generator to produce reproducible scramble sequences + * @property {Boolean|String} [override] - controls the starting appearance: false shows original text, true scrambles it (default), '' starts from blank, ' ' replaces characters with spaces, a custom string (supports range syntax like 'A-Z') uses its characters as scramble set + * @property {Number} [revealRate] - characters per second entering the active zone; higher values make the reveal wave move faster (default: 60) + * @property {Number} [settleDuration] - time in ms each character spends scrambling before settling into its final glyph (default: 300) + * @property {Number} [settleRate] - how many times per second scramble characters cycle in the active zone (default: 30) + * @property {Number|function(Target, Number, TargetsArray): Number} [duration] - if set to a value greater than 0, overrides the computed duration from interval and settle; if unset or 0, duration is calculated automatically from text length and timing parameters + * @property {Number|function(Target, Number, TargetsArray): Number} [revealDelay] - delay in ms before the reveal wave starts within the scramble animation + * @property {Number|function(Target, Number, TargetsArray): Number} [delay] - delay in ms before the entire scramble animation starts + * @property {function(String, Number): void} [onChange] - callback fired each time a character changes during scramble; receives the current scrambled text and the eased progress (0-1) + */ + // SVG types /** diff --git a/src/utils/chainable.js b/src/utils/chainable.js index 453dfed16..afe71a589 100644 --- a/src/utils/chainable.js +++ b/src/utils/chainable.js @@ -30,10 +30,13 @@ const chain = fn => { const result = fn(...args); return new Proxy(noop, { apply: (_, __, [v]) => result(v), - get: (_, prop) => chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { - const nextResult = chainables[prop](...nextArgs); - return (/**@type {Number|String} */v) => nextResult(result(v)); - }) + get: (_, prop) => { + if (!chainables[prop]) return undefined; + return chain(/**@param {...Number|String} nextArgs */(...nextArgs) => { + const nextResult = chainables[prop](...nextArgs); + return (/**@type {Number|String} */v) => nextResult(result(v)); + }) + } }); } } diff --git a/src/utils/index.js b/src/utils/index.js index 3229c0eb8..db509d5d8 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -2,4 +2,5 @@ export * from './chainable.js'; export * from './random.js'; export * from './time.js'; export * from './target.js'; -export * from './stagger.js'; \ No newline at end of file +export * from './stagger.js'; +export { forEachChildren, addChild, removeChild } from '../core/helpers.js'; \ No newline at end of file diff --git a/src/utils/stagger.js b/src/utils/stagger.js index 301e69b97..46f2d59bc 100644 --- a/src/utils/stagger.js +++ b/src/utils/stagger.js @@ -41,6 +41,7 @@ import { * @import { * StaggerParams, * StaggerFunction, + * JSTarget, * } from '../types/index.js' */ @@ -56,24 +57,28 @@ import { * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @overload * @param {String} val * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @overload * @param {[Number, Number]} val * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @overload * @param {[String, String]} val * @param {StaggerParams} [params] * @return {StaggerFunction} */ + /** * @param {Number|String|[Number, Number]|[String, String]} val The staggered value or range * @param {StaggerParams} [params] The stagger parameters @@ -82,6 +87,7 @@ import { export const stagger = (val, params = {}) => { let values = []; let maxValue = 0; + let cachedOffset; const from = params.from; const reversed = params.reversed; const ease = params.ease; @@ -89,12 +95,14 @@ export const stagger = (val, params = {}) => { const hasSpring = hasEasing && !isUnd(/** @type {Spring} */(ease).ease); const staggerEase = hasSpring ? /** @type {Spring} */(ease).ease : hasEasing ? parseEase(ease) : null; const grid = params.grid; + const autoGrid = grid === true; const axis = params.axis; const customTotal = params.total; const fromFirst = isUnd(from) || from === 0 || from === 'first'; const fromCenter = from === 'center'; const fromLast = from === 'last'; const fromRandom = from === 'random'; + const fromArr = isArr(from); const isRange = isArr(val); const useProp = params.use; const val1 = isRange ? parseNumber(val[0]) : parseNumber(val); @@ -102,40 +110,129 @@ export const stagger = (val, params = {}) => { const unitMatch = unitsExecRgx.exec((isRange ? val[1] : val) + emptyString); const start = params.start || 0 + (isRange ? val1 : 0); let fromIndex = fromFirst ? 0 : isNum(from) ? from : 0; - return (target, i, t, tl) => { + return (target, i, t, _, tl) => { const [ registeredTarget ] = registerTargets(target); - const total = isUnd(customTotal) ? t : customTotal; + const total = isUnd(customTotal) ? t.length : customTotal; const customIndex = !isUnd(useProp) ? isFnc(useProp) ? useProp(registeredTarget, i, total) : getOriginalAnimatableValue(registeredTarget, useProp) : false; const staggerIndex = isNum(customIndex) || isStr(customIndex) && isNum(+customIndex) ? +customIndex : i; if (fromCenter) fromIndex = (total - 1) / 2; if (fromLast) fromIndex = total - 1; if (!values.length) { - for (let index = 0; index < total; index++) { - if (!grid) { - values.push(abs(fromIndex - index)); + if (autoGrid) { + let hasPositions = true; + let minPosX = Infinity; + let minPosY = Infinity; + let maxPosX = -Infinity; + let maxPosY = -Infinity; + const pxArr = []; + const pyArr = []; + for (let index = 0; index < total; index++) { + const el = t[index]; + let px = 0; + let py = 0; + let found = false; + if (el && isFnc(el.getBoundingClientRect)) { + const rect = el.getBoundingClientRect(); + px = rect.left + rect.width / 2; + py = rect.top + rect.height / 2; + found = true; + } else { + const obj = /** @type {JSTarget} */(el); + if (obj && isNum(obj.x) && isNum(obj.y)) { + px = obj.x; + py = obj.y; + found = true; + } + } + if (!found) { + hasPositions = false; + break; + } + pxArr.push(px); + pyArr.push(py); + if (px < minPosX) minPosX = px; + if (py < minPosY) minPosY = py; + if (px > maxPosX) maxPosX = px; + if (py > maxPosY) maxPosY = py; + } + if (hasPositions) { + let fX = pxArr[0]; + let fY = pyArr[0]; + if (fromArr) { + fX = minPosX + from[0] * (maxPosX - minPosX); + fY = minPosY + from[1] * (maxPosY - minPosY); + } else if (fromCenter) { + fX = (minPosX + maxPosX) / 2; + fY = (minPosY + maxPosY) / 2; + } else if (fromLast) { + fX = pxArr[total - 1]; + fY = pyArr[total - 1]; + } else if (isNum(from)) { + fX = pxArr[from]; + fY = pyArr[from]; + } + for (let index = 0; index < total; index++) { + const distanceX = fX - pxArr[index]; + const distanceY = fY - pyArr[index]; + let value = sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values.push(value); + } + let minDist = Infinity; + for (let index = 0, l = values.length; index < l; index++) { + const absVal = abs(values[index]); + if (absVal > 0 && absVal < minDist) minDist = absVal; + } + if (minDist > 0 && minDist < Infinity) { + for (let index = 0, l = values.length; index < l; index++) { + values[index] = values[index] / minDist; + } + } } else { - const fromX = !fromCenter ? fromIndex % grid[0] : (grid[0] - 1) / 2; - const fromY = !fromCenter ? floor(fromIndex / grid[0]) : (grid[1] - 1) / 2; - const toX = index % grid[0]; - const toY = floor(index / grid[0]); - const distanceX = fromX - toX; - const distanceY = fromY - toY; - let value = sqrt(distanceX * distanceX + distanceY * distanceY); - if (axis === 'x') value = -distanceX; - if (axis === 'y') value = -distanceY; - values.push(value); + for (let index = 0; index < total; index++) { + values.push(abs(fromIndex - index)); + } + } + } else { + for (let index = 0; index < total; index++) { + if (!grid) { + values.push(abs(fromIndex - index)); + } else { + let fromX, fromY; + if (fromArr) { + fromX = from[0] * (grid[0] - 1); + fromY = from[1] * (grid[1] - 1); + } else if (fromCenter) { + fromX = (grid[0] - 1) / 2; + fromY = (grid[1] - 1) / 2; + } else { + fromX = fromIndex % grid[0]; + fromY = floor(fromIndex / grid[0]); + } + const toX = index % grid[0]; + const toY = floor(index / grid[0]); + const distanceX = fromX - toX; + const distanceY = fromY - toY; + let value = sqrt(distanceX * distanceX + distanceY * distanceY); + if (axis === 'x') value = -distanceX; + if (axis === 'y') value = -distanceY; + values.push(value); + } } - maxValue = max(...values); } + maxValue = max(...values); if (staggerEase) values = values.map(val => staggerEase(val / maxValue) * maxValue); if (reversed) values = values.map(val => axis ? (val < 0) ? val * -1 : -val : abs(maxValue - val)); if (fromRandom) values = shuffle(values); } const spacing = isRange ? (val2 - val1) / maxValue : val1; - const offset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); + if (isUnd(cachedOffset)) { + cachedOffset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start); + } /** @type {String|Number} */ - let output = offset + ((spacing * round(values[staggerIndex], 2)) || 0); - if (params.modifier) output = params.modifier(output); + let output = cachedOffset + ((spacing * round(values[staggerIndex], 2)) || 0); + if (params.modifier) output = params.modifier(/** @type {Number} */(output)); if (unitMatch) output = `${output}${unitMatch[2]}`; return output; } diff --git a/src/utils/time.js b/src/utils/time.js index 8e3b6f8bd..1f96b92c5 100644 --- a/src/utils/time.js +++ b/src/utils/time.js @@ -31,19 +31,20 @@ export const sync = (callback = noop) => { } /** - * @param {(...args: any[]) => Tickable | ((...args: any[]) => void)} constructor + * @param {(...args: any[]) => Tickable | ((...args: any[]) => void) | void} constructor * @return {(...args: any[]) => Tickable | ((...args: any[]) => void)} */ export const keepTime = constructor => { /** @type {Tickable} */ let tracked; return (...args) => { - let currentIteration, currentIterationProgress, reversed, alternate; + let currentIteration, currentIterationProgress, reversed, alternate, startTime; if (tracked) { currentIteration = tracked.currentIteration; currentIterationProgress = tracked.iterationProgress; reversed = tracked.reversed; alternate = tracked._alternate; + startTime = tracked._startTime; tracked.revert(); } const cleanup = constructor(...args); @@ -51,6 +52,7 @@ export const keepTime = constructor => { if (!isUnd(currentIterationProgress)) { /** @type {Tickable} */(tracked).currentIteration = currentIteration; /** @type {Tickable} */(tracked).iterationProgress = (alternate ? !(currentIteration % 2) ? reversed : !reversed : reversed) ? 1 - currentIterationProgress : currentIterationProgress; + /** @type {Tickable} */(tracked)._startTime = startTime; } return cleanup || noop; } diff --git a/src/waapi/composition.js b/src/waapi/composition.js index 8193f0b0d..bc6c81c1e 100644 --- a/src/waapi/composition.js +++ b/src/waapi/composition.js @@ -74,7 +74,7 @@ export const addWAAPIAnimation = (parent, $el, property, keyframes, params) => { parent.animations.push(animation); removeWAAPIAnimation($el, property); addChild(WAAPIAnimationsLookups, { parent, animation, $el, property, _next: null, _prev: null }); - const handleRemove = () => { removeWAAPIAnimation($el, property, parent); }; + const handleRemove = () => removeWAAPIAnimation($el, property, parent); animation.oncancel = handleRemove; animation.onremove = handleRemove; if (!parent.persist) { diff --git a/src/waapi/waapi.js b/src/waapi/waapi.js index bb5e9f325..6d1b26cee 100644 --- a/src/waapi/waapi.js +++ b/src/waapi/waapi.js @@ -35,7 +35,6 @@ import { transformsFragmentStrings, transformsSymbol, validTransforms, - cssVarPrefix, } from '../core/consts.js'; import { @@ -150,12 +149,12 @@ let transformsPropertiesRegistered = null; * @param {WAAPIKeyframeValue} value * @param {DOMTarget} $el * @param {Number} i - * @param {Number} targetsLength + * @param {DOMTargetsArray} parsedTargets * @return {String} */ -const normalizeTweenValue = (propName, value, $el, i, targetsLength) => { +const normalizeTweenValue = (propName, value, $el, i, parsedTargets) => { // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables - let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, targetsLength); + let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, parsedTargets, null, null); if (!isNum(v)) return v; if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`; if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`; @@ -168,18 +167,18 @@ const normalizeTweenValue = (propName, value, $el, i, targetsLength) => { * @param {WAAPIKeyframeValue} from * @param {WAAPIKeyframeValue} to * @param {Number} i - * @param {Number} targetsLength + * @param {DOMTargetsArray} parsedTargets * @return {WAAPITweenValue} */ -const parseIndividualTweenValue = ($el, propName, from, to, i, targetsLength) => { +const parseIndividualTweenValue = ($el, propName, from, to, i, parsedTargets) => { /** @type {WAAPITweenValue} */ let tweenValue = '0'; - const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, targetsLength) : getComputedStyle($el)[propName]; + const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, parsedTargets) : getComputedStyle($el)[propName]; if (!isUnd(from)) { - const computedFrom = normalizeTweenValue(propName, from, $el, i, targetsLength); + const computedFrom = normalizeTweenValue(propName, from, $el, i, parsedTargets); tweenValue = [computedFrom, computedTo]; } else { - tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, targetsLength)) : computedTo; + tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, parsedTargets)) : computedTo; } return tweenValue; } @@ -219,14 +218,11 @@ export class WAAPIAnimation { } const parsedTargets = registerTargets(targets); - const targetsLength = parsedTargets.length; - if (!targetsLength) { + if (!parsedTargets.length) { console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`); } - const ease = setValue(params.ease, parseWAAPIEasing(globals.defaults.ease)); - const spring = /** @type {Spring} */(ease).ease && ease; const autoplay = setValue(params.autoplay, globals.defaults.autoplay); const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false; const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true; @@ -237,8 +233,6 @@ export class WAAPIAnimation { const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal'; /** @type {FillMode} */ const fill = 'both'; // We use 'both' here because the animation can be reversed during playback - /** @type {String} */ - const easing = parseWAAPIEasing(ease); const timeScale = (globals.timeScale === 1 ? 1 : K); /** @type {DOMTargetsArray}] */ @@ -279,10 +273,19 @@ export class WAAPIAnimation { const elStyle = $el.style; const inlineStyles = this._inlineStyles[i] = {}; + const easeToParse = setValue(params.ease, globals.defaults.ease); + + const easeFunctionResult = getFunctionValue(easeToParse, $el, i, parsedTargets, null, null); + const keyEasing = isFnc(easeFunctionResult) || isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; + + const spring = /** @type {Spring} */(easeToParse).ease && easeToParse; + /** @type {String} */ + const easing = parseWAAPIEasing(keyEasing); + /** @type {Number} */ - const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, targetsLength)) * timeScale; + const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, parsedTargets, null, null)) * timeScale; /** @type {Number} */ - const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, targetsLength) * timeScale; + const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, parsedTargets, null, null) * timeScale; /** @type {CompositeOperation} */ const composite = /** @type {CompositeOperation} */(setValue(params.composition, 'replace')); @@ -303,24 +306,24 @@ export class WAAPIAnimation { let parsedPropertyValue; if (isObj(propertyValue)) { const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue); - const tweenOptionsEase = setValue(tweenOptions.ease, ease); + const tweenOptionsEase = setValue(tweenOptions.ease, easing); const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase; const to = /** @type {WAAPITweenOptions} */(tweenOptions).to; const from = /** @type {WAAPITweenOptions} */(tweenOptions).from; /** @type {Number} */ - tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, targetsLength)) * timeScale; + tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, parsedTargets, null, null)) * timeScale; /** @type {Number} */ - tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, targetsLength) * timeScale; + tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, parsedTargets, null, null) * timeScale; /** @type {CompositeOperation} */ tweenParams.composite = /** @type {CompositeOperation} */(setValue(tweenOptions.composition, composite)); /** @type {String} */ tweenParams.easing = parseWAAPIEasing(tweenOptionsEase); - parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, targetsLength); + parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); if (individualTransformProperty) { keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; cachedTransforms[individualTransformProperty] = parsedPropertyValue; } else { - keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, targetsLength); + keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, parsedTargets); } addWAAPIAnimation(this, $el, name, keyframes, tweenParams); if (!isUnd(from)) { @@ -333,8 +336,8 @@ export class WAAPIAnimation { } } else { parsedPropertyValue = isArr(propertyValue) ? - propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, targetsLength)) : - normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, targetsLength); + propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, parsedTargets)) : + normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, parsedTargets); if (individualTransformProperty) { keyframes[`--${individualTransformProperty}`] = parsedPropertyValue; cachedTransforms[individualTransformProperty] = parsedPropertyValue; @@ -368,8 +371,10 @@ export class WAAPIAnimation { * @return {this} */ forEach(callback) { - const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; - this.animations.forEach(cb); + try { + const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; + this.animations.forEach(cb); + } catch {}; return this; } @@ -466,14 +471,21 @@ export class WAAPIAnimation { cancel() { this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise - return this.commitStyles().forEach('cancel'); + this.commitStyles().forEach('cancel'); + this.animations.length = 0; // Needed to release all animations from memory + requestAnimationFrame(() => { + this.targets.forEach(($el) => { // Needed to avoid unecessary inline transorms + if ($el.style.transform === 'none') $el.style.removeProperty('transform'); + }); + }); + return this; } revert() { // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted, // This means if you have multiple animations animating different transforms on the same target, // reverting one of them will also override the transform property of the other animations. - // A better approach would be to store the original custom property values is they exist instead of the entire transform value, + // A better approach would be to store the original custom property values if they exist instead of the entire transform value, // and update the CSS variables with the orignal value this.cancel().targets.forEach(($el, i) => { const targetStyle = $el.style; @@ -483,7 +495,7 @@ export class WAAPIAnimation { if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) { targetStyle.removeProperty(toLowerCase(name)); } else { - targetStyle[name] = originalInlinedValue; + $el.style[name] = originalInlinedValue; } } // Remove style attribute if empty diff --git a/tests/index.html b/tests/index.html index 3163fb09d..245471aed 100644 --- a/tests/index.html +++ b/tests/index.html @@ -25,7 +25,7 @@ --color-green: #18FF74;/* green: */ --color-darkgreen: #00D672;/* darkgreen: */ --color-turquoise: #3CFFEC;/* turquoise: */ - --color-skyblue: #61C3FF;/* skyblue: */ + --color-blue: #61C3FF;/* blue: */ --color-kingblue: #5A87FF;/* kingblue: */ --color-lavender: #8453E3;/* lavender: */ --color-purple: #C26EFF;/* purple: */ diff --git a/tests/playground/additive-animations-2.html b/tests/playground/additive-animations-2.html index 85d79fcf8..5b9bda219 100644 --- a/tests/playground/additive-animations-2.html +++ b/tests/playground/additive-animations-2.html @@ -25,7 +25,7 @@ left: 0; width: 100%; height: 100%; - color: var(--red); + color: var(--red-1); } .ball { --diam: 100px; diff --git a/tests/playground/animejs-v2-logo-animation.html b/tests/playground/animejs-v2-logo-animation.html index 764e950cf..9c3466000 100644 --- a/tests/playground/animejs-v2-logo-animation.html +++ b/tests/playground/animejs-v2-logo-animation.html @@ -194,7 +194,6 @@ utils.set(['.fill.out', '.line.out'], { opacity: 0 }); const logoTimeline = createTimeline({ - autoplay: false, alternate: true, loop: 1, defaults: { @@ -294,8 +293,6 @@ }, 4200) .init(); - inspect(logoTimeline); - // logoTimeline.seek(660); // logoTimeline.seek(660); diff --git a/tests/playground/assets/css/styles.css b/tests/playground/assets/css/styles.css index 99afce33d..21fca87f1 100644 --- a/tests/playground/assets/css/styles.css +++ b/tests/playground/assets/css/styles.css @@ -1,19 +1,131 @@ :root { - --black: #252423; - --white: #F6F4F2; - --red: #FF4B4B; - --orange: #FF8F42; - --lightorange: #FFC730; - --yellow: #F6FF56; - --citrus: #A4FF4F; - --green: #18FF74; - --darkgreen: #00D672; - --turquoise: #3CFFEC; - --skyblue: #61C3FF; - --kingblue: #5A87FF; - --lavender: #8453E3; - --purple: #C26EFF; - --pink: #FB89FB; + --black-1: #252423; + --black-2: #262422; + --black-3: #262422; + --black-4: #262422; + --black-5: #272421; + --black-6: #272421; + --white-1: #f6f4f2; + --white-2: #b8b6b3; + --white-3: #9a9593; + --white-4: #524f49; + --white-5: #413d39; + --white-6: #312e2b; + --red-1: #ff4b4b; + --red-2: #bf3e3e; + --red-3: #9e3838; + --red-4: #532a29; + --red-5: #412726; + --red-6: #322523; + --corail-1: #ff8333; + --corail-2: #bf672d; + --corail-3: #9e582b; + --corail-4: #533724; + --corail-5: #412f23; + --corail-6: #322922; + --orange-1: #ffa828; + --orange-2: #bf8026; + --orange-3: #9e6d25; + --orange-4: #523e23; + --orange-5: #413422; + --orange-6: #322b21; + --yellow-1: #ffcc2a; + --yellow-2: #bf9a27; + --yellow-3: #9e8026; + --yellow-4: #524723; + --yellow-5: #413922; + --yellow-6: #322d21; + --citrus-1: #f9f640; + --citrus-2: #bab836; + --citrus-3: #9b9932; + --citrus-4: #515027; + --citrus-5: #403f24; + --citrus-6: #323022; + --lime-1: #b7ff54; + --lime-2: #8bbe44; + --lime-3: #759d3d; + --lime-4: #42522b; + --lime-5: #384027; + --lime-6: #2d3123; + --green-1: #6aff65; + --green-2: #54be50; + --green-3: #4a9e45; + --green-4: #31522e; + --green-5: #2c4029; + --green-6: #273124; + --emerald-1: #57f695; + --emerald-2: #47b873; + --emerald-3: #409962; + --emerald-4: #2d5039; + --emerald-5: #293f30; + --emerald-6: #263128; + --turquoise-1: #66ffbc; + --turquoise-2: #52be8e; + --turquoise-3: #479e77; + --turquoise-4: #305242; + --turquoise-5: #2b4035; + --turquoise-6: #26312b; + --cyan-1: #26f2d5; + --cyan-2: #25b5a0; + --cyan-3: #259686; + --cyan-4: #244e48; + --cyan-5: #233f39; + --cyan-6: #23302d; + --sega-1: #05dbe9; + --sega-2: #0fa4ae; + --sega-3: #138990; + --sega-4: #1e4a4c; + --sega-5: #203b3c; + --sega-6: #212f2f; + --sky-1: #33b3f1; + --sky-2: #2e88b4; + --sky-3: #2c7395; + --sky-4: #26424e; + --sky-5: #25363e; + --sky-6: #242c2f; + --indigo-1: #717aff; + --indigo-2: #595fbe; + --indigo-3: #4d519e; + --indigo-4: #323351; + --indigo-5: #2c2c3f; + --indigo-6: #282630; + --lavender-1: #a369ff; + --lavender-2: #7d53be; + --lavender-3: #6a489e; + --lavender-4: #3e3051; + --lavender-5: #342a3f; + --lavender-6: #2b2530; + --purple-1: #c06ddf; + --purple-2: #9356a8; + --purple-3: #7b4a8c; + --purple-4: #45314b; + --purple-5: #392b3c; + --purple-6: #2f262d; + --magenta-1: #e962bf; + --magenta-2: #af4e90; + --magenta-3: #93447a; + --magenta-4: #4e2e43; + --magenta-5: #3f2936; + --magenta-6: #31252b; + --pink-1: #ff86a7; + --pink-2: #bf687f; + --pink-3: #9f586b; + --pink-4: #53363c; + --pink-5: #412e32; + --pink-6: #322729; + --bg-1: #252423; + --bg-2: #2a2928; + --bg-3: #2f2e2d; + --bg-4: #353433; + --bg-5: #3a3938; + --fg-1: #dddcda; + --fg-2: #c6c3c1; + --fg-3: #96918f; + --fg-4: #65655e; + --fg-5: #33332e; + --br: 1rem; + --padding: 1rem; + --border-width: 1px; } *, *:before, *:after { @@ -27,16 +139,22 @@ html, body { - background-color: var(--black); - color: var(--white); - font-family: sans-serif; + background-color: var(--bg-1); + color: var(--fg-1); + font-family: BerkeleyMono-Regular, monospace, sans-serif; } -.black { color: var(--black); } -.white { color: var(--white); } -.red { color: var(--red); } -.blue { color: var(--skyblue); } -.green { color: var(--green); } +.br { border-radius: var(--br); } +.br .br { border-radius: calc(var(--br) * .5); } +.br .br .br { border-radius: calc(var(--br) * .25); } + +.black { color: var(--bg-1); } +.white { color: var(--fg-1); } +.red { color: var(--red-1); } +.orange { color: var(--corail-1); } +.yellow { color: var(--yellow-1); } +.blue { color: var(--sky-1); } +.green { color: var(--green-1); } .grid { --one-cell: 100px; diff --git a/tests/playground/draggables/callbacks/index.html b/tests/playground/draggables/callbacks/index.html index b7a1987c2..7ed965c91 100644 --- a/tests/playground/draggables/callbacks/index.html +++ b/tests/playground/draggables/callbacks/index.html @@ -30,7 +30,7 @@ z-index: 1; width: 50dvw; height: 50dvh; - border: 1px solid var(--red); + border: 1px solid var(--red-1); } .draggable { display: flex; @@ -38,7 +38,7 @@ align-items: center; width: 4rem; height: 4rem; - background-color: var(--red); + background-color: var(--red-1); } diff --git a/tests/playground/keyframes/index.html b/tests/playground/keyframes/index.html index 378f5690d..8c41ccef7 100644 --- a/tests/playground/keyframes/index.html +++ b/tests/playground/keyframes/index.html @@ -53,7 +53,7 @@ } .css { - background-color: var(--yellow); + background-color: var(--yellow-1); } .css.is-animated { @@ -61,11 +61,11 @@ } .waapi { - background-color: var(--orange); + background-color: var(--orange-1); } .anime { - background-color: var(--red); + background-color: var(--red-1); } diff --git a/tests/playground/layout/clone/index.html b/tests/playground/layout/clone/index.html new file mode 100644 index 000000000..4be179a03 --- /dev/null +++ b/tests/playground/layout/clone/index.html @@ -0,0 +1,98 @@ + + + + Auto layout / anime.js + + + + + + +

        Click on a card to clone it

        +
        +
        â– â– â– â– â– â– â–  â– â–  â– â– â– â– â– â– â– â– â– â–  â– â– â–  â– â– â– â– â– â– â– â–  â– â– â– â–  â– â– â– â– â– â– â– â– â–  â– â– â– â– â– â–  â– â– â– â–  â– â– â– â– â– â– â–  â– â– â–  â– â–  â– â– â– â– â– â– â– â– â–  â– â– â– â– 
        â– â– â– â– â– â– â– â– â–  â– â– â– â– â– â– â–  â– â–  â– â– â– â– â– â– â– â– â–  â– â– â– â–  â– â– â– â– â– â– â– â– â–  â– â–  â– â– â–  â– â– â– â– â– â– â–  â– â– â– â– â– â– â–  â– â– â–  â– â–  â– â– â– â– â– â– â– â– â–  â– â– 
        â– â–  â– â– â– â– â– â– â– â– â–  â– â– â– â– â– â– â–  â– â–  â– â– â– â– â– â– â– â– â–  â– â– â– â–  â– â– â– â– â– â– â– â– â–  â– â–  â– â– â– â– â– â– â– â–  â– â– â– â–  â– â– â– â– â– â– â– â– â–  â– â– â– â– â– â–  â– â– â– â– 
        â– â– â–  â– â–  â– â– â– â– â– â– â– â– â–  â– â– â– â–  â– â– â– â– â– â– â– â– â–  â– â–  â– â– â– â– â– â– â– â–  â– â– â– â– â– â– â–  â– â– 
        â– â– â– â– â– â– â– â– â–  â– â– â– â–  â– â– â– â– â– â– â–  â– â– â– â– â– â– â– â– â– â–  â– â–  â– â– â–  â– â– â– â– â– â– â–  â– â– â– â– â– â– â–  â– â– â–  â– â–  â– â– â– â– â– â– â– â– â–  â– â– â– â– 
        +
        +
        + + + diff --git a/tests/playground/layout/clone/index.js b/tests/playground/layout/clone/index.js new file mode 100644 index 000000000..7d3c653b7 --- /dev/null +++ b/tests/playground/layout/clone/index.js @@ -0,0 +1,20 @@ +import { createLayout, utils } from '../../../dist/modules/index.js'; + +const layout = createLayout('.container', { + properties: ['color'], + duration: 1000 +}); + +const moveCard = $card => { + const $parent = $card.parentElement; + const $nextParent = $parent.nextElementSibling || $parent.previousElementSibling; + $parent.style.zIndex = '0'; + $nextParent.style.zIndex = '1'; + $nextParent.appendChild($card); +} + +document.addEventListener('click', e => { + const $card = e.target.closest('.card'); + console.log($card); + if ($card) layout.update(() => moveCard($card)); +}); diff --git a/tests/playground/layout/index.html b/tests/playground/layout/index.html new file mode 100644 index 000000000..b9ed00b0a --- /dev/null +++ b/tests/playground/layout/index.html @@ -0,0 +1,579 @@ + + + + Layout tests / anime.js + + + + + + +
        + + +
        +
        + +
        +
        +

        Simple absolute root

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        + +
        +
        +

        Simple sticky root

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        + +
        +
        +

        Simple fixed root

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        + +
        +
        +

        Simple relative root

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        + +
        +
        +

        Inlined HTML comments

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        + +
        +
        +

        Simple flex root

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        + +
        +
        +

        Flex root with inlined DIVs

        + +
        +
        +
        +
        A direct
        DIV
        element.
        And a
        sub DIV
        element
        +
        A direct
        DIV
        element.
        And a
        sub DIV
        element
        +
        +
        +
        + +
        +
        +

        Simple static root

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        + +
        +
        +

        Specific children

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        + +
        +
        +

        Specific properties

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Remove with "display: none"

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Remove with "visibility: hidden"

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Custom frozen properties

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Custom removed properties

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Added properties

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Custom timings

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Spring ease

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Swap elements with id

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Swap element outside fixed root

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Stagger timings

        + +
        +
        +
        +
        anim
        +
        rem
        + +
        anim
        +
        rem
        + +
        anim
        +
        rem
        + +
        +
        +
        +
        +
        +

        Stagger values

        + +
        +
        +
        +
        anim
        +
        rem
        + +
        anim
        +
        rem
        + +
        anim
        +
        rem
        + +
        +
        +
        +
        +
        +

        Transformed parent

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +

        Swap element inside root

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        +
        +
        +

        Swap element outside root

        + +
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        A direct child element.
        And a sub child element
        +
        A direct child element.
        And a sub child element
        +
        +
        +
        +
        + + + diff --git a/tests/playground/layout/index.js b/tests/playground/layout/index.js new file mode 100644 index 000000000..13021549e --- /dev/null +++ b/tests/playground/layout/index.js @@ -0,0 +1,359 @@ +import { createLayout, utils, createScope, stagger, spring } from '../../../dist/modules/index.js'; + +const duration = 1000; + +const [ $animateAll ] = utils.$('#animate-all'); +const [ $revertAll ] = utils.$('#revert-all'); + +$animateAll.addEventListener('click', () => { + tests.forEach(test => { + test.data.$button.click(); + }) +}) + +$revertAll.addEventListener('click', () => { + tests.forEach(test => { + test.revert(); + }) +}) + +const tests = [ + createScope({ root: '#simple-fixed-root' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#simple-absolute-root' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#simple-sticky-root' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#simple-relative-root' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#simple-static-root' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#inlined-comments' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#simple-flex-root' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#flex-root-inline-divs' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#specific-children' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + children: ['.child', '.child > span'], + }); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#specific-properties' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + properties: ['background-color', 'color'], + }); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#hide-display-none' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + children: ['.child', '.child > span'], + }); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + const $child = root.querySelector('.child'); + root.classList.toggle('vertical'); + $child.style.display = $child.style.display === 'none' ? 'block' : 'none'; + }, { duration }) + }); + }), + createScope({ root: '#hide-visibility-hidden' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + children: ['.child', '.child > span'], + }); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + const $child = root.querySelector('.child'); + root.classList.toggle('vertical'); + $child.style.visibility = $child.style.visibility === 'hidden' ? 'visible' : 'hidden'; + }, { duration }) + }); + }), + createScope({ root: '#frozen-properties' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + children: ['.child', '.child > span'], + swapAt: { + color: 'var(--red-1)', + borderColor: 'var(--red-1)', + filter: 'blur(5px)', + transform: 'scale(.8)', + ease: 'inOutExpo' + }, + }); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration, frozen: { color: 'var(--green-1)' } }) + }); + }), + createScope({ root: '#removed-properties' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + children: ['.child', '.child > span'], + properties: ['background-color'], + leaveTo: { + transform: 'rotate(45deg) scale(0)', + }, + }); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + const $child = root.querySelector('.child'); + root.classList.toggle('vertical'); + $child.style.display = $child.style.display === 'none' ? 'block' : 'none'; + }, { duration, leaveTo: { 'background-color': 'var(--red-1)' } }) + }); + }), + createScope({ root: '#added-properties' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + properties: ['background-color'], + children: ['.child', '.child > span'], + enterFrom: { + transform: 'rotate(-45deg) scale(0)', + }, + }); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + const $child = root.querySelector('.child'); + $child.style.display = $child.style.display === 'none' ? 'block' : 'none'; + }, { duration, enterFrom: { 'background-color': 'var(--green-1)' } }) + }); + }), + createScope({ root: '#custom-timings' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + duration: 1000, + ease: 'outExpo', + }); + data.$button.addEventListener('click', () => { + const isVertical = data.layout.root.classList.contains('vertical'); + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { + delay: stagger([0, 400], { from: isVertical ? 'last' : 'first' }), + duration, + }) + }); + }), + createScope({ root: '#spring-ease' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + duration: 1000, + ease: spring(), + delay: stagger(25) + }); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#layout-id' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + }, { duration }) + }); + }), + createScope({ root: '#swap-outside-fixed-root' }).add(({ root, data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.demo', { + delay: stagger([0, 100]), + }); + data.$button.addEventListener('click', () => { + const $newContainer = root.querySelector('.container.target-container'); + const $oldContainer = root.querySelector('.container:not(.target-container)'); + data.layout.update(() => { + $newContainer.classList.remove('target-container'); + $oldContainer.classList.add('target-container'); + }, { duration }) + }); + }), + createScope({ root: '#swap-inside-root' }).add(({ data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.demo'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.querySelectorAll('.container').forEach($container => $container.classList.toggle('vertical')); + const $child = root.querySelector('.child'); + const $parent = $child.parentElement; + const $nextParent = $parent.nextElementSibling || $parent.previousElementSibling; + $parent.style.zIndex = '0'; + $nextParent.style.zIndex = '1'; + $nextParent.appendChild($child); + }, { duration }) + }); + }), + createScope({ root: '#swap-outside-root' }).add(({ root, data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container-b', { + children: ['.container', '.child', '.child > span'], + delay: stagger([0, 100]), + }); + data.$button.addEventListener('click', () => { + const $newContainer = root.querySelector('.container.target-container'); + const $oldContainer = root.querySelector('.container:not(.target-container)'); + data.layout.update(({root}) => { + root.classList.toggle('vertical'); + $newContainer.classList.remove('target-container'); + $oldContainer.classList.add('target-container'); + }, { duration }) + }); + }), + createScope({ root: '#stagger-timings' }).add(({ root, data }) => { + data.$button = utils.$('button')[0]; + const $container = utils.$('.container'); + data.layout = createLayout('.container', { + children: '.child', + delay: stagger(200), + onBegin: () => { + utils.set($container, { + background: 'var(--sky-2)', + }) + }, + onComplete: () => { + utils.set($container, { + background: 'var(--green-2)', + }) + }, + enterFrom: { + opacity: 0, + background: 'var(--green-1)', + transform: 'translateX(-100%)', + }, + leaveTo: { + opacity: 0, + background: 'var(--red-1)', + transform: 'translateX(100%)', + }, + swapAt: { + opacity: 0, + background: 'var(--yellow-1)', + } + }); + data.$button.addEventListener('click', () => { + const $added = root.querySelectorAll('.child-added'); + const $removed = root.querySelectorAll('.child-removed'); + data.layout.update(({root}) => { + const isReverse = root.classList.contains('reverse'); + root.classList.toggle('reverse'); + root.classList.toggle('vertical'); + utils.set($added, { display: isReverse ? 'block' : 'none' }); + utils.set($removed, { display: isReverse ? 'none' : 'block' }); + }, { duration }) + }); + }), + createScope({ root: '#stagger-values' }).add(({ root, data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container', { + children: '.child', + delay: stagger(200), + enterFrom: { + opacity: 0, + innerHTML: (_, i, targets) => `enter: ${i}, ${targets.length}`, + }, + leaveTo: { + opacity: 0, + innerHTML: (_, i, targets) => `leave: ${i}, ${targets.length}`, + }, + swapAt: { + opacity: 0, + innerHTML: (_, i, targets) => `swap: ${i}, ${targets.length}`, + } + }); + data.$button.addEventListener('click', () => { + const $added = root.querySelectorAll('.child-added'); + const $removed = root.querySelectorAll('.child-removed'); + data.layout.update(({root}) => { + const isReverse = root.classList.contains('reverse'); + root.classList.toggle('reverse'); + root.classList.toggle('vertical'); + utils.set($added, { display: isReverse ? 'block' : 'none' }); + utils.set($removed, { display: isReverse ? 'none' : 'block' }); + }, { duration }) + }); + }), + createScope({ root: '#transformed-parent' }).add(({ root, data }) => { + data.$button = utils.$('button')[0]; + data.layout = createLayout('.container'); + data.$button.addEventListener('click', () => { + data.layout.update(({ root }) => { + root.classList.toggle('vertical'); + root.style.transform = `rotateX(${utils.random(-75, 75)}deg) rotateY(${utils.random(-75, 75)}deg) scale(${utils.random(.75, 1, 2)})`; + }, { duration }) + }); + }) +] \ No newline at end of file diff --git a/tests/playground/lerp/index.html b/tests/playground/lerp/index.html index 934596b85..d2bea8ee3 100644 --- a/tests/playground/lerp/index.html +++ b/tests/playground/lerp/index.html @@ -28,7 +28,7 @@ .square { width: 100px; height: 100px; - background: var(--red); + background: var(--red-1); border-radius: 1rem; padding: 1rem; } diff --git a/tests/playground/onscroll/assets/onscroll.css b/tests/playground/onscroll/assets/onscroll.css index d990efc83..4224e094d 100644 --- a/tests/playground/onscroll/assets/onscroll.css +++ b/tests/playground/onscroll/assets/onscroll.css @@ -4,8 +4,8 @@ section { align-items: center; width: 100%; min-height: 100lvh; - border-top: 1px dotted var(--red); - border-bottom: 1px dotted var(--red); + border-top: 1px dotted var(--red-1); + border-bottom: 1px dotted var(--red-1); } section.spacer { @@ -66,14 +66,14 @@ h2 { z-index: 0; background-image: url(card-back.svg); transform: rotateY(180deg); - background-color: var(--white); + background-color: var(--white-1); } .front { z-index: 1; backface-visibility: hidden; background-image: url(card.svg); - background-color: var(--white); + background-color: var(--white-1); background-size: 89%; background-repeat: no-repeat; } diff --git a/tests/playground/onscroll/debug.html b/tests/playground/onscroll/debug.html index 2313196a6..b29058df9 100644 --- a/tests/playground/onscroll/debug.html +++ b/tests/playground/onscroll/debug.html @@ -44,7 +44,7 @@ .target { width: 100px; height: 100px; - background: var(--red); + background: var(--red-1); } diff --git a/tests/playground/onscroll/function-based-values.html b/tests/playground/onscroll/function-based-values.html index cbfb90f3c..79887a34d 100644 --- a/tests/playground/onscroll/function-based-values.html +++ b/tests/playground/onscroll/function-based-values.html @@ -39,7 +39,7 @@ flex-shrink: 0; width: 100vw; height: 100lvh; - border: 1px dotted var(--green); + border: 1px dotted var(--green-1); } @media (orientation: landscape) { diff --git a/tests/playground/onscroll/horizontal-container.html b/tests/playground/onscroll/horizontal-container.html index ef0218eeb..7bc033042 100644 --- a/tests/playground/onscroll/horizontal-container.html +++ b/tests/playground/onscroll/horizontal-container.html @@ -26,8 +26,8 @@ transform-style: preserve-3d; overflow-x: auto; overflow-y: hidden; - border: 1px dotted var(--green); - background-color: var(--black); + border: 1px dotted var(--green-1); + background-color: var(--black-1); } .sticky-scroller { transform-style: preserve-3d; diff --git a/tests/playground/onscroll/standalone.html b/tests/playground/onscroll/standalone.html index 1cf82fb59..173af0578 100644 --- a/tests/playground/onscroll/standalone.html +++ b/tests/playground/onscroll/standalone.html @@ -44,7 +44,7 @@ margin: 1rem; padding: .5rem; border-radius: 1rem; - background-color: var(--white); + background-color: var(--white-1); } diff --git a/tests/playground/onscroll/sticky-snap.html b/tests/playground/onscroll/sticky-snap.html index bb3a28255..54e6c6252 100644 --- a/tests/playground/onscroll/sticky-snap.html +++ b/tests/playground/onscroll/sticky-snap.html @@ -21,7 +21,7 @@ height: calc(100vh - 1px); scroll-snap-align: start; scroll-snap-stop: always; - border: 1px dotted var(--red); + border: 1px dotted var(--red-1); } .half { width: 50%; @@ -37,7 +37,7 @@ position: sticky; top: 0; height: 100vh; - border: 1px dotted var(--green); + border: 1px dotted var(--green-1); } .stack { border: 2px dashed rgba(255,255,255,.25); diff --git a/tests/playground/onscroll/svg-target.html b/tests/playground/onscroll/svg-target.html index 48ff85455..cd753bcc3 100644 --- a/tests/playground/onscroll/svg-target.html +++ b/tests/playground/onscroll/svg-target.html @@ -40,7 +40,7 @@ flex-shrink: 0; width: 100vw; height: 100lvh; - border: 1px dotted var(--green); + border: 1px dotted var(--green-1); } body { diff --git a/tests/playground/sandbox/index.html b/tests/playground/sandbox/index.html index 3a62ee880..c7ab64d24 100644 --- a/tests/playground/sandbox/index.html +++ b/tests/playground/sandbox/index.html @@ -6,46 +6,17 @@ -
        -
        -
        -
        -
        -
        +
        +
        +
        diff --git a/tests/playground/sandbox/index.js b/tests/playground/sandbox/index.js index 2f9aa7366..56bed6102 100644 --- a/tests/playground/sandbox/index.js +++ b/tests/playground/sandbox/index.js @@ -1,5 +1,10 @@ -import { waapi, animate, onScroll, $, set, stagger, spring, random, utils } from '../../../dist/modules/index.js'; +import { createTimeline, waapi } from '../../../dist/modules/index.js'; -async function animation() { - await animate('test', { x: 100 }); -} \ No newline at end of file +const { animate } = waapi; + +const red = animate('.red', { x: '15rem', autoplay: false }); +const blue = animate('.blue', { x: '15rem', autoplay: false }); + +const tl = createTimeline({loop: 1}) +.sync(red, 0) +.sync(blue, 500); \ No newline at end of file diff --git a/tests/playground/scope/index.html b/tests/playground/scope/index.html index f861a039b..6824bed0b 100644 --- a/tests/playground/scope/index.html +++ b/tests/playground/scope/index.html @@ -27,7 +27,7 @@ .square { width: 100px; height: 100px; - background: var(--red); + background: var(--red-1); border-radius: 1rem; padding: 1rem; } diff --git a/tests/playground/scope/index.js b/tests/playground/scope/index.js index a4e44b692..0834652cc 100644 --- a/tests/playground/scope/index.js +++ b/tests/playground/scope/index.js @@ -42,8 +42,6 @@ const scope = createScope({ alternate: true })); - - // Recreate the animation while keeping track of its current time between mediaquery changes // self.keepTime(() => animate('.square', { // scale: self.matches.isSmall ? .5 : 1.5, @@ -70,7 +68,7 @@ const scope = createScope({ } self.keepTime(() => animate('.square', { - background: ($el) => utils.get($el, utils.randomPick(['--skyblue', '--lavender', '--pink'])), + background: ($el) => utils.get($el, utils.randomPick(['--cyan-1', '--lavender-1', '--pink-1'])), loop: true, ease: 'inOut(2)', alternate: true, @@ -90,4 +88,4 @@ const scope = createScope({ document.body.addEventListener('click', () => { console.log('REVERT'); scope.revert(); -}) \ No newline at end of file +}); diff --git a/tests/playground/scramble/index.html b/tests/playground/scramble/index.html new file mode 100644 index 000000000..144191c45 --- /dev/null +++ b/tests/playground/scramble/index.html @@ -0,0 +1,176 @@ + + + + Scramble Text / Anime.js + + + + + + +
        +
        +
        Default
        +
        Each character cycles through random glyphs before settling into place
        +
        +
        +
        From right
        +
        The reveal wave starts from the rightmost character and sweeps left, unscrambling each glyph in reverse order until the full string has settled into place
        +
        +
        +
        From center
        +
        Characters resolve outward from the midpoint of the string, spreading symmetrically in both directions until the first and last characters settle into their final place
        +
        +
        +
        From random
        +
        Each character picks a random position in the reveal sequence, so the text resolves in an unpredictable order with no directional bias from left to right
        +
        +
        +
        Binary characters
        +
        The scramble pool is limited to zeros and ones, replacing every unrevealed character with binary digits to create a data stream aesthetic before the final text appears
        +
        +
        +
        With ease
        +
        An easing curve shapes the reveal progress, so characters settle slowly at first, accelerate through the middle, then slow down again toward the end of the animation
        +
        +
        +
        Staggered
        +
        Each element scrambles its characters before settling left to right
        +
        A stagger delay offsets each animation from the one before it
        +
        The sequence completes as the final line resolves into place
        +
        +
        +
        Low CPS
        +
        A low characters per second rate means scramble glyphs refresh less often, so each random character lingers visibly before being replaced by the next one
        +
        +
        +
        From index 20
        +
        The reveal originates at character index twenty and spreads outward from that point, resolving characters in order of their distance from the chosen origin position
        +
        +
        +
        Cursor
        +
        An underscore character forms a leading cursor that sweeps just ahead of the reveal boundary, marking positions that are about to settle before the final glyph appears
        +
        +
        +
        Cursor pattern (█▓▒░)
        +
        A block gradient pattern forms a growing cursor front that sweeps across the string, fading from solid to light before each character settles into its final glyph
        +
        +
        +
        Perturbation 0.4
        +
        With perturbation at 0.4, most characters follow the natural reveal order while a portion settle slightly ahead or behind their expected position in the sequence
        +
        +
        +
        Seeded (42)
        +
        Providing a numeric seed to the generator means the scramble pattern, reveal order, and character choices are all identical on every replay for fully reproducible animations
        +
        +
        +
        Cursor + perturbation + center
        +
        Cursor, center origin, and perturbation combine so characters burst outward from the middle through a sweeping glyph front before resolving into the final text
        +
        +
        +
        Text longer
        +
        Short text
        +
        +
        +
        Text shorter
        +
        This is a longer piece of text that will scramble down to a much shorter string
        +
        +
        +
        Text longer + space override
        +
        Short text
        +
        +
        +
        Text shorter + from right
        +
        This text shrinks from right to left, with extra characters scrambling away before the shorter target settles into place
        +
        +
        +
        Reversed
        +
        Reversing a center origin makes characters settle from the outer edges inward, converging toward the middle instead of the usual outward spread from the center
        +
        +
        +
        Blank start
        +
        Starting from a blank string, each character appears from nothing, cycling through random glyphs before settling into its final position
        +
        +
        +
        Duration 500ms
        +
        A short duration makes the entire scramble resolve quickly, with characters cycling through fewer random glyphs before settling into place
        +
        +
        +
        Duration 3000ms
        +
        A long duration stretches the scramble over more time, letting characters linger in their randomized state before gradually resolving
        +
        +
        +
        Settle duration 100ms
        +
        A short settle duration means each character snaps into its final glyph almost instantly once the reveal front reaches its position
        +
        +
        +
        Settle duration 800ms
        +
        A long settle duration lets each character keep cycling through random glyphs for a while after being reached by the reveal front
        +
        +
        +
        Reveal rate 100
        +
        A high reveal rate means characters enter the active zone quickly, creating a fast sweeping reveal effect
        +
        +
        +
        Reveal rate 10
        +
        A low reveal rate means characters enter the active zone slowly, so the reveal wave takes longer to sweep across the text
        +
        +
        +
        Resize loop
        +
        Hi
        +
        +
        + + + diff --git a/tests/playground/scramble/index.js b/tests/playground/scramble/index.js new file mode 100644 index 000000000..5c94ac924 --- /dev/null +++ b/tests/playground/scramble/index.js @@ -0,0 +1,64 @@ +import { animate, createTimeline, scrambleText, stagger } from '../../../dist/modules/index.js'; + +const demos = [ + { id: 'default', params: null }, + { id: 'from-end', params: { from: 'right' } }, + { id: 'from-center', params: { from: 'center' } }, + { id: 'from-random', params: { from: 'random' } }, + { id: 'binary', params: { chars: '01' } }, + { id: 'eased', params: { ease: 'inOut(2)' } }, + { id: 'staggered', params: {}, animParams: { delay: stagger(200) } }, + { id: 'low-cps', params: { settleRate: 5 } }, + { id: 'from-index', params: { from: 20 } }, + { id: 'cursor', params: { cursor: true } }, + { id: 'cursor-pattern', params: { cursor: 'â–‘â–’â–“â–ˆ' } }, + { id: 'low-perturbation', params: { perturbation: 0.4 } }, + { id: 'seeded', params: { seed: 42 } }, + { id: 'combined', params: { cursor: true, perturbation: 0.5, from: 'center' } }, + { id: 'text-longer', params: { text: 'This text is significantly longer than the original short text, growing smoothly with a scramble transition effect' } }, + { id: 'text-shorter', params: { text: 'Short' } }, + { id: 'text-longer-space', params: { text: 'This text grows with space override padding to maintain consistent width during the transition', override: ' ' } }, + { id: 'text-shorter-end', params: { text: 'End', from: 'right' } }, + { id: 'reversed', params: { from: 'center', reversed: true } }, + { id: 'blank-start', params: { override: '' } }, + { id: 'duration-short', params: { duration: 500 } }, + { id: 'duration-long', params: { duration: 3000 } }, + { id: 'settle-short', params: { settleDuration: 100 } }, + { id: 'settle-long', params: { settleDuration: 800 } }, + { id: 'interval-short', params: { revealRate: 100 } }, + { id: 'interval-long', params: { revealRate: 10 } }, +]; + +for (let i = 0, l = demos.length; i < l; i++) { + const demo = demos[i]; + const $test = document.getElementById(demo.id); + const $els = $test.querySelectorAll('.scramble'); + const anim = animate($els, { + innerHTML: scrambleText(demo.params), + duration: 1500, + ...demo.animParams, + }); + const replay = () => anim.restart(); + $test.addEventListener('pointerenter', replay); + $test.addEventListener('pointerdown', replay); +} + +const resizeTexts = [ + 'Hi', + 'Hello World', + 'The quick brown fox jumps over the lazy dog and keeps on running', + 'Pack my box with five dozen liquor jugs and then add a few more for good measure', + 'Nope', + 'The five boxing wizards jump quickly over the lazy dog while the crowd watches in stunned silence', + 'Done', +]; + +const $resizeEl = document.querySelector('#resize-loop .scramble'); +const resizeTl = createTimeline({ loop: true }); +for (let i = 1, l = resizeTexts.length; i < l; i++) { + resizeTl.add($resizeEl, { + innerHTML: scrambleText({ text: resizeTexts[i], override: false }), + duration: 1500, + }, '+=800'); +} +resizeTl.init(); diff --git a/tests/playground/sprite-animation.html b/tests/playground/sprite-animation.html index 4d13732b0..824738284 100644 --- a/tests/playground/sprite-animation.html +++ b/tests/playground/sprite-animation.html @@ -43,11 +43,11 @@ + + diff --git a/tests/playground/svg-morph-timeline/index.js b/tests/playground/svg-morph-timeline/index.js new file mode 100644 index 000000000..18c4b5689 --- /dev/null +++ b/tests/playground/svg-morph-timeline/index.js @@ -0,0 +1,25 @@ +import { createTimeline, svg } from '../../../dist/modules/index.js'; + +const duration = 800; + +// Path: A -> B -> C +const tl2 = createTimeline({ loop: true, defaults: { duration, ease: 'inOutQuad' } }); +tl2.add('#morph-2', { d: svg.morphTo('#shape-b') }); +tl2.add('#morph-2', { d: svg.morphTo('#shape-c') }); + +// Path: A -> B -> C -> D +const tl3 = createTimeline({ loop: true, defaults: { duration, ease: 'inOutQuad' } }); +tl3.add('#morph-3', { d: svg.morphTo('#shape-b') }); +tl3.add('#morph-3', { d: svg.morphTo('#shape-c') }); +tl3.add('#morph-3', { d: svg.morphTo('#shape-d') }); + +// Path: A -> B -> C -> A (loop back to original) +const tlLoop = createTimeline({ loop: true, defaults: { duration, ease: 'inOutQuad' } }); +tlLoop.add('#morph-loop', { d: svg.morphTo('#shape-b') }); +tlLoop.add('#morph-loop', { d: svg.morphTo('#shape-c') }); +tlLoop.add('#morph-loop', { d: svg.morphTo('#shape-a') }); + +// Polygon: A -> B -> C +const tlPoly = createTimeline({ loop: true, defaults: { duration, ease: 'inOutQuad' } }); +tlPoly.add('#morph-poly', { points: svg.morphTo('#poly-b') }); +tlPoly.add('#morph-poly', { points: svg.morphTo('#poly-c') }); diff --git a/tests/playground/svg-motion-path/index.html b/tests/playground/svg-motion-path/index.html index b9d213d53..dab2c1fd6 100644 --- a/tests/playground/svg-motion-path/index.html +++ b/tests/playground/svg-motion-path/index.html @@ -34,11 +34,11 @@ left: -1rem; width: 2rem; height: 2rem; - color: var(--red); + color: var(--red-1); border: 2px solid currentColor; } .rect-el { - stroke: var(--green); + stroke: var(--green-1); } diff --git a/tests/playground/timekeeper/index.html b/tests/playground/timekeeper/index.html index 708ff6d17..2640e862e 100644 --- a/tests/playground/timekeeper/index.html +++ b/tests/playground/timekeeper/index.html @@ -29,7 +29,7 @@ .square { width: 100px; height: 100px; - background: var(--red); + background: var(--red-1); border-radius: 1rem; padding: 1rem; } diff --git a/tests/playground/timeline/nested/index.html b/tests/playground/timeline/nested/index.html index 16d255a4b..879b785e6 100644 --- a/tests/playground/timeline/nested/index.html +++ b/tests/playground/timeline/nested/index.html @@ -25,7 +25,7 @@ height: 4rem; margin: 1rem; border-radius: .5rem; - background: var(--red); + background: var(--red-1); } diff --git a/tests/playground/tween-composition.html b/tests/playground/tween-composition.html index 977460edd..0584650f6 100644 --- a/tests/playground/tween-composition.html +++ b/tests/playground/tween-composition.html @@ -73,7 +73,7 @@ const green = '#18FF74'; const darkgreen = '#00D672'; const turquoise = '#3CFFEC'; - const skyblue = '#61C3FF'; + const blue = '#61C3FF'; const kingblue = '#5A87FF'; const lavender = '#8453E3'; const purple = '#C26EFF'; @@ -108,7 +108,7 @@ scale: [{ from: 1.4, to: .5, duration: 4000 }, { to: 1.6, duration: 2000 }], rotate: [{ to: 45, duration: 2000 }], zIndex: { to: 999, modifier: utils.round(0), ease: 'linear', duration: 25 }, - color: { to: el.dataset.clicked ? skyblue : orange, duration: 300, ease: 'out(4)' }, + color: { to: el.dataset.clicked ? blue : orange, duration: 300, ease: 'out(4)' }, boxShadow: '0 0 10px 0 rgba(0, 0, 0, .3)', duration: 900, onBegin() { el.dataset.hover = true; }, diff --git a/tests/playground/waapi/composition/index.html b/tests/playground/waapi/composition/index.html index f9fa23a55..4778e6e8b 100644 --- a/tests/playground/waapi/composition/index.html +++ b/tests/playground/waapi/composition/index.html @@ -33,13 +33,13 @@ background-color: currentColor; } .container-A .square { - color: var(--red); + color: var(--red-1); } .container-B .square { - color: var(--orange); + color: var(--orange-1); } .container-C .square { - color: var(--yellow); + color: var(--yellow-1); } diff --git a/tests/playground/waapi/eases/index.js b/tests/playground/waapi/eases/index.js index 2b0ddaace..61d2d8f1b 100644 --- a/tests/playground/waapi/eases/index.js +++ b/tests/playground/waapi/eases/index.js @@ -18,7 +18,7 @@ for (let i = 0; i < total; i++) { // targets.forEach(($el, i) => { // animations[i] = $el.animate({ // transform: `rotate(${(i / total) * 360}deg) translateY(200px) scaleX(.25)`, -// backgroundColor: [`var(--orange)`, `var(--red)`], +// backgroundColor: [`var(--orange-1)`, `var(--red-1)`], // }, { // easing: 'linear(0, 0.02, 0.0749, 0.1573, 0.2596, 0.3749, 0.4966, 0.6191, 0.7374, 0.8476, 0.9467, 1.0325, 1.1038, 1.16, 1.2014, 1.2285, 1.2425, 1.245, 1.2376, 1.222, 1.2003, 1.1742, 1.1453, 1.1152, 1.0854, 1.0568, 1.0304, 1.007, 0.9869, 0.9704, 0.9576, 0.9484, 0.9427, 0.9401, 0.9402, 0.9426, 0.9468, 0.9525, 0.9592, 0.9664, 0.9737, 0.981, 0.9879, 0.9942, 0.9997, 1.0044, 1.0082, 1.0111, 1.0131, 1.0143, 1.0148, 1.0146, 1.0138, 1.0127, 1.0112, 1.0095, 1.0078, 1.0059, 1.0042, 1.0025, 1.001, 0.9997, 0.9986, 0.9978, 0.9971, 0.9967, 0.9964, 0.9965, 0.9967, 0.997, 0.9974, 0.9978, 0.9982, 0.9987, 0.9991, 0.9995, 0.9998, 1.0001, 1.0004, 1.0006, 1.0007, 1.0008, 1.0009, 1.0008, 1.0007, 1.0006, 1.0005, 1.0004, 1.0003, 1.0002, 1.0001, 1, 0.9999, 0.9998, 1)', // iterations: Infinity, @@ -38,7 +38,7 @@ for (let i = 0; i < total; i++) { waapi.animate('.square', { transform: (_, i) => `rotate(${(i / total) * 360}deg) translateY(200px) scaleX(.25)`, backgroundColor: { - to: [`var(--orange)`, `var(--red)`], + to: [`var(--orange-1)`, `var(--red-1)`], ease: 'linear', duration: 3000, }, diff --git a/tests/playground/waapi/playback/index.html b/tests/playground/waapi/playback/index.html index 71ca90a19..36c2c51af 100644 --- a/tests/playground/waapi/playback/index.html +++ b/tests/playground/waapi/playback/index.html @@ -34,7 +34,7 @@ margin-bottom: 10rem; } .square { - --bg: var(--green); + --bg: var(--green-1); position: relative; width: 4rem; height: 4rem; diff --git a/tests/playground/waapi/playback/index.js b/tests/playground/waapi/playback/index.js index d375a5ed2..5fa556c4b 100644 --- a/tests/playground/waapi/playback/index.js +++ b/tests/playground/waapi/playback/index.js @@ -40,7 +40,7 @@ const scope = createScope({ }) const scrollAnim = waapi.animate('.square', { - translate: ($el, i, t) => `0px ${stagger([-20, 20])($el, i, t)}rem`, + translate: ($el, i, targets) => `0px ${stagger([-20, 20])($el, i, targets)}rem`, rotate: `90deg`, delay: stagger(100), reversed: true, diff --git a/tests/playground/waapi/values/index.html b/tests/playground/waapi/values/index.html index 7fc158ef5..5bfda8c3c 100644 --- a/tests/playground/waapi/values/index.html +++ b/tests/playground/waapi/values/index.html @@ -31,7 +31,7 @@ height: 1em; border-radius: 0px; background-color: currentColor; - color: var(--red); + color: var(--red-1); } diff --git a/tests/run.js b/tests/run.js index a8593b29c..0da912c7e 100644 --- a/tests/run.js +++ b/tests/run.js @@ -24,6 +24,7 @@ import './suites/text.test.js'; import './suites/units.test.js'; import './suites/utils.test.js'; import './suites/values.test.js'; +import './suites/transforms.test.js'; import './suites/colors.test.js'; import './suites/eases.test.js'; import './suites/leaks.test.js'; diff --git a/tests/setup.js b/tests/setup.js index 0283f9132..5ac05ab97 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -76,6 +76,9 @@ mocha.setup({

        +
        +
        Hello World
        +
        `; done(); } diff --git a/tests/suites/animations.test.js b/tests/suites/animations.test.js index 86ef67682..377ff6a25 100644 --- a/tests/suites/animations.test.js +++ b/tests/suites/animations.test.js @@ -70,8 +70,6 @@ suite('Animations', () => { skewX: 100, skewY: 100, perspective: 100, - matrix: 100, - matrix3d: 100, duration: 100, }); diff --git a/tests/suites/colors.test.js b/tests/suites/colors.test.js index d75518336..cc85d6b13 100644 --- a/tests/suites/colors.test.js +++ b/tests/suites/colors.test.js @@ -89,20 +89,60 @@ function createColorTestsByType(fromType, toType) { } } -suite('Colors', () => { +const domColorInputs = { + rgb: { + HEX: { input: '#FF4B4B', output: [255, 75, 75, 1] }, + RGB: { input: 'rgb(255, 168, 40)', output: [255, 168, 40, 1] }, + HSL: { input: 'hsl(44, 100%, 59%)', output: [255, 199, 46, 1] }, + }, + rgba: { + HEXA: { input: '#FF4B4B33', output: [255, 75, 75, .2] }, + RGBA: { input: 'rgba(255, 168, 40, .2)', output: [255, 168, 40, .2] }, + HSLA: { input: 'hsla(44, 100%, 59%, .2)', output: [255, 199, 46, .2] }, + }, +}; + +const domColorProperties = ['background', 'color', 'backgroundColor', 'borderColor']; - test('Properly apply transparency from computed styles', resolve => { +function createColorConversionFromDOMTest(property, colorType, colorName) { + const { input, output } = domColorInputs[colorType][colorName]; + const testName = `Animate ${property} to ${colorName}`; + return test(testName, () => { const [ targetEl ] = utils.$('#target-id'); - animate(targetEl, { - backgroundColor: 'rgba(0, 0, 0, 0)', - duration: 10, - onComplete: () => { - expect(targetEl.style.backgroundColor).to.equal('rgba(0, 0, 0, 0)'); - resolve(); + const animation = animate(targetEl, { [property]: input, autoplay: false }); + expect(getChildAtIndex(animation, 0)._valueType).to.equal(valueTypes.COLOR); + expect(getChildAtIndex(animation, 0)._toNumbers).to.deep.equal(output); + animation.seek(animation.duration); + if (colorType === 'rgba') { + expect(targetEl.style[property]).to.equal(`rgba(${output[0]}, ${output[1]}, ${output[2]}, ${output[3]})`); + } else { + expect(targetEl.style[property]).to.equal(`rgb(${output[0]}, ${output[1]}, ${output[2]})`); + } + }); +} + +function createColorConversionFromDOMTests() { + domColorProperties.forEach(property => { + for (let colorType in domColorInputs) { + for (let colorName in domColorInputs[colorType]) { + createColorConversionFromDOMTest(property, colorType, colorName); } - }); + } }); +} +suite('Colors', () => { + test('boxShadow should not be parsed as color value', () => { + const [ targetEl ] = utils.$('#target-id'); + targetEl.style.boxShadow = `0px 0px 6px red`; + const computedShadowA = getComputedStyle(targetEl).boxShadow; + targetEl.style.boxShadow = `inset 0px 0px 20px yellow`; + const computedShadowB = getComputedStyle(targetEl).boxShadow; + // computedShadowA: 'rgb(255, 0, 0) 0px 0px 6px 0px'; computedShadowB: 'rgb(255, 255, 0) 0px 0px 20px 0px inset'; + const animation = animate(targetEl, { boxShadow: [computedShadowA, computedShadowB], autoplay: false }); + expect(getChildAtIndex(animation, 0)._valueType).to.equal(valueTypes.COMPLEX); // Fails and return valueTypes.COLOR + }); + createColorConversionFromDOMTests(); createColorTestsByType('rgb', 'rgb'); createColorTestsByType('rgb', 'rgba'); createColorTestsByType('rgba', 'rgb'); diff --git a/tests/suites/controls.test.js b/tests/suites/controls.test.js index 1351de458..5a0e67ef3 100644 --- a/tests/suites/controls.test.js +++ b/tests/suites/controls.test.js @@ -204,7 +204,7 @@ suite('Controls', () => { expect(tl.currentTime).to.equal(tl.duration); expect(getChildAtIndex(tl, 0).currentTime).to.equal(getChildAtIndex(tl, 0).duration); expect(getChildAtIndex(tl, 1).currentTime).to.equal(getChildAtIndex(tl, 1).duration); - expect($target.getAttribute('style')).to.equal('height: 400px; transform: translateY(200px) translateX(200px); width: 64px;'); + expect($target.getAttribute('style')).to.equal('height: 400px; transform: translate(200px, 200px); width: 64px;'); }); test('Cancel a timer', () => { @@ -251,7 +251,7 @@ suite('Controls', () => { expect(tl.paused).to.equal(true); expect(getChildAtIndex(tl, 0).paused).to.equal(true); expect(getChildAtIndex(tl, 1).paused).to.equal(true); - expect($target.getAttribute('style')).to.equal('height: 200px; transform: translateY(100px) translateX(0px); width: 32px;'); + expect($target.getAttribute('style')).to.equal('height: 200px; transform: translate(0px, 100px); width: 32px;'); }); test('Revert a timeline', () => { @@ -655,6 +655,101 @@ suite('Controls', () => { expect(utils.get($target, 'rotate')).to.equal('20deg'); }); + test('Refresh an animation from value', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + $target.setAttribute('data-width', '200px'); + const animation1 = animate($target, { + width: [() => $target.dataset.width, '400px'], + duration: 100, + ease: 'linear', + autoplay: false + }); + animation1.seek(0); + expect($target.style.width).to.equal('200px'); + animation1.seek(50); + expect($target.style.width).to.equal('300px'); + animation1.seek(100); + expect($target.style.width).to.equal('400px'); + $target.setAttribute('data-width', '100px'); + animation1.refresh().restart().seek(0); + expect($target.style.width).to.equal('100px'); + animation1.seek(50); + expect($target.style.width).to.equal('250px'); + animation1.seek(100); + expect($target.style.width).to.equal('400px'); + }); + + test('Refresh an animation from value with unit conversion', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + $target.setAttribute('data-width', '50vw'); + const animation1 = animate($target, { + width: [() => $target.dataset.width, '400px'], + duration: 100, + ease: 'linear', + autoplay: false + }); + const initialFromWidth = parseFloat($target.style.width); + animation1.seek(100); + expect($target.style.width).to.equal('400px'); + $target.setAttribute('data-width', '25vw'); + animation1.refresh().restart().seek(0); + const refreshedFromWidth = parseFloat($target.style.width); + // After refresh, from value should be recalculated (25vw converted to px) + expect(refreshedFromWidth).to.be.below(initialFromWidth); + animation1.seek(100); + expect($target.style.width).to.equal('400px'); + }); + + test('Refresh an animation with from / to values', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + $target.setAttribute('data-from', '100px'); + $target.setAttribute('data-to', '200px'); + const animation1 = animate($target, { + width: [() => $target.dataset.from, () => $target.dataset.to], + duration: 100, + ease: 'linear', + autoplay: false + }); + animation1.seek(0); + expect($target.style.width).to.equal('100px'); + animation1.seek(100); + expect($target.style.width).to.equal('200px'); + $target.setAttribute('data-from', '50px'); + $target.setAttribute('data-to', '150px'); + animation1.refresh().restart().seek(0); + expect($target.style.width).to.equal('50px'); + animation1.seek(50); + expect($target.style.width).to.equal('100px'); + animation1.seek(100); + expect($target.style.width).to.equal('150px'); + }); + + test('Refresh an animation with from, to, unit conversion and relative operator', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + $target.setAttribute('data-from', '10vw'); + const animation1 = animate($target, { + width: [() => $target.dataset.from, () => '+=100'], + duration: 100, + ease: 'linear', + autoplay: false + }); + animation1.seek(0); + const initialFromWidth = parseFloat($target.style.width); + animation1.seek(100); + const initialToWidth = parseFloat($target.style.width); + // To value should be from + 100px + expect(initialToWidth).to.equal(initialFromWidth + 100); + $target.setAttribute('data-from', '20vw'); + animation1.refresh().restart().seek(0); + const refreshedFromWidth = parseFloat($target.style.width); + // After refresh, from value should be larger (20vw > 10vw) + expect(refreshedFromWidth).to.be.above(initialFromWidth); + animation1.seek(100); + const refreshedToWidth = parseFloat($target.style.width); + // To value should still be from + 100px (relative operator applied to new from) + expect(refreshedToWidth).to.equal(refreshedFromWidth + 100); + }); + test('Refresh a timeline', () => { const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); $target.setAttribute('data-width', '128px'); diff --git a/tests/suites/engine.test.js b/tests/suites/engine.test.js index 419a358a5..5261fa24e 100644 --- a/tests/suites/engine.test.js +++ b/tests/suites/engine.test.js @@ -26,7 +26,7 @@ suite('Engine', () => { }); expect(animation1._offset).to.be.above(50); // Above the setTimeout value expect(animation1._offset).to.be.below(animation2._offset); // Below animation2._offset - expect(animation2._offset).to.be.above(animation1._offset + 15); + expect(animation2._offset).to.be.above(animation1._offset); resolve(); } }); diff --git a/tests/suites/function-based-values.test.js b/tests/suites/function-based-values.test.js index ba31eca0c..01f4ee3af 100644 --- a/tests/suites/function-based-values.test.js +++ b/tests/suites/function-based-values.test.js @@ -18,16 +18,19 @@ suite('Function based values', () => { const $targets = document.querySelectorAll('.target-class'); const animation = animate($targets, { autoplay: false, - translateX: (el, i, total) => { + translateX: (el, i, targets) => { return el.getAttribute('data-index'); }, - duration: (el, i, total) => { + duration: (el, i, targets) => { const index = parseFloat(el.dataset.index); - return total + ((i + index) * 100); + return targets.length + ((i + index) * 100); }, - delay: (el, i, total) => { + delay: (el, i, targets) => { const index = parseFloat(el.dataset.index); - return total + ((i + index) * 100); + return targets.length + ((i + index) * 100); + }, + ease: () => { + return (/** @type {Number} */t) => t; }, }); @@ -83,8 +86,8 @@ suite('Function based values', () => { const animation = animate($targets, { autoplay: false, translateX: [ - { to: el => el.getAttribute('data-index') * 100, duration: stagger(100), delay: stagger(100) }, - { to: el => el.getAttribute('data-index') * 50, duration: stagger(100), delay: stagger(100) } + { to: el => el.getAttribute('data-index') * 100, duration: stagger(100), delay: stagger(100), ease: () => t => t }, + { to: el => el.getAttribute('data-index') * 50, duration: stagger(100), delay: stagger(100), ease: () => t => t } ], }); @@ -141,7 +144,7 @@ suite('Function based values', () => { const $targets = document.querySelectorAll('.target-class'); const animation = animate($targets, { autoplay: false, - translateX: ($el, i, t) => [$el.dataset.index, (t - 1) - i], + translateX: ($el, i, targets) => [$el.dataset.index, (targets.length - 1) - i], }); // From @@ -162,8 +165,8 @@ suite('Function based values', () => { const animation = animate($targets, { autoplay: false, translateX: [ - ($el, i, t) => $el.dataset.index, - ($el, i, t) => (t - 1) - i + ($el, i, targets) => $el.dataset.index, + ($el, i, targets) => (targets.length - 1) - i ], }); @@ -185,8 +188,8 @@ suite('Function based values', () => { // const animation = animate($targets, { // autoplay: false, // translateX: [ - // ($el, i, t) => $el.dataset.index, - // ($el, i, t) => utils.ran + // ($el, i, targets) => $el.dataset.index, + // ($el, i, targets) => utils.ran // ], // }); diff --git a/tests/suites/keyframes.test.js b/tests/suites/keyframes.test.js index 4edaa3c29..fe509cf39 100644 --- a/tests/suites/keyframes.test.js +++ b/tests/suites/keyframes.test.js @@ -284,11 +284,11 @@ suite('Keyframes', () => { // Easing should be continuous throughout the sequence animation.seek(250); - expect($target.style.transform).to.equal('translateY(-25px) translateX(0px)'); + expect($target.style.transform).to.equal('translate(0px, -25px)'); animation.seek(500); - expect($target.style.transform).to.equal('translateY(0px) translateX(250px)'); + expect($target.style.transform).to.equal('translate(250px, 0px)'); animation.seek(750); - expect($target.style.transform).to.equal('translateY(25px) translateX(0px)'); + expect($target.style.transform).to.equal('translate(0px, 25px)'); }); test('Percentage based keyframes values', () => { @@ -309,15 +309,15 @@ suite('Keyframes', () => { // Easing should be continuous throughout the sequence animation.seek(0); - expect($target.style.transform).to.equal('translateX(100px) translateY(100px)'); + expect($target.style.transform).to.equal('translate(100px, 100px)'); animation.seek(200); - expect($target.style.transform).to.equal('translateX(-100px) translateY(60px)'); + expect($target.style.transform).to.equal('translate(-100px, 60px)'); animation.seek(500); - expect($target.style.transform).to.equal('translateX(100px) translateY(0px)'); + expect($target.style.transform).to.equal('translate(100px, 0px)'); animation.seek(800); - expect($target.style.transform).to.equal('translateX(-100px) translateY(-60px)'); + expect($target.style.transform).to.equal('translate(-100px, -60px)'); animation.seek(1000); - expect($target.style.transform).to.equal('translateX(100px) translateY(-100px)'); + expect($target.style.transform).to.equal('translate(100px, -100px)'); }); test('Percentage based keyframes with float percentage values', () => { diff --git a/tests/suites/parameters.test.js b/tests/suites/parameters.test.js index 44a1ace0f..a53f10aaf 100644 --- a/tests/suites/parameters.test.js +++ b/tests/suites/parameters.test.js @@ -211,10 +211,10 @@ suite('Parameters', () => { expect(getTweenDelay(getChildAtIndex(animation, 2))).to.equal(duration * (.1)); expect(getChildAtIndex(animation, 2)._changeDuration).to.equal(duration * .70); - expect(targetEl.style.transform).to.equal('translateX(0px) rotate(0deg) translateY(0px)'); + expect(targetEl.style.transform).to.equal('translate(0px, 0px) rotate(0deg)'); animation.pause(); animation.seek(animation.duration * .5); - expect(targetEl.style.transform).to.equal('translateX(66.7px) rotate(302.4deg) translateY(134.69px)'); + expect(targetEl.style.transform).to.equal('translate(66.7px, 134.69px) rotate(302.4deg)'); }); test('Specific property parameters on transforms values when last transform value update after everything else', resolve => { @@ -286,7 +286,7 @@ suite('Parameters', () => { }); expect(animation.duration).to.equal(minValue); - expect(targetEl.style.transform).to.equal('translateX(100px) translateY(100px)'); + expect(targetEl.style.transform).to.equal('translate(100px, 100px)'); }); test('0 duration timeline with infinite loop', () => { diff --git a/tests/suites/scroll.test.js b/tests/suites/scroll.test.js index b56e15ee4..8a228cb19 100644 --- a/tests/suites/scroll.test.js +++ b/tests/suites/scroll.test.js @@ -6,10 +6,166 @@ import { onScroll, scrollContainers, animate, + createTimeline, utils } from '../../dist/modules/index.js'; suite('Scroll', () => { + test('ScrollObserver with custom id', () => { + const observer = onScroll({ + target: '#target-id', + id: 'my-scroll-observer', + }); + expect(observer.id).to.equal('my-scroll-observer'); + observer.revert(); + }); + + test('ScrollObserver horizontal axis', resolve => { + const observer = onScroll({ + target: '#target-id', + axis: 'x', + }); + utils.sync(() => { + expect(observer.horizontal).to.equal(true); + observer.revert(); + resolve(); + }); + }); + + test('ScrollObserver with sync mode', () => { + const observer = onScroll({ + target: '#target-id', + sync: true, + }); + expect(observer.sync).to.equal(true); + expect(observer.syncSmooth).to.equal(1); + observer.revert(); + }); + + test('ScrollObserver with sync smooth value', () => { + const observer = onScroll({ + target: '#target-id', + sync: 0.5, + }); + expect(observer.sync).to.equal(true); + expect(observer.syncSmooth).to.equal(0.5); + observer.revert(); + }); + + test('ScrollObserver with sync linear', () => { + const observer = onScroll({ + target: '#target-id', + sync: 'linear', + }); + expect(observer.sync).to.equal(true); + expect(observer.syncSmooth).to.equal(1); + observer.revert(); + }); + + test('ScrollObserver callbacks are assigned', () => { + let enterCalled = false; + let leaveCalled = false; + let updateCalled = false; + let resizeCalled = false; + const observer = onScroll({ + target: '#target-id', + onEnter: () => { enterCalled = true; }, + onLeave: () => { leaveCalled = true; }, + onUpdate: () => { updateCalled = true; }, + onResize: () => { resizeCalled = true; }, + }); + expect(typeof observer.onEnter).to.equal('function'); + expect(typeof observer.onLeave).to.equal('function'); + expect(typeof observer.onUpdate).to.equal('function'); + expect(typeof observer.onResize).to.equal('function'); + observer.revert(); + }); + + test('ScrollObserver link() method', () => { + const animation = animate('#target-id', { + x: 100, + autoplay: false, + }); + const observer = onScroll({ + target: '#target-id', + }).link(animation); + expect(observer.linked).to.equal(animation); + expect(animation.paused).to.equal(true); + observer.revert(); + animation.revert(); + }); + + test('ScrollObserver linked via autoplay', () => { + const observer = onScroll({ + target: '#target-id', + }); + const animation = animate('#target-id', { + x: 100, + autoplay: observer, + }); + expect(observer.linked).to.equal(animation); + observer.revert(); + animation.revert(); + }); + + test('ScrollObserver linked timeline via autoplay', () => { + const observer = onScroll({ + target: '#target-id', + }); + const tl = createTimeline({ + autoplay: observer, + }) + .add('#target-id', { x: 100 }) + .add('#target-id', { y: 100 }); + expect(observer.linked).to.equal(tl); + observer.revert(); + tl.revert(); + }); + + test('ScrollObserver repeat false should revert after completion', resolve => { + const observer = onScroll({ + target: '#target-id', + repeat: false, + }); + utils.sync(() => { + expect(observer.repeat).to.equal(false); + observer.revert(); + resolve(); + }); + }); + + test('ScrollObserver refresh() updates bounds', resolve => { + const observer = onScroll({ + target: '#target-id', + }); + utils.sync(() => { + observer.refresh(); + expect(observer.offset).to.be.a('number'); + expect(observer.offsetStart).to.be.a('number'); + expect(observer.offsetEnd).to.be.a('number'); + expect(observer.distance).to.be.a('number'); + observer.revert(); + resolve(); + }); + }); + + test('ScrollObserver onResize callback is triggered on container refresh', resolve => { + let resizeCount = 0; + const observer = onScroll({ + target: '#target-id', + onResize: () => { resizeCount++; }, + }); + utils.sync(() => { + expect(resizeCount).to.equal(0); + observer.container.refreshScrollObservers(); + expect(resizeCount).to.equal(1); + observer.container.refreshScrollObservers(); + expect(resizeCount).to.equal(2); + observer.revert(); + resolve(); + }); + }); + test('Reverting an animation with onScroll should also revert the ScrollObserver', () => { const [ $container ] = utils.$('#css-tests'); const animation = animate('#target-id', { @@ -20,7 +176,7 @@ suite('Scroll', () => { }); expect(scrollContainers.get($container)).to.not.equal(undefined); $container.remove(); - $container.width = '100px'; + $container.style.width = '100px'; animation.revert(); expect(scrollContainers.get($container)).to.equal(undefined); }); diff --git a/tests/suites/stagger.test.js b/tests/suites/stagger.test.js index b2d29e9cc..4ae51f881 100644 --- a/tests/suites/stagger.test.js +++ b/tests/suites/stagger.test.js @@ -310,4 +310,144 @@ suite('Stagger', () => { expect(getChildAtIndex(tl, 2)._head._toNumber).to.equal(100); expect(getChildAtIndex(tl, 3)._head._toNumber).to.equal(0); }); + + test('Grid staggering with from as [x, y] array', () => { + const animation = animate('#grid div', { + scale: [1, 0], + delay: stagger(10, {grid: [5, 3], from: [0.5, 0.5]}), + autoplay: false + }); + + expect(getTweenDelay(getChildAtIndex(animation, 0))).to.be.closeTo(22.4, .0001); + expect(getTweenDelay(getChildAtIndex(animation, 1))).to.be.closeTo(14.1, .01); + expect(getTweenDelay(getChildAtIndex(animation, 2))).to.equal(10); + expect(getTweenDelay(getChildAtIndex(animation, 3))).to.be.closeTo(14.1, .01); + expect(getTweenDelay(getChildAtIndex(animation, 4))).to.be.closeTo(22.4, .0001); + + expect(getTweenDelay(getChildAtIndex(animation, 5))).to.equal(20); + expect(getTweenDelay(getChildAtIndex(animation, 6))).to.equal(10); + expect(getTweenDelay(getChildAtIndex(animation, 7))).to.equal(0); + expect(getTweenDelay(getChildAtIndex(animation, 8))).to.equal(10); + expect(getTweenDelay(getChildAtIndex(animation, 9))).to.equal(20); + + expect(getTweenDelay(getChildAtIndex(animation, 10))).to.be.closeTo(22.4, .0001); + expect(getTweenDelay(getChildAtIndex(animation, 11))).to.be.closeTo(14.1, .01); + expect(getTweenDelay(getChildAtIndex(animation, 12))).to.equal(10); + expect(getTweenDelay(getChildAtIndex(animation, 13))).to.be.closeTo(14.1, .01); + expect(getTweenDelay(getChildAtIndex(animation, 14))).to.be.closeTo(22.4, .0001); + }); + + test('Auto grid staggering with DOM elements', () => { + const animation = animate('#grid div', { + scale: [1, 0], + delay: stagger(10, {grid: true, from: 'center'}), + autoplay: false + }); + + // Center element should have delay 0 + expect(getTweenDelay(getChildAtIndex(animation, 7))).to.equal(0); + + // Symmetric elements should have equal delays + // Middle row: left/right of center + expect(getTweenDelay(getChildAtIndex(animation, 6))).to.equal(getTweenDelay(getChildAtIndex(animation, 8))); + expect(getTweenDelay(getChildAtIndex(animation, 5))).to.equal(getTweenDelay(getChildAtIndex(animation, 9))); + + // Center column: top/bottom of center + expect(getTweenDelay(getChildAtIndex(animation, 2))).to.equal(getTweenDelay(getChildAtIndex(animation, 12))); + + // All four corners should have equal delays + const cornerDelay = getTweenDelay(getChildAtIndex(animation, 0)); + expect(getTweenDelay(getChildAtIndex(animation, 4))).to.equal(cornerDelay); + expect(getTweenDelay(getChildAtIndex(animation, 10))).to.equal(cornerDelay); + expect(getTweenDelay(getChildAtIndex(animation, 14))).to.equal(cornerDelay); + + // Corners should have the largest delay + expect(cornerDelay).to.be.greaterThan(getTweenDelay(getChildAtIndex(animation, 6))); + expect(cornerDelay).to.be.greaterThan(getTweenDelay(getChildAtIndex(animation, 2))); + }); + + test('Auto grid staggering with JS objects from center', () => { + const targets = []; + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 5; col++) { + targets.push({ x: col, y: row, val: 1 }); + } + } + const animation = animate(targets, { + val: 0, + delay: stagger(10, { grid: true, from: 'center' }), + autoplay: false, + }); + + expect(getTweenDelay(getChildAtIndex(animation, 0))).to.be.closeTo(22.4, .0001); + expect(getTweenDelay(getChildAtIndex(animation, 1))).to.be.closeTo(14.1, .01); + expect(getTweenDelay(getChildAtIndex(animation, 2))).to.equal(10); + expect(getTweenDelay(getChildAtIndex(animation, 3))).to.be.closeTo(14.1, .01); + expect(getTweenDelay(getChildAtIndex(animation, 4))).to.be.closeTo(22.4, .0001); + + expect(getTweenDelay(getChildAtIndex(animation, 5))).to.equal(20); + expect(getTweenDelay(getChildAtIndex(animation, 6))).to.equal(10); + expect(getTweenDelay(getChildAtIndex(animation, 7))).to.equal(0); + expect(getTweenDelay(getChildAtIndex(animation, 8))).to.equal(10); + expect(getTweenDelay(getChildAtIndex(animation, 9))).to.equal(20); + + expect(getTweenDelay(getChildAtIndex(animation, 10))).to.be.closeTo(22.4, .0001); + expect(getTweenDelay(getChildAtIndex(animation, 11))).to.be.closeTo(14.1, .01); + expect(getTweenDelay(getChildAtIndex(animation, 12))).to.equal(10); + expect(getTweenDelay(getChildAtIndex(animation, 13))).to.be.closeTo(14.1, .01); + expect(getTweenDelay(getChildAtIndex(animation, 14))).to.be.closeTo(22.4, .0001); + }); + + test('Auto grid staggering with from as [x, y] array', () => { + const targets = []; + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 5; col++) { + targets.push({ x: col, y: row, val: 1 }); + } + } + const animation = animate(targets, { + val: 0, + delay: stagger(10, { grid: true, from: [1, 1] }), + autoplay: false, + }); + + expect(getTweenDelay(getChildAtIndex(animation, 14))).to.equal(0); + expect(getTweenDelay(getChildAtIndex(animation, 0))).to.be.closeTo(44.7, .01); + expect(getTweenDelay(getChildAtIndex(animation, 4))).to.equal(20); + expect(getTweenDelay(getChildAtIndex(animation, 10))).to.equal(40); + }); + + test('Auto grid staggering with axis parameter', () => { + const targets = []; + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 5; col++) { + targets.push({ x: col, y: row, val: 0 }); + } + } + const animation = animate(targets, { + val: stagger(10, { grid: true, from: 'center', axis: 'x' }), + autoplay: false, + }); + + expect(getChildAtIndex(animation, 0)._toNumber).to.equal(-20); + expect(getChildAtIndex(animation, 1)._toNumber).to.equal(-10); + expect(getChildAtIndex(animation, 2)._toNumber).to.equal(0); + expect(getChildAtIndex(animation, 3)._toNumber).to.equal(10); + expect(getChildAtIndex(animation, 4)._toNumber).to.equal(20); + expect(getChildAtIndex(animation, 5)._toNumber).to.equal(-20); + expect(getChildAtIndex(animation, 10)._toNumber).to.equal(-20); + }); + + test('Auto grid staggering fallback to 1D without spatial data', () => { + const targets = [{ val: 1 }, { val: 1 }, { val: 1 }, { val: 1 }]; + const animation = animate(targets, { + val: 0, + delay: stagger(10, { grid: true }), + autoplay: false, + }); + expect(getTweenDelay(getChildAtIndex(animation, 0))).to.equal(0); + expect(getTweenDelay(getChildAtIndex(animation, 1))).to.equal(10); + expect(getTweenDelay(getChildAtIndex(animation, 2))).to.equal(20); + expect(getTweenDelay(getChildAtIndex(animation, 3))).to.equal(30); + }); }); diff --git a/tests/suites/svg.test.js b/tests/suites/svg.test.js index 5236c9178..a85644e5e 100644 --- a/tests/suites/svg.test.js +++ b/tests/suites/svg.test.js @@ -1,12 +1,12 @@ import { expect, - getChildAtIndex, } from '../utils.js'; import { animate, utils, svg, + createTimeline, } from '../../dist/modules/index.js'; suite('SVG', () => { @@ -153,7 +153,7 @@ suite('SVG', () => { }); // Scale property should be set as a CSS transform on non SVG filter elements - expect(filterPolygonEl.style.transform).to.equal('translateX(430px) translateY(35px) scale(0.75)'); + expect(filterPolygonEl.style.transform).to.equal('translate(430px, 35px) scale(0.75)'); // Non stylistic SVG attribute should be declared in came case expect(feTurbulenceEl.hasAttribute('baseFrequency')).to.equal(true); @@ -163,6 +163,7 @@ suite('SVG', () => { }); test('svg.createMotionPath with offset', resolve => { + /** @type {HTMLElement} */ const squareEl = document.querySelector('#square'); const [pathEl] = utils.$('#tests path'); const pathSelector = 'motion-path-offset-test'; @@ -195,4 +196,84 @@ suite('SVG', () => { resolve(); }); + test('morphTo morphs path d attribute from one shape to another', resolve => { + const $path = document.querySelector('#path'); + const originalD = $path.getAttribute('d'); + const anim = animate($path, { + d: svg.morphTo('#polygon'), + duration: 10, + autoplay: false, + }); + anim.seek(0); + expect($path.getAttribute('d')).to.not.equal(originalD); + anim.seek(anim.duration); + const endD = $path.getAttribute('d'); + expect(endD).to.not.equal(originalD); + resolve(); + }); + + test('morphTo morphs polygon points attribute', resolve => { + const $polygon = document.querySelector('#polygon'); + const originalPoints = $polygon.getAttribute('points'); + const anim = animate($polygon, { + points: svg.morphTo('#polyline', 0), + duration: 10, + autoplay: false, + }); + anim.seek(anim.duration); + expect($polygon.getAttribute('points')).to.not.equal(originalPoints); + resolve(); + }); + + test('morphTo timeline chaining reads previous end value', resolve => { + const $polygon = document.querySelector('#polygon'); + const originalPoints = $polygon.getAttribute('points'); + // Set polygon points to match polyline so DOM-based reads produce a polyline->polyline no-op + $polygon.setAttribute('points', document.querySelector('#polyline').getAttribute('points')); + const tl = createTimeline({ defaults: { duration: 10 }, autoplay: false }); + tl.add($polygon, { points: svg.morphTo('#path') }); + tl.add($polygon, { points: svg.morphTo('#polyline') }); + tl.seek(15); + const midPoints = $polygon.getAttribute('points'); + tl.seek(tl.duration); + const endPoints = $polygon.getAttribute('points'); + // If prevTween works, second morph goes path->polyline (different mid vs end) + // If broken, second morph goes polyline->polyline (mid equals end) + expect(midPoints).to.not.equal(endPoints); + $polygon.setAttribute('points', originalPoints); + resolve(); + }); + + test('morphTo keyframe array reads prevTween value between keyframes', resolve => { + const $path = document.querySelector('#path'); + const originalD = $path.getAttribute('d'); + const anim = animate($path, { + d: [{ to: svg.morphTo('#polygon') }, { to: svg.morphTo('#polyline') }], + duration: 100, + autoplay: false, + }); + anim.seek(50); + const midD = $path.getAttribute('d'); + anim.seek(anim.duration); + const endD = $path.getAttribute('d'); + // Both keyframes produce different endpoints (second morph starts from polygon, not original) + expect(midD).to.not.equal(originalD); + expect(endD).to.not.equal(originalD); + expect(midD).to.not.equal(endD); + $path.setAttribute('d', originalD); + resolve(); + }); + + test('morphTo revert restores original path', resolve => { + const $path = document.querySelector('#path'); + const originalD = $path.getAttribute('d'); + const tl = createTimeline({ defaults: { duration: 10 }, autoplay: false }); + tl.add($path, { d: svg.morphTo('#polygon') }); + tl.seek(tl.duration); + expect($path.getAttribute('d')).to.not.equal(originalD); + tl.revert(); + expect($path.getAttribute('d')).to.equal(originalD); + resolve(); + }); + }); diff --git a/tests/suites/text.test.js b/tests/suites/text.test.js index 46fcd3d04..84a32db58 100644 --- a/tests/suites/text.test.js +++ b/tests/suites/text.test.js @@ -7,6 +7,8 @@ import { utils, splitText, animate, + scrambleText, + createTimeline, } from '../../dist/modules/index.js'; // Firefox detect Japanse words differently @@ -428,4 +430,137 @@ suite('Text', () => { }); }); + test('scrambleText returns a function-based value', () => { + const value = scrambleText(); + expect(typeof value).to.equal('function'); + }); + + test('scrambleText returns a tween object with from, to, ease, duration and modifier', () => { + const $el = document.querySelector('#scramble-text'); + const tweenObj = scrambleText()($el); + expect(tweenObj.from).to.equal(0); + expect(tweenObj.to).to.equal(1); + expect(tweenObj.ease).to.equal('linear'); + expect(tweenObj.duration).to.be.above(0); + expect(typeof tweenObj.modifier).to.equal('function'); + }); + + test('scrambleText default override scrambles text at start', () => { + const $el = document.querySelector('#scramble-text'); + const tweenObj = scrambleText({ seed: 1 })($el); + expect(tweenObj.modifier(0)).to.not.equal($el.textContent); + }); + + test('scrambleText override false preserves original text at start', () => { + const $el = document.querySelector('#scramble-text'); + const tweenObj = scrambleText({ override: false })($el); + expect(tweenObj.modifier(0)).to.equal($el.textContent); + }); + + test('scrambleText modifier returns target text at 1', () => { + const $el = document.querySelector('#scramble-text'); + const tweenObj = scrambleText()($el); + expect(tweenObj.modifier(1)).to.equal($el.textContent); + }); + + test('scrambleText modifier returns scrambled text for intermediate values', () => { + const $el = document.querySelector('#scramble-text'); + const tweenObj = scrambleText({ seed: 1 })($el); + const result = tweenObj.modifier(.5); + expect(result.length).to.equal($el.textContent.length); + expect(result).to.not.equal($el.textContent); + }); + + test('scrambleText modifier preserves whitespace', () => { + const $el = document.querySelector('#scramble-text'); + const tweenObj = scrambleText({ seed: 1 })($el); + const result = tweenObj.modifier(.125); + const spaceIndex = $el.textContent.indexOf(' '); + expect(result[spaceIndex]).to.equal(' '); + }); + + test('scrambleText modifier caches value for same input', () => { + const $el = document.querySelector('#scramble-text'); + const tweenObj = scrambleText()($el); + const first = tweenObj.modifier(.25); + const second = tweenObj.modifier(.25); + expect(first).to.equal(second); + }); + + test('scrambleText custom chars option', () => { + const $el = document.querySelector('#scramble-text'); + const tweenObj = scrambleText({ chars: 'XY', seed: 1 })($el); + const result = tweenObj.modifier(.5); + for (let i = 0; i < result.length; i++) { + if (result[i] !== ' ' && result[i] !== $el.textContent[i]) { + expect(result[i] === 'X' || result[i] === 'Y').to.equal(true); + } + } + }); + + test('scrambleText progressive reveal increases revealed characters', () => { + const $el = document.querySelector('#scramble-text'); + const originalText = $el.textContent; + const tweenObj = scrambleText({ seed: 1 })($el); + const at25 = tweenObj.modifier(.25); + const at75 = tweenObj.modifier(.75); + let revealed25 = 0; + let revealed75 = 0; + for (let i = 0; i < originalText.length; i++) { + if (at25[i] === originalText[i] && originalText[i] !== ' ') revealed25++; + if (at75[i] === originalText[i] && originalText[i] !== ' ') revealed75++; + } + expect(revealed75).to.be.above(revealed25); + }); + + test('scrambleText custom text param transitions to different text', () => { + const $el = document.querySelector('#scramble-text'); + const tweenObj = scrambleText({ text: 'Goodbye' })($el); + expect(tweenObj.modifier(1)).to.equal('Goodbye'); + }); + + test('scrambleText works with animate()', resolve => { + const $el = document.querySelector('#scramble-text'); + const originalText = $el.textContent; + animate($el, { + innerHTML: scrambleText(), + duration: 100, + autoplay: true, + onComplete: () => { + expect($el.innerHTML).to.equal(originalText); + resolve(); + } + }); + }); + + test('scrambleText keyframe array reads prevTween value between keyframes', resolve => { + const $el = document.querySelector('#scramble-text'); + $el.textContent = ''; + const anim = animate($el, { + innerHTML: [ + { to: scrambleText({ text: 'First', duration: 100 }) }, + { to: scrambleText({ text: 'Second', duration: 100 }) }, + ], + autoplay: false, + }); + // End of first keyframe: should equal first target text + anim.seek(100); + expect($el.innerHTML).to.equal('First'); + // End of second keyframe: proves prevTween._value was read as starting text + anim.seek(anim.duration); + expect($el.innerHTML).to.equal('Second'); + resolve(); + }); + + test('scrambleText timeline chaining reads previous end value', resolve => { + const $el = document.querySelector('#scramble-text'); + $el.textContent = ''; + const tl = createTimeline({ defaults: { duration: 100 }, autoplay: false }); + tl.add($el, { innerHTML: scrambleText({ text: 'Hello' }) }); + tl.add($el, { innerHTML: scrambleText({ override: false }) }); + tl.seek(tl.duration); + expect($el.innerHTML).to.equal('Hello'); + resolve(); + }); + }); diff --git a/tests/suites/timelines.test.js b/tests/suites/timelines.test.js index 636aff17e..18509ca7d 100644 --- a/tests/suites/timelines.test.js +++ b/tests/suites/timelines.test.js @@ -500,13 +500,13 @@ suite('Timelines', () => { }, '-=5') tl.seek(15) - expect($target.style.transform).to.equal('translateX(200px) translateY(100px)'); + expect($target.style.transform).to.equal('translate(200px, 100px)'); tl.seek(16) - expect($target.style.transform).to.equal('translateX(185px) translateY(90px)'); + expect($target.style.transform).to.equal('translate(185px, 90px)'); tl.seek(15) - expect($target.style.transform).to.equal('translateX(200px) translateY(100px)'); + expect($target.style.transform).to.equal('translate(200px, 100px)'); tl.seek(14) - expect($target.style.transform).to.equal('translateX(185px) translateY(90px)'); + expect($target.style.transform).to.equal('translate(185px, 90px)'); }); test('Previous tween before last shouln\'t render on loop', resolve => { @@ -946,5 +946,45 @@ suite('Timelines', () => { tl.remove(animation); }); + test('Animating a child timeline progress should not corrupt parent render state via nested tick', () => { + const [ $target1, $target2 ] = utils.$('.target-class'); + + const innerTL = createTimeline({ + autoplay: false, + defaults: { duration: 100, ease: 'linear' }, + }) + .add($target1, { translateX: 200 }); + + let call1Count = 0; + let call2Count = 0; + + const parentTL = createTimeline({ + autoplay: false, + defaults: { ease: 'linear' }, + }) + .call(() => { call1Count += 1; }, 0) + .add(innerTL, { progress: 1, duration: 200 }) + .add($target2, { translateY: 200, duration: 200 }, 0) + .call(() => { call2Count += 1; }, 200); + + parentTL.seek(100); + expect(call1Count).to.equal(1); + expect($target1.style.transform).to.equal('translateX(100px)'); + expect($target2.style.transform).to.equal('translateY(100px)'); + + parentTL.seek(200); + expect(call2Count).to.equal(1); + expect($target1.style.transform).to.equal('translateX(200px)'); + expect($target2.style.transform).to.equal('translateY(200px)'); + + parentTL.seek(100); + expect($target1.style.transform).to.equal('translateX(100px)'); + expect($target2.style.transform).to.equal('translateY(100px)'); + expect(call2Count).to.equal(2); + + parentTL.seek(0); + expect(call1Count).to.equal(2); + }); + }); diff --git a/tests/suites/transforms.test.js b/tests/suites/transforms.test.js new file mode 100644 index 000000000..c67b88c2e --- /dev/null +++ b/tests/suites/transforms.test.js @@ -0,0 +1,1200 @@ +import { + expect, +} from '../utils.js'; + +import { + animate, + utils, +} from '../../dist/modules/index.js'; + +suite('Transforms', () => { + + // --- Parsing inline shorthand transforms --- + + test('Parse inline translate(x, y) shorthand', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate(20px, 30px)'; + expect(utils.get($target, 'translateX')).to.equal('20px'); + expect(utils.get($target, 'translateY')).to.equal('30px'); + }); + + test('Parse inline translate(x) single value', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate(15px)'; + expect(utils.get($target, 'translateX')).to.equal('15px'); + }); + + test('Parse inline translate3d(x, y, z)', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate3d(10px, 20px, 30px)'; + expect(utils.get($target, 'translateX')).to.equal('10px'); + expect(utils.get($target, 'translateY')).to.equal('20px'); + expect(utils.get($target, 'translateZ')).to.equal('30px'); + }); + + test('Parse inline translate3d with calc values', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate3d(calc(10px + 5vw), calc(20px - 3vh), 50px)'; + expect(utils.get($target, 'translateX')).to.equal('calc(10px + 5vw)'); + expect(utils.get($target, 'translateY')).to.equal('calc(20px - 3vh)'); + expect(utils.get($target, 'translateZ')).to.equal('50px'); + }); + + test('Parse inline translate3d with nested calc parens', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate3d(calc((100% - 50px) / 2), calc(var(--h) + 10px), 0px)'; + expect(utils.get($target, 'translateX')).to.equal('calc((100% - 50px) / 2)'); + expect(utils.get($target, 'translateY')).to.equal('calc(var(--h) + 10px)'); + expect(utils.get($target, 'translateZ')).to.equal('0px'); + }); + + test('Parse inline translate with calc and clamp values', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate(clamp(10px, 50%, 200px), calc(100vh - 50px))'; + expect(utils.get($target, 'translateX')).to.equal('clamp(10px, 50%, 200px)'); + expect(utils.get($target, 'translateY')).to.equal('calc(-50px + 100vh)'); + }); + + test('Parse inline scale(x, y) shorthand', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'scale(2, 3)'; + expect(utils.get($target, 'scaleX')).to.equal('2'); + expect(utils.get($target, 'scaleY')).to.equal('3'); + }); + + test('Parse inline scale3d(x, y, z)', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'scale3d(1.5, 2, 0.5)'; + expect(utils.get($target, 'scaleX')).to.equal('1.5'); + expect(utils.get($target, 'scaleY')).to.equal('2'); + expect(utils.get($target, 'scaleZ')).to.equal('0.5'); + }); + + test('Parse mixed shorthand and individual inline transforms', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate(5px, 10px) rotate(30deg) scaleX(2)'; + expect(utils.get($target, 'translateX')).to.equal('5px'); + expect(utils.get($target, 'translateY')).to.equal('10px'); + expect(utils.get($target, 'rotate')).to.equal('30deg'); + expect(utils.get($target, 'scaleX')).to.equal('2'); + }); + + test('Parse translate shorthand followed by other transforms', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate(40px, 60px) rotate(45deg) scale3d(2, 2, 1)'; + expect(utils.get($target, 'translateX')).to.equal('40px'); + expect(utils.get($target, 'translateY')).to.equal('60px'); + expect(utils.get($target, 'rotate')).to.equal('45deg'); + expect(utils.get($target, 'scaleX')).to.equal('2'); + expect(utils.get($target, 'scaleY')).to.equal('2'); + expect(utils.get($target, 'scaleZ')).to.equal('1'); + }); + + test('Parse inline scale(single_value)', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'scale(0.75)'; + expect(utils.get($target, 'scale')).to.equal('0.75'); + }); + + test('Parse inline individual rotateX', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'rotateX(45deg)'; + expect(utils.get($target, 'rotateX')).to.equal('45deg'); + }); + + test('Parse complex inline transform with all types combined', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'perspective(800px) translate3d(10px, 20px, 30px) rotate(45deg) rotateX(15deg) scale3d(1.5, 2, 1) skewX(10deg) skewY(5deg)'; + expect(utils.get($target, 'perspective')).to.equal('800px'); + expect(utils.get($target, 'translateX')).to.equal('10px'); + expect(utils.get($target, 'translateY')).to.equal('20px'); + expect(utils.get($target, 'translateZ')).to.equal('30px'); + expect(utils.get($target, 'rotate')).to.equal('45deg'); + expect(utils.get($target, 'rotateX')).to.equal('15deg'); + expect(utils.get($target, 'scaleX')).to.equal('1.5'); + expect(utils.get($target, 'scaleY')).to.equal('2'); + expect(utils.get($target, 'scaleZ')).to.equal('1'); + expect(utils.get($target, 'skewX')).to.equal('10deg'); + expect(utils.get($target, 'skewY')).to.equal('5deg'); + }); + + test('Parse translate(x, y) with calc containing nested parens', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate(calc((100vw - 300px) / 2), calc(var(--offset, 10px) * 2))'; + expect(utils.get($target, 'translateX')).to.equal('calc((100vw - 300px) / 2)'); + expect(utils.get($target, 'translateY')).to.equal('calc(var(--offset, 10px) * 2)'); + }); + + // --- Anime.js x/y/z shorthand properties --- + + test('Render x shorthand as translateX', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + x: 50, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'translateX')).to.equal('50px'); + expect($target.style.transform).to.equal('translateX(50px)'); + }); + + test('Render y shorthand as translateY', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + y: 100, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'translateY')).to.equal('100px'); + expect($target.style.transform).to.equal('translateY(100px)'); + }); + + test('Render z shorthand as translateZ', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + z: 50, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'translateZ')).to.equal('50px'); + expect($target.style.transform).to.equal('translateZ(50px)'); + }); + + test('Render x and y shorthands produce translate(x, y)', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + x: 100, + y: 200, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(100px, 200px)'); + }); + + test('Render x, y, z shorthands produce translate3d(x, y, z)', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + x: 100, + y: 200, + z: 300, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(100px, 200px, 300px)'); + }); + + test('Animate z with inline translate3d preserves x and y', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate3d(10px, 20px, 30px)'; + const animation = animate($target, { z: 300, duration: 10 }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(10px, 20px, 300px)'); + }); + + test('Animate x with inline translate3d preserves y and z', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate3d(10px, 20px, 30px)'; + const animation = animate($target, { x: 100, duration: 10 }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(100px, 20px, 30px)'); + }); + + test('Animate y with inline translate3d preserves x and z', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate3d(10px, 20px, 30px)'; + const animation = animate($target, { y: 200, duration: 10 }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(10px, 200px, 30px)'); + }); + + test('Animate x and y with inline translate3d preserves z', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate3d(10px, 20px, 30px)'; + const animation = animate($target, { x: 100, y: 200, duration: 10 }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(100px, 200px, 30px)'); + }); + + test('Animate z with inline translate3d and other transforms', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate3d(10px, 20px, 30px) rotate(45deg)'; + const animation = animate($target, { z: 300, duration: 10 }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(10px, 20px, 300px) rotate(45deg)'); + }); + + test('Render x and z shorthands without y produce individual transforms', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + x: 50, + z: 100, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translateX(50px) translateZ(100px)'); + }); + + test('Render x/y shorthands with other transforms', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + x: 50, + y: 100, + rotate: 45, + scale: 1.5, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(50px, 100px) rotate(45deg) scale(1.5)'); + }); + + test('Render x shorthand with string unit value', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + x: '5rem', + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'translateX')).to.equal('5rem'); + }); + + test('Render x/y/z shorthands with from-to values', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + x: [0, 100], + y: [0, 200], + z: [0, 300], + ease: 'linear', + duration: 10, + }); + animation.pause().seek(0); + expect($target.style.transform).to.equal('translate3d(0px, 0px, 0px)'); + animation.seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(100px, 200px, 300px)'); + }); + + test('Transforms shorthand properties values', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translateX(10px) translateY(calc(-100px + 10vh)) translateZ(50px) scale(0.75)'; + const animation = animate('#target-id', { + x: 100, + y: 100, + z: 100, + scale: 10, + duration: 10, + }); + expect(utils.get('#target-id', 'x')).to.equal('10px'); + expect(utils.get('#target-id', 'y')).to.equal('calc(-100px + 10vh)'); + expect(utils.get('#target-id', 'z')).to.equal('50px'); + expect(utils.get('#target-id', 'scale')).to.equal('0.75'); + animation.pause().seek(animation.duration); + expect(utils.get('#target-id', 'x')).to.equal('100px'); + expect(utils.get('#target-id', 'y')).to.equal('calc(100px + 100vh)'); + expect(utils.get('#target-id', 'z')).to.equal('100px'); + expect(utils.get('#target-id', 'scale')).to.equal('10'); + }); + + // --- Rendering individual transforms --- + + test('Render individual translateY alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateY: 75, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translateY(75px)'); + }); + + test('Render individual translateZ alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateZ: 100, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translateZ(100px)'); + }); + + test('Render individual skew alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + skew: 20, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('skew(20deg)'); + }); + + test('Render translateY and translateZ without translateX', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateY: 50, + translateZ: 100, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translateY(50px) translateZ(100px)'); + }); + + test('Render individual translateX when translateY is absent', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: 50, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translateX(50px)'); + }); + + test('Render individual translateX and translateZ without translateY', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: 50, + translateZ: 100, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translateX(50px) translateZ(100px)'); + }); + + test('Render individual scaleX alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + scaleX: 2, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'scaleX')).to.equal('2'); + }); + + test('Render individual scaleY alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + scaleY: 3, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'scaleY')).to.equal('3'); + }); + + test('Render individual scaleZ alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + scaleZ: 0.5, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'scaleZ')).to.equal('0.5'); + }); + + test('Render individual rotate alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + rotate: 90, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'rotate')).to.equal('90deg'); + }); + + test('Render individual rotateX alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + rotateX: 45, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'rotateX')).to.equal('45deg'); + }); + + test('Render individual rotateY alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + rotateY: 60, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'rotateY')).to.equal('60deg'); + }); + + test('Render individual rotateZ alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + rotateZ: 180, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'rotateZ')).to.equal('180deg'); + }); + + test('Render individual skewX alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + skewX: 30, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'skewX')).to.equal('30deg'); + }); + + test('Render individual skewY alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + skewY: 15, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'skewY')).to.equal('15deg'); + }); + + test('Render individual perspective alone', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + perspective: 500, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect(utils.get($target, 'perspective')).to.equal('500px'); + }); + + // --- Rendering shorthand transforms --- + + test('Render translate(x, y) shorthand', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: 100, + translateY: 200, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(100px, 200px)'); + }); + + test('Render translate3d(x, y, z) shorthand', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: 100, + translateY: 200, + translateZ: 300, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(100px, 200px, 300px)'); + }); + + test('Render translate3d shorthand with plain initial values', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translateX(10px) translateY(20px) translateZ(30px)'; + const animation = animate($target, { + translateX: 100, + translateY: 200, + translateZ: 300, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(100px, 200px, 300px)'); + }); + + test('Render scale(x, y) shorthand without standalone scale', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + scaleX: 2, + scaleY: 3, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('scale(2, 3)'); + }); + + test('Render scale3d(x, y, z) shorthand', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + scaleX: 1.5, + scaleY: 2, + scaleZ: 0.5, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('scale3d(1.5, 2, 0.5)'); + }); + + test('Render individual scaleX and scaleY when standalone scale is present', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + scale: 1.5, + scaleX: 2, + scaleY: 3, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('scale(1.5) scaleX(2) scaleY(3)'); + }); + + // --- Complex CSS values in transforms --- + + test('Render translateX with calc expression', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: 'calc(calc(15px * 2) - 42rem)', + duration: 10, + }); + expect($target.style.transform).to.equal('translateX(calc(0px + 0rem))'); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translateX(calc(30px - 42rem))'); + }); + + test('Render translateX with calc containing mixed units', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: 'calc(50px + 10vw)', + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translateX(calc(50px + 10vw))'); + }); + + test('Render translateX and translateY with different calc expressions', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: 'calc(50% - 20px)', + translateY: 'calc(50px + 10vh)', + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(calc(50% - 20px), calc(50px + 10vh))'); + }); + + // --- Complex transform combinations --- + + test('Render perspective with translate3d and rotate', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + perspective: 800, + translateX: 100, + translateY: 50, + translateZ: -200, + rotate: 45, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('perspective(800px) translate3d(100px, 50px, -200px) rotate(45deg)'); + }); + + test('Render translate with all rotation axes', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + x: 50, + y: 100, + rotate: 30, + rotateX: 45, + rotateY: 60, + rotateZ: 90, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(50px, 100px) rotate(30deg) rotateX(45deg) rotateY(60deg) rotateZ(90deg)'); + }); + + test('Render translate3d with scale3d and skew', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: 100, + translateY: 200, + translateZ: 50, + scaleX: 1.5, + scaleY: 2, + scaleZ: 1, + skewX: 10, + skewY: 5, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(100px, 200px, 50px) scale3d(1.5, 2, 1) skewX(10deg) skewY(5deg)'); + }); + + test('Render perspective with x/y shorthand, rotate and scale', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + perspective: 1000, + x: 50, + y: -30, + rotate: 15, + scale: 1.2, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('perspective(1000px) translate(50px, -30px) rotate(15deg) scale(1.2)'); + }); + + test('Render all transform types in correct order', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + perspective: 500, + translateX: 10, + translateY: 20, + translateZ: 30, + rotate: 45, + rotateX: 10, + rotateY: 20, + rotateZ: 30, + scale: 1.5, + scaleX: 2, + scaleY: 2, + scaleZ: 1, + skewX: 5, + skewY: 10, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('perspective(500px) translate3d(10px, 20px, 30px) rotate(45deg) rotateX(10deg) rotateY(20deg) rotateZ(30deg) scale(1.5) scaleX(2) scaleY(2) scaleZ(1) skewX(5deg) skewY(10deg)'); + }); + + test('Render x shorthand with skew and perspective', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + perspective: 600, + x: 80, + skewX: 15, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('perspective(600px) translateX(80px) skewX(15deg)'); + }); + + test('Render translate(x, y) with rotate3d', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'rotate3d(1, 1, 0, 45deg)'; + const animation = animate($target, { + translateX: 100, + translateY: 200, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(100px, 200px) rotate3d(1, 1, 0, 45deg)'); + }); + + test('Render rotate3d before scale in correct order', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'rotate3d(1, 0, 0, 30deg)'; + const animation = animate($target, { + scale: 2, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('rotate3d(1, 0, 0, 30deg) scale(2)'); + }); + + test('Render with inline matrix preserved', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'matrix(1, 0, 0, 1, 50, 100)'; + const animation = animate($target, { + translateX: 200, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translateX(200px) matrix(1, 0, 0, 1, 50, 100)'); + }); + + test('Render with inline matrix3d preserved', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 50, 100, 0, 1)'; + const animation = animate($target, { + rotate: 45, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('rotate(45deg) matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 50, 100, 0, 1)'); + }); + + test('Render with inline rotate3d and matrix both preserved', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'rotate3d(0, 1, 0, 60deg) matrix(1, 0, 0, 1, 0, 0)'; + const animation = animate($target, { + translateX: 100, + translateY: 50, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(100px, 50px) rotate3d(0, 1, 0, 60deg) matrix(1, 0, 0, 1, 0, 0)'); + }); + + // --- Zero value transforms --- + + test('Render all-zero transforms produce correct shorthands', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: [100, 0], + translateY: [100, 0], + translateZ: [100, 0], + rotate: [90, 0], + rotateX: [90, 0], + rotateY: [90, 0], + rotateZ: [90, 0], + scale: [2, 0], + scaleX: [2, 0], + scaleY: [2, 0], + scaleZ: [2, 0], + skewX: [45, 0], + skewY: [45, 0], + ease: 'linear', + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(0px, 0px, 0px) rotate(0deg) rotateX(0deg) rotateY(0deg) rotateZ(0deg) scale(0) scaleX(0) scaleY(0) scaleZ(0) skewX(0deg) skewY(0deg)'); + }); + + test('Render translate3d(0, 0, 0) shorthand when all axes are zero', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: [50, 0], + translateY: [50, 0], + translateZ: [50, 0], + ease: 'linear', + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(0px, 0px, 0px)'); + }); + + test('Render translate(0, 0) shorthand when X and Y are zero', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + translateX: [50, 0], + translateY: [50, 0], + ease: 'linear', + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(0px, 0px)'); + }); + + test('Render scale3d(0, 0, 0) shorthand when all axes are zero', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + scaleX: [2, 0], + scaleY: [2, 0], + scaleZ: [2, 0], + ease: 'linear', + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('scale3d(0, 0, 0)'); + }); + + test('Render scale(0, 0) shorthand when X and Y are zero', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + scaleX: [2, 0], + scaleY: [2, 0], + ease: 'linear', + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('scale(0, 0)'); + }); + + test('Render x/y shorthands from non-zero to zero', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const animation = animate($target, { + x: [100, 0], + y: [200, 0], + ease: 'linear', + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(0px, 0px)'); + }); + + // --- Roundtrip (parse and re-render) --- + + test('Parse and re-render translate3d shorthand roundtrip', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate3d(10px, 20px, 30px)'; + const animation = animate($target, { + translateX: 100, + translateY: 200, + translateZ: 300, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate3d(100px, 200px, 300px)'); + }); + + test('Parse and re-render scale3d shorthand roundtrip', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'scale3d(1, 1, 1)'; + const animation = animate($target, { + scaleX: 2, + scaleY: 3, + scaleZ: 4, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('scale3d(2, 3, 4)'); + }); + + test('Parse and re-render translate(x, y) shorthand roundtrip', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'translate(50px, 75px)'; + const animation = animate($target, { + translateX: 200, + translateY: 300, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('translate(200px, 300px)'); + }); + + test('Parse and re-render scale(x, y) shorthand roundtrip', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'scale(2, 3)'; + const animation = animate($target, { + scaleX: 4, + scaleY: 5, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('scale(4, 5)'); + }); + + test('Parse complex inline and animate to new values', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + $target.style.transform = 'perspective(500px) translate(30px, 60px) rotate(10deg) scale3d(1.2, 1.5, 1)'; + const animation = animate($target, { + perspective: 800, + translateX: 100, + translateY: 200, + rotate: 45, + scaleX: 2, + scaleY: 3, + scaleZ: 1.5, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.style.transform).to.equal('perspective(800px) translate(100px, 200px) rotate(45deg) scale3d(2, 3, 1.5)'); + }); + + // --- Aggregation (multiple animations on same target) --- + + test('Multiple animations on same target aggregate into translate(x, y)', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const anim1 = animate($target, { + x: 100, + duration: 10, + }); + const anim2 = animate($target, { + y: 200, + duration: 10, + }); + anim1.pause().seek(anim1.duration); + anim2.pause().seek(anim2.duration); + expect($target.style.transform).to.equal('translate(100px, 200px)'); + }); + + test('Multiple animations on same target aggregate into translate3d(x, y, z)', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const anim1 = animate($target, { + x: 100, + duration: 10, + }); + const anim2 = animate($target, { + y: 200, + duration: 10, + }); + const anim3 = animate($target, { + z: 300, + duration: 10, + }); + anim1.pause().seek(anim1.duration); + anim2.pause().seek(anim2.duration); + anim3.pause().seek(anim3.duration); + expect($target.style.transform).to.equal('translate3d(100px, 200px, 300px)'); + }); + + test('Multiple animations on same target aggregate into scale(x, y)', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const anim1 = animate($target, { + scaleX: 2, + duration: 10, + }); + const anim2 = animate($target, { + scaleY: 3, + duration: 10, + }); + anim1.pause().seek(anim1.duration); + anim2.pause().seek(anim2.duration); + expect($target.style.transform).to.equal('scale(2, 3)'); + }); + + test('Multiple animations on same target aggregate into scale3d(x, y, z)', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const anim1 = animate($target, { + scaleX: 1.5, + duration: 10, + }); + const anim2 = animate($target, { + scaleY: 2, + duration: 10, + }); + const anim3 = animate($target, { + scaleZ: 0.5, + duration: 10, + }); + anim1.pause().seek(anim1.duration); + anim2.pause().seek(anim2.duration); + anim3.pause().seek(anim3.duration); + expect($target.style.transform).to.equal('scale3d(1.5, 2, 0.5)'); + }); + + test('Multiple animations mixing x/y shorthand and other transforms', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const anim1 = animate($target, { + x: 50, + rotate: 45, + duration: 10, + }); + const anim2 = animate($target, { + y: 100, + scale: 2, + duration: 10, + }); + anim1.pause().seek(anim1.duration); + anim2.pause().seek(anim2.duration); + expect($target.style.transform).to.equal('translate(50px, 100px) rotate(45deg) scale(2)'); + }); + + test('Multiple animations aggregate translate, rotate and skew', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const anim1 = animate($target, { + x: 100, + y: 50, + rotate: 30, + duration: 10, + }); + const anim2 = animate($target, { + skewX: 15, + skewY: 10, + duration: 10, + }); + anim1.pause().seek(anim1.duration); + anim2.pause().seek(anim2.duration); + expect($target.style.transform).to.equal('translate(100px, 50px) rotate(30deg) skewX(15deg) skewY(10deg)'); + }); + + test('Multiple animations aggregate perspective, translate3d and scale3d', () => { + /** @type {HTMLElement} */ + const $target = document.querySelector('#target-id'); + const anim1 = animate($target, { + perspective: 800, + x: 50, + duration: 10, + }); + const anim2 = animate($target, { + y: 100, + z: -50, + duration: 10, + }); + const anim3 = animate($target, { + scaleX: 1.5, + scaleY: 2, + scaleZ: 1, + duration: 10, + }); + anim1.pause().seek(anim1.duration); + anim2.pause().seek(anim2.duration); + anim3.pause().seek(anim3.duration); + expect($target.style.transform).to.equal('perspective(800px) translate3d(50px, 100px, -50px) scale3d(1.5, 2, 1)'); + }); + + // --- Revert and clean inline styles --- + + test('Revert cleans translate(x, y) shorthand transforms', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + const animation = animate($target, { + translateX: 200, + translateY: 200, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.getAttribute('style')).to.equal('transform: translate(200px, 200px);'); + animation.revert(); + expect($target.getAttribute('style')).to.equal(null); + }); + + test('Revert cleans translate3d(x, y, z) shorthand transforms', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + const animation = animate($target, { + translateX: 100, + translateY: 200, + translateZ: 300, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.getAttribute('style')).to.equal('transform: translate3d(100px, 200px, 300px);'); + animation.revert(); + expect($target.getAttribute('style')).to.equal(null); + }); + + test('Revert cleans scale(x, y) shorthand transforms', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + const animation = animate($target, { + scaleX: 2, + scaleY: 3, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.getAttribute('style')).to.equal('transform: scale(2, 3);'); + animation.revert(); + expect($target.getAttribute('style')).to.equal(null); + }); + + test('Revert cleans scale3d(x, y, z) shorthand transforms', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + const animation = animate($target, { + scaleX: 1.5, + scaleY: 2, + scaleZ: 0.5, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.getAttribute('style')).to.equal('transform: scale3d(1.5, 2, 0.5);'); + animation.revert(); + expect($target.getAttribute('style')).to.equal(null); + }); + + test('Revert cleans x/y shorthand transforms', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + const animation = animate($target, { + x: 150, + y: 250, + duration: 10, + }); + animation.pause().seek(animation.duration); + expect($target.getAttribute('style')).to.equal('transform: translate(150px, 250px);'); + animation.revert(); + expect($target.getAttribute('style')).to.equal(null); + }); + + test('Revert preserves existing inline translate shorthand', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + $target.style.transform = 'translate(50px, 60px)'; + const animation = animate($target, { + translateX: 200, + translateY: 200, + duration: 10, + }); + animation.pause().seek(animation.duration); + animation.revert(); + expect($target.getAttribute('style')).to.equal('transform: translate(50px, 60px);'); + }); + + test('Revert preserves existing inline translate3d shorthand', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + $target.style.transform = 'translate3d(10px, 20px, 30px)'; + const animation = animate($target, { + translateX: 200, + translateY: 200, + translateZ: 200, + duration: 10, + }); + animation.pause().seek(animation.duration); + animation.revert(); + expect($target.getAttribute('style')).to.equal('transform: translate3d(10px, 20px, 30px);'); + }); + + test('Revert partially cleans transforms keeping non-animated ones', () => { + const $target = /** @type {HTMLElement} */(document.querySelector('#target-id')); + $target.style.transform = 'translate(10px, 20px) rotate(45deg)'; + const animation = animate($target, { + translateX: 200, + translateY: 200, + duration: 10, + }); + animation.pause().seek(animation.duration); + animation.revert(); + expect($target.getAttribute('style')).to.equal('transform: translate(10px, 20px) rotate(45deg);'); + }); + +}); diff --git a/tests/suites/types.test.js b/tests/suites/types.test.js index efb86d0b4..f4d32c2a3 100644 --- a/tests/suites/types.test.js +++ b/tests/suites/types.test.js @@ -7,7 +7,7 @@ animate('.anime-css', { b: 'string', c0: (el) => el.dataset.index, c1: (el, i) => el.dataset.index + i, - c2: (el, i, t) => t - (el.dataset.index + i), + c2: (el, i, targets) => targets.length - (el.dataset.index + i), }, '20%' : { x: '0rem', y: '-2.5rem', rotate: 45, ease: 'out' }, '40%' : { x: '17rem', y: '-2.5rem' }, @@ -35,13 +35,13 @@ const animation = animate('#target-id', { b: 'string', c0: (el) => el.dataset.index, // el should be of type target c1: (el, i) => el.dataset.index + i, - c2: (el, i, t) => { t - (el.dataset.index + i) }, // Should throw because not returing a valid value + c2: (el, i, targets) => { targets.length - (el.dataset.index + i) }, // Should throw because not returing a valid value d: { to: 100, duration: 10, }, e: { - from: (el, i, t) => t - (el.dataset.index + i), + from: (el, i, targets) => targets.length - (el.dataset.index + i), delay: 10, }, f: [0, 100], @@ -50,8 +50,8 @@ const animation = animate('#target-id', { { to: [0, 1], duration: 200, ease: 'outBack' }, { to: 1, duration: 100, delay: 500, ease: 'inQuart' }, ], - delay: (_, i, t) => i * t, - duration: (_, i, t) => i * t, + delay: (_, i, targets) => i * targets.length, + duration: (_, i, targets) => i * targets.length, modifier: v => v * 100, loopDelay: 100, loop: true, @@ -93,19 +93,19 @@ const tl = createTimeline({ .add('#target-id', { a: 100, b: 'string', - c: (el, i, t) => { t - (el.dataset.index + i) }, // Should throw + c: (el, i, targets) => { targets.length - (el.dataset.index + i) }, // Should throw d: { to: 100, duration: 10, }, e: { - from: (el, i, t) => t - (el.dataset.index + i), + from: (el, i, targets) => targets.length - (el.dataset.index + i), delay: 10, }, f: [0, 100], g: (el) => el.dataset.index, // el should be of type target - delay: (_, i, t) => i * t, - duration: (_, i, t) => i * t, + delay: (_, i, targets) => i * targets.length, + duration: (_, i, targets) => i * targets.length, modifier: v => v * 100, loopDelay: 100, loop: true, diff --git a/tests/suites/values.test.js b/tests/suites/values.test.js index 76a0d3e40..e6594a420 100644 --- a/tests/suites/values.test.js +++ b/tests/suites/values.test.js @@ -377,7 +377,7 @@ suite('Values', () => { /** @type {HTMLElement} */ const $target = document.querySelector('#target-id'); - expect($target.style.transform).to.equal('translateX(100px) translateY(100px) translateZ(100px) rotate(360deg) rotateX(360deg) rotateY(360deg) rotateZ(360deg) skew(45deg) skewX(45deg) skewY(45deg) scale(10) scaleX(10) scaleY(10) scaleZ(10) perspective(1000px)'); + expect($target.style.transform).to.equal('perspective(1000px) translate3d(100px, 100px, 100px) rotate(360deg) rotateX(360deg) rotateY(360deg) rotateZ(360deg) scale(10) scaleX(10) scaleY(10) scaleZ(10) skew(45deg) skewX(45deg) skewY(45deg)'); }); test('Get inline transforms values', () => { @@ -399,35 +399,6 @@ suite('Values', () => { }); - test('Transforms shorthand properties values', () => { - - /** @type {HTMLElement} */ - const $target = document.querySelector('#target-id'); - - $target.style.transform = 'translateX(10px) translateY(calc(-100px + 10vh)) translateZ(50px) scale(0.75)'; - - const animation = animate('#target-id', { - x: 100, - y: 100, - z: 100, - scale: 10, - duration: 10, - }); - - expect(utils.get('#target-id', 'x')).to.equal('10px'); - expect(utils.get('#target-id', 'y')).to.equal('calc(-100px + 10vh)'); - expect(utils.get('#target-id', 'z')).to.equal('50px'); - expect(utils.get('#target-id', 'scale')).to.equal('0.75'); - - animation.pause().seek(animation.duration); - - expect(utils.get('#target-id', 'x')).to.equal('100px'); - expect(utils.get('#target-id', 'y')).to.equal('calc(100px + 100vh)'); - expect(utils.get('#target-id', 'z')).to.equal('100px'); - expect(utils.get('#target-id', 'scale')).to.equal('10'); - - }); - test('Values with white space', () => { /** @type {HTMLElement} */ const $target = document.querySelector('#target-id'); diff --git a/tests/suites/waapi.test.js b/tests/suites/waapi.test.js index e0504f39b..7a45f5b6e 100644 --- a/tests/suites/waapi.test.js +++ b/tests/suites/waapi.test.js @@ -7,6 +7,7 @@ import { utils, stagger, eases, + createTimeline, } from '../../dist/modules/index.js'; suite('WAAPI', () => { @@ -46,7 +47,7 @@ suite('WAAPI', () => { test('Animate multiple elements', resolve => { const targets = utils.$('.target-class'); - const animation = waapi.animate(targets, { + waapi.animate(targets, { transform: `translateX(100px)`, duration: 10, onComplete: anim => { @@ -62,7 +63,7 @@ suite('WAAPI', () => { test('Animate multiple elements with stagger', resolve => { const targets = utils.$('.target-class'); - const animation = waapi.animate(targets, { + waapi.animate(targets, { transform: `translateX(100px)`, duration: 10, delay: stagger(1), @@ -79,7 +80,7 @@ suite('WAAPI', () => { test('Animate with function based values', resolve => { const targets = utils.$('.target-class'); - const animation = waapi.animate(targets, { + waapi.animate(targets, { transform: (_, i) => `translateX(${i * 100}px)`, duration: 10, delay: stagger(1), @@ -93,9 +94,25 @@ suite('WAAPI', () => { }); }); - test('Animate with function based keyframes value', resolve => { + test('Animate with function based properties', () => { const targets = utils.$('.target-class'); const animation = waapi.animate(targets, { + transform: `translateX(100px)`, + duration: (_, i) => i * 20, + delay: (_, i) => i * 5, + ease: () => (t) => t, + autoplay: false + }); + animation.seek(20).commitStyles(); + expect(utils.get(targets[0], 'x')).to.equal('100px'); + expect(utils.get(targets[1], 'x')).to.equal('75px'); + expect(utils.get(targets[2], 'x')).to.equal('25px'); + expect(utils.get(targets[3], 'x')).to.equal('8.33333px'); + }); + + test('Animate with function based keyframes value', resolve => { + const targets = utils.$('.target-class'); + waapi.animate(targets, { transform: ['translateX(200px)', (_, i) => `translateX(${i * 100}px)`], duration: 10, delay: stagger(1), @@ -318,4 +335,53 @@ suite('WAAPI', () => { expect(utils.get($target1, 'opacity')).to.not.equal(utils.get($target2, 'opacity')); }); + test('Looped timeline syncing a WAAPI animation updates values correctly after initial loop', () => { + const [ $target1, $target2 ] = utils.$('.target-class'); + const waapiAnimA = waapi.animate($target1, { + opacity: [0, 1], + duration: 100, + autoplay: false, + ease: 'linear', + }); + const waapiAnimB = waapi.animate($target2, { + opacity: [0, 1], + duration: 100, + autoplay: false, + ease: 'linear', + }); + const tl = createTimeline({ + autoplay: false, + loop: 1, + }) + .sync(waapiAnimA) + .sync(waapiAnimB, 50); // This caused syncing of the first waapi anim to fail before forcing persist + tl.seek(0); + expect(utils.get($target1, 'opacity')).to.equal('0'); + expect(utils.get($target2, 'opacity')).to.equal('0'); + tl.seek(50); + expect(utils.get($target1, 'opacity')).to.equal('0.5'); + expect(utils.get($target2, 'opacity')).to.equal('0'); + tl.seek(100); + expect(utils.get($target1, 'opacity')).to.equal('1'); + expect(utils.get($target2, 'opacity')).to.equal('0.5'); + tl.seek(149); + expect(utils.get($target1, 'opacity')).to.equal('1'); + expect(utils.get($target2, 'opacity')).to.equal('0.99'); + tl.seek(150); + expect(utils.get($target1, 'opacity')).to.equal('0'); // value goes back to 0 when currrent time is the begining of the second loop + expect(utils.get($target2, 'opacity')).to.equal('0'); + tl.seek(200); + expect(utils.get($target1, 'opacity')).to.equal('0.5'); + expect(utils.get($target2, 'opacity')).to.equal('0'); + tl.seek(250); + expect(utils.get($target1, 'opacity')).to.equal('1'); + expect(utils.get($target2, 'opacity')).to.equal('0.5'); + tl.seek(299); + expect(utils.get($target1, 'opacity')).to.equal('1'); + expect(utils.get($target2, 'opacity')).to.equal('0.99'); + tl.seek(300); + expect(utils.get($target1, 'opacity')).to.equal('1'); + expect(utils.get($target2, 'opacity')).to.equal('1'); + }); + });