Files
godot-website/assets/js/modules/anime@4.1.2_esm.js
Adam Scott 5387797774 Godot 4.5 release page
Huge thanks to JohnVeness, AThousandShips, and Meorge for their
great help rewording my broken English.
2025-09-15 16:03:40 -04:00

8271 lines
279 KiB
JavaScript

/**
* anime.js - ESM
* @version v4.1.2
* @author Julian Garnier
* @license MIT
* @copyright (c) 2025 Julian Garnier
* @see https://animejs.com
*/
// Global types ///////////////////////////////////////////////////////////////
/**
* @typedef {Object} DefaultsParams
* @property {Number|String} [id]
* @property {PercentageKeyframes|DurationKeyframes} [keyframes]
* @property {EasingParam} [playbackEase]
* @property {Number} [playbackRate]
* @property {Number} [frameRate]
* @property {Number|Boolean} [loop]
* @property {Boolean} [reversed]
* @property {Boolean} [alternate]
* @property {Boolean|ScrollObserver} [autoplay]
* @property {Number|FunctionValue} [duration]
* @property {Number|FunctionValue} [delay]
* @property {Number} [loopDelay]
* @property {EasingParam} [ease]
* @property {'none'|'replace'|'blend'|compositionTypes} [composition]
* @property {(v: any) => any} [modifier]
* @property {(tickable: Tickable) => void} [onBegin]
* @property {(tickable: Tickable) => void} [onBeforeUpdate]
* @property {(tickable: Tickable) => void} [onUpdate]
* @property {(tickable: Tickable) => void} [onLoop]
* @property {(tickable: Tickable) => void} [onPause]
* @property {(tickable: Tickable) => void} [onComplete]
* @property {(renderable: Renderable) => void} [onRender]
*/
/** @typedef {JSAnimation|Timeline} Renderable */
/** @typedef {Timer|Renderable} Tickable */
/** @typedef {Timer&JSAnimation&Timeline} CallbackArgument */
/** @typedef {Animatable|Tickable|Draggable|ScrollObserver|TextSplitter|Scope} Revertible */
// Stagger types //////////////////////////////////////////////////////////////
/**
* @callback StaggerFunction
* @param {Target} [target]
* @param {Number} [index]
* @param {Number} [length]
* @param {Timeline} [tl]
* @return {Number|String}
*/
/**
* @typedef {Object} StaggerParams
* @property {Number|String} [start]
* @property {Number|'first'|'center'|'last'|'random'} [from]
* @property {Boolean} [reversed]
* @property {Array.<Number>} [grid]
* @property {('x'|'y')} [axis]
* @property {String|StaggerFunction} [use]
* @property {Number} [total]
* @property {EasingParam} [ease]
* @property {TweenModifier} [modifier]
*/
// Eases types ////////////////////////////////////////////////////////////////
/**
* @callback EasingFunction
* @param {Number} time
* @return {Number}
*/
/**
* @typedef {('linear'|'linear(x1, x2 25%, x3)'|'in'|'out'|'inOut'|'inQuad'|'outQuad'|'inOutQuad'|'inCubic'|'outCubic'|'inOutCubic'|'inQuart'|'outQuart'|'inOutQuart'|'inQuint'|'outQuint'|'inOutQuint'|'inSine'|'outSine'|'inOutSine'|'inCirc'|'outCirc'|'inOutCirc'|'inExpo'|'outExpo'|'inOutExpo'|'inBounce'|'outBounce'|'inOutBounce'|'inBack'|'outBack'|'inOutBack'|'inElastic'|'outElastic'|'inOutElastic'|'irregular'|'cubicBezier'|'steps'|'in(p = 1.675)'|'out(p = 1.675)'|'inOut(p = 1.675)'|'inBack(overshoot = 1.70158)'|'outBack(overshoot = 1.70158)'|'inOutBack(overshoot = 1.70158)'|'inElastic(amplitude = 1, period = .3)'|'outElastic(amplitude = 1, period = .3)'|'inOutElastic(amplitude = 1, period = .3)'|'irregular(length = 10, randomness = 1)'|'cubicBezier(x1, y1, x2, y2)'|'steps(steps = 10)')} EaseStringParamNames
*/
// 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 {HTMLElement|SVGElement} DOMTarget */
/** @typedef {Record<String, any>} JSTarget */
/** @typedef {DOMTarget|JSTarget} Target */
/** @typedef {Target|NodeList|String} TargetSelector */
/** @typedef {DOMTarget|NodeList|String} DOMTargetSelector */
/** @typedef {Array.<DOMTargetSelector>|DOMTargetSelector} DOMTargetsParam */
/** @typedef {Array.<DOMTarget>} DOMTargetsArray */
/** @typedef {Array.<JSTarget>|JSTarget} JSTargetsParam */
/** @typedef {Array.<JSTarget>} JSTargetsArray */
/** @typedef {Array.<TargetSelector>|TargetSelector} TargetsParam */
/** @typedef {Array.<Target>} TargetsArray */
// Callback types ////////////////////////////////////////////////////////////
/**
* @template T
* @callback Callback
* @param {T} self - Returns itself
* @param {PointerEvent} [e]
* @return {*}
*/
/**
* @template {object} T
* @typedef {Object} TickableCallbacks
* @property {Callback<T>} [onBegin]
* @property {Callback<T>} [onBeforeUpdate]
* @property {Callback<T>} [onUpdate]
* @property {Callback<T>} [onLoop]
* @property {Callback<T>} [onPause]
* @property {Callback<T>} [onComplete]
*/
/**
* @template {object} T
* @typedef {Object} RenderableCallbacks
* @property {Callback<T>} [onRender]
*/
// Timer types ////////////////////////////////////////////////////////////////
/**
* @typedef {Object} TimerOptions
* @property {Number|String} [id]
* @property {TweenParamValue} [duration]
* @property {TweenParamValue} [delay]
* @property {Number} [loopDelay]
* @property {Boolean} [reversed]
* @property {Boolean} [alternate]
* @property {Boolean|Number} [loop]
* @property {Boolean|ScrollObserver} [autoplay]
* @property {Number} [frameRate]
* @property {Number} [playbackRate]
*/
/**
/**
* @typedef {TimerOptions & TickableCallbacks<Timer>} TimerParams
*/
// Tween types ////////////////////////////////////////////////////////////////
/**
* @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.<Number|String|TweenObjectValue>}
*/
/**
* @callback TweenModifier
* @param {Number} value - The animated value
* @return {Number|String}
*/
/** @typedef {[Number, Number, Number, Number]} ColorArray */
/**
* @typedef {Object} Tween
* @property {Number} id
* @property {JSAnimation} parent
* @property {String} property
* @property {Target} target
* @property {String|Number} _value
* @property {Function|null} _func
* @property {EasingFunction} _ease
* @property {Array.<Number>} _fromNumbers
* @property {Array.<Number>} _toNumbers
* @property {Array.<String>} _strings
* @property {Number} _fromNumber
* @property {Number} _toNumber
* @property {Array.<Number>} _numbers
* @property {Number} _number
* @property {String} _unit
* @property {TweenModifier} _modifier
* @property {Number} _currentTime
* @property {Number} _delay
* @property {Number} _updateDuration
* @property {Number} _startTime
* @property {Number} _changeDuration
* @property {Number} _absoluteStartTime
* @property {tweenTypes} _tweenType
* @property {valueTypes} _valueType
* @property {Number} _composition
* @property {Number} _isOverlapped
* @property {Number} _isOverridden
* @property {Number} _renderTransforms
* @property {Tween} _prevRep
* @property {Tween} _nextRep
* @property {Tween} _prevAdd
* @property {Tween} _nextAdd
* @property {Tween} _prev
* @property {Tween} _next
*/
/**
* @typedef TweenDecomposedValue
* @property {Number} t - Type
* @property {Number} n - Single number value
* @property {String} u - Value unit
* @property {String} o - Value operator
* @property {Array.<Number>} d - Array of Numbers (in case of complex value type)
* @property {Array.<String>} s - Strings (in case of complex value type)
*/
/** @typedef {{_head: null|Tween, _tail: null|Tween}} TweenPropertySiblings */
/** @typedef {Record<String, TweenPropertySiblings>} TweenLookups */
/** @typedef {WeakMap.<Target, TweenLookups>} TweenReplaceLookups */
/** @typedef {Map.<Target, TweenLookups>} TweenAdditiveLookups */
// Animation types ////////////////////////////////////////////////////////////
/**
* @typedef {Number|String|FunctionValue} TweenParamValue
*/
/**
* @typedef {TweenParamValue|[TweenParamValue, TweenParamValue]} TweenPropValue
*/
/**
* @typedef {(String & {})|'none'|'replace'|'blend'|compositionTypes} TweenComposition
*/
/**
* @typedef {Object} TweenParamsOptions
* @property {TweenParamValue} [duration]
* @property {TweenParamValue} [delay]
* @property {EasingParam} [ease]
* @property {TweenModifier} [modifier]
* @property {TweenComposition} [composition]
*/
/**
* @typedef {Object} TweenValues
* @property {TweenParamValue} [from]
* @property {TweenPropValue} [to]
* @property {TweenPropValue} [fromTo]
*/
/**
* @typedef {TweenParamsOptions & TweenValues} TweenKeyValue
*/
/**
* @typedef {Array.<TweenKeyValue|TweenPropValue>} ArraySyntaxValue
*/
/**
* @typedef {TweenParamValue|ArraySyntaxValue|TweenKeyValue} TweenOptions
*/
/**
* @typedef {Partial<{to: TweenParamValue|Array.<TweenParamValue>; from: TweenParamValue|Array.<TweenParamValue>; fromTo: TweenParamValue|Array.<TweenParamValue>;}>} TweenObjectValue
*/
/**
* @typedef {Object} PercentageKeyframeOptions
* @property {EasingParam} [ease]
*/
/**
* @typedef {Record<String, TweenParamValue>} PercentageKeyframeParams
*/
/**
* @typedef {Record<String, PercentageKeyframeParams & PercentageKeyframeOptions>} PercentageKeyframes
*/
/**
* @typedef {Array<Record<String, TweenOptions | TweenModifier | boolean> & TweenParamsOptions>} DurationKeyframes
*/
/**
* @typedef {Object} AnimationOptions
* @property {PercentageKeyframes|DurationKeyframes} [keyframes]
* @property {EasingParam} [playbackEase]
*/
// TODO: Currently setting TweenModifier to the intersected Record<> makes the FunctionValue type target param any if only one parameter is set
/**
* @typedef {Record<String, TweenOptions | Callback<JSAnimation> | TweenModifier | boolean | PercentageKeyframes | DurationKeyframes | ScrollObserver> & TimerOptions & AnimationOptions & TweenParamsOptions & TickableCallbacks<JSAnimation> & RenderableCallbacks<JSAnimation>} AnimationParams
*/
// Timeline types /////////////////////////////////////////////////////////////
/**
* @typedef {Object} TimelineOptions
* @property {DefaultsParams} [defaults]
* @property {EasingParam} [playbackEase]
*/
/**
* @typedef {TimerOptions & TimelineOptions & TickableCallbacks<Timeline> & RenderableCallbacks<Timeline>} TimelineParams
*/
// Animatable types ///////////////////////////////////////////////////////////
/**
* @callback AnimatablePropertySetter
* @param {Number|Array.<Number>} to
* @param {Number} [duration]
* @param {EasingParam} [ease]
* @return {AnimatableObject}
*/
/**
* @callback AnimatablePropertyGetter
* @return {Number|Array.<Number>}
*/
/**
* @typedef {AnimatablePropertySetter & AnimatablePropertyGetter} AnimatableProperty
*/
/**
* @typedef {Animatable & Record<String, AnimatableProperty>} AnimatableObject
*/
/**
* @typedef {Object} AnimatablePropertyParamsOptions
* @property {String} [unit]
* @property {TweenParamValue} [duration]
* @property {EasingParam} [ease]
* @property {TweenModifier} [modifier]
* @property {TweenComposition} [composition]
*/
/**
* @typedef {Record<String, TweenParamValue | EasingParam | TweenModifier | TweenComposition | AnimatablePropertyParamsOptions> & AnimatablePropertyParamsOptions} AnimatableParams
*/
// Scope types ////////////////////////////////////////////////////////////////
/**
* @typedef {Object} ReactRef
* @property {HTMLElement|SVGElement|null} [current]
*/
/**
* @typedef {Object} AngularRef
* @property {HTMLElement|SVGElement} [nativeElement]
*/
/**
* @typedef {Object} ScopeParams
* @property {DOMTargetSelector|ReactRef|AngularRef} [root]
* @property {DefaultsParams} [defaults]
* @property {Record<String, String>} [mediaQueries]
*/
/**
* @template T
* @callback ScopedCallback
* @param {Scope} scope
* @return {T}
*/
/**
* @callback ScopeCleanupCallback
* @param {Scope} [scope]
*/
/**
* @callback ScopeConstructorCallback
* @param {Scope} [scope]
* @return {ScopeCleanupCallback|void}
*/
/**
* @callback ScopeMethod
* @param {...*} args
* @return {ScopeCleanupCallback|void}
*/
// Draggable types ////////////////////////////////////////////////////////////
/**
* @typedef {Object} DraggableAxisParam
* @property {String} [mapTo]
* @property {TweenModifier} [modifier]
* @property {TweenComposition} [composition]
* @property {Number|Array<Number>|((draggable: Draggable) => Number|Array<Number>)} [snap]
*/
/**
* @typedef {Object} DraggableCursorParams
* @property {String} [onHover]
* @property {String} [onGrab]
*/
/**
* @typedef {Object} DraggableParams
* @property {DOMTargetSelector} [trigger]
* @property {DOMTargetSelector|Array<Number>|((draggable: Draggable) => DOMTargetSelector|Array<Number>)} [container]
* @property {Boolean|DraggableAxisParam} [x]
* @property {Boolean|DraggableAxisParam} [y]
* @property {TweenModifier} [modifier]
* @property {Number|Array<Number>|((draggable: Draggable) => Number|Array<Number>)} [snap]
* @property {Number|Array<Number>|((draggable: Draggable) => Number|Array<Number>)} [containerPadding]
* @property {Number|((draggable: Draggable) => Number)} [containerFriction]
* @property {Number|((draggable: Draggable) => Number)} [releaseContainerFriction]
* @property {Number|((draggable: Draggable) => Number)} [dragSpeed]
* @property {Number|((draggable: Draggable) => Number)} [scrollSpeed]
* @property {Number|((draggable: Draggable) => Number)} [scrollThreshold]
* @property {Number|((draggable: Draggable) => Number)} [minVelocity]
* @property {Number|((draggable: Draggable) => Number)} [maxVelocity]
* @property {Number|((draggable: Draggable) => Number)} [velocityMultiplier]
* @property {Number} [releaseMass]
* @property {Number} [releaseStiffness]
* @property {Number} [releaseDamping]
* @property {Boolean} [releaseDamping]
* @property {EasingParam} [releaseEase]
* @property {Boolean|DraggableCursorParams|((draggable: Draggable) => Boolean|DraggableCursorParams)} [cursor]
* @property {Callback<Draggable>} [onGrab]
* @property {Callback<Draggable>} [onDrag]
* @property {Callback<Draggable>} [onRelease]
* @property {Callback<Draggable>} [onUpdate]
* @property {Callback<Draggable>} [onSettle]
* @property {Callback<Draggable>} [onSnap]
* @property {Callback<Draggable>} [onResize]
* @property {Callback<Draggable>} [onAfterResize]
*/
// Text types /////////////////////////////////////////////////////////////////
/**
* @typedef {Object} splitTemplateParams
* @property {false|String} [class]
* @property {Boolean|'hidden'|'clip'|'visible'|'scroll'|'auto'} [wrap]
* @property {Boolean|'top'|'right'|'bottom'|'left'|'center'} [clone]
*/
/**
* @typedef {Boolean|String} SplitValue
*/
/**
* @callback SplitFunctionValue
* @param {Node|HTMLElement} [value]
* @return String
*/
/**
* @typedef {Object} TextSplitterParams
* @property {SplitValue|splitTemplateParams|SplitFunctionValue} [lines]
* @property {SplitValue|splitTemplateParams|SplitFunctionValue} [words]
* @property {SplitValue|splitTemplateParams|SplitFunctionValue} [chars]
* @property {Boolean} [accessible]
* @property {Boolean} [includeSpaces]
* @property {Boolean} [debug]
*/
// SVG types //////////////////////////////////////////////////////////////////
/**
* @typedef {SVGGeometryElement & {
* setAttribute(name: 'draw', value: `${number} ${number}`): void;
* draw: `${number} ${number}`;
* }} DrawableSVGGeometry
*/
// Environments
// 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;
/** @type {Document|null} */
const doc = isBrowser ? document : null;
// Enums
/** @enum {Number} */
const tweenTypes = {
OBJECT: 0,
ATTRIBUTE: 1,
CSS: 2,
TRANSFORM: 3,
CSS_VAR: 4,
};
/** @enum {Number} */
const valueTypes = {
NUMBER: 0,
UNIT: 1,
COLOR: 2,
COMPLEX: 3,
};
/** @enum {Number} */
const tickModes = {
NONE: 0,
AUTO: 1,
FORCE: 2,
};
/** @enum {Number} */
const compositionTypes = {
replace: 0,
none: 1,
blend: 2,
};
// Cache symbols
const isRegisteredTargetSymbol = Symbol();
const isDomSymbol = Symbol();
const isSvgSymbol = Symbol();
const transformsSymbol = Symbol();
const morphPointsSymbol = Symbol();
const proxyTargetSymbol = Symbol();
// Numbers
const minValue = 1e-11;
const maxValue = 1e12;
const K = 1e3;
const maxFps = 120;
// Strings
const emptyString = '';
const shortTransforms = /*#__PURE__*/ (() => {
const map = new Map();
map.set('x', 'translateX');
map.set('y', 'translateY');
map.set('z', 'translateZ');
return map;
})();
const validTransforms = [
'translateX',
'translateY',
'translateZ',
'rotate',
'rotateX',
'rotateY',
'rotateZ',
'scale',
'scaleX',
'scaleY',
'scaleZ',
'skew',
'skewX',
'skewY',
'perspective',
'matrix',
'matrix3d',
];
const transformsFragmentStrings = /*#__PURE__*/ validTransforms.reduce((a, v) => ({...a, [v]: v + '('}), {});
// Functions
/** @return {void} */
const noop = () => {};
// Regex
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;
const hslExecRgx = /hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i;
const hslaExecRgx = /hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i;
// export const digitWithExponentRgx = /[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/g;
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 = /(\*=|\+=|-=)/;
/** @type {DefaultsParams} */
const defaults = {
id: null,
keyframes: null,
playbackEase: null,
playbackRate: 1,
frameRate: maxFps,
loop: 0,
reversed: false,
alternate: false,
autoplay: true,
duration: K,
delay: 0,
loopDelay: 0,
ease: 'out(2)',
composition: compositionTypes.replace,
modifier: v => v,
onBegin: noop,
onBeforeUpdate: noop,
onUpdate: noop,
onLoop: noop,
onPause: noop,
onComplete: noop,
onRender: noop,
};
const scope = {
/** @type {Scope} */
current: null,
/** @type {Document|DOMTarget} */
root: doc,
};
const globals = {
/** @type {DefaultsParams} */
defaults,
/** @type {Number} */
precision: 4,
/** @type {Number} */
timeScale: 1,
/** @type {Number} */
tickThreshold: 200,
};
const globalVersions = { version: '4.1.2', engine: null };
if (isBrowser) {
if (!win.AnimeJS) win.AnimeJS = [];
win.AnimeJS.push(globalVersions);
}
// Strings
/**
* @param {String} str
* @return {String}
*/
const toLowerCase = str => str.replace(lowerCaseRgx, '$1-$2').toLowerCase();
/**
* Prioritize this method instead of regex when possible
* @param {String} str
* @param {String} sub
* @return {Boolean}
*/
const stringStartsWith = (str, sub) => str.indexOf(sub) === 0;
// Time
// Note: Date.now is used instead of performance.now since it is precise enough for timings calculations, performs slightly faster and works in Node.js environement.
const now = Date.now;
// Types checkers
const isArr = Array.isArray;
/**@param {any} a @return {a is Record<String, any>} */
const isObj = a => a && a.constructor === Object;
/**@param {any} a @return {a is Number} */
const isNum = a => typeof a === 'number' && !isNaN(a);
/**@param {any} a @return {a is String} */
const isStr = a => typeof a === 'string';
/**@param {any} a @return {a is Function} */
const isFnc = a => typeof a === 'function';
/**@param {any} a @return {a is undefined} */
const isUnd = a => typeof a === 'undefined';
/**@param {any} a @return {a is null | undefined} */
const isNil = a => isUnd(a) || a === null;
/**@param {any} a @return {a is SVGElement} */
const isSvg = a => isBrowser && a instanceof SVGElement;
/**@param {any} a @return {Boolean} */
const isHex = a => hexTestRgx.test(a);
/**@param {any} a @return {Boolean} */
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} */
const isKey = a => !globals.defaults.hasOwnProperty(a);
// Number
/**
* @param {Number|String} str
* @return {Number}
*/
const parseNumber = str => isStr(str) ?
parseFloat(/** @type {String} */(str)) :
/** @type {Number} */(str);
// Math
const pow = Math.pow;
const sqrt = Math.sqrt;
const sin = Math.sin;
const cos = Math.cos;
const abs = Math.abs;
const exp = Math.exp;
const ceil = Math.ceil;
const floor = Math.floor;
const asin = Math.asin;
const max = Math.max;
const atan2 = Math.atan2;
const PI = Math.PI;
const _round = Math.round;
/**
* @param {Number} v
* @param {Number} min
* @param {Number} max
* @return {Number}
*/
const clamp = (v, min, max) => v < min ? min : v > max ? max : v;
const powCache = {};
/**
* @param {Number} v
* @param {Number} decimalLength
* @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;
};
/**
* @param {Number} v
* @param {Number|Array<Number>} increment
* @return {Number}
*/
const snap = (v, increment) => isArr(increment) ? increment.reduce((closest, cv) => (abs(cv - v) < abs(closest - v) ? cv : closest)) : increment ? _round(v / increment) * increment : v;
/**
* @param {Number} start
* @param {Number} end
* @param {Number} progress
* @return {Number}
*/
const interpolate = (start, end, progress) => start + (end - start) * progress;
/**
* @param {Number} min
* @param {Number} max
* @param {Number} [decimalLength]
* @return {Number}
*/
const random = (min, max, decimalLength) => { const m = 10 ** (decimalLength || 0); return floor((Math.random() * (max - min + (1 / m)) + min) * m) / m };
/**
* Adapted from https://bost.ocks.org/mike/shuffle/
* @param {Array} items
* @return {Array}
*/
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;
};
/**
* @param {Number} v
* @return {Number}
*/
const clampInfinity = v => v === Infinity ? maxValue : v === -Infinity ? -1e12 : v;
/**
* @param {Number} v
* @return {Number}
*/
const normalizeTime = v => v <= minValue ? minValue : clampInfinity(round(v, 11));
// Arrays
/**
* @template T
* @param {T[]} a
* @return {T[]}
*/
const cloneArray = a => isArr(a) ? [ ...a ] : a;
// Objects
/**
* @template T
* @template U
* @param {T} o1
* @param {U} o2
* @return {T & U}
*/
const mergeObjects = (o1, o2) => {
const merged = /** @type {T & U} */({ ...o1 });
for (let p in o2) {
const o1p = /** @type {T & U} */(o1)[p];
merged[p] = isUnd(o1p) ? /** @type {T & U} */(o2)[p] : o1p;
} return merged;
};
// Linked lists
/**
* @param {Object} parent
* @param {Function} callback
* @param {Boolean} [reverse]
* @param {String} [prevProp]
* @param {String} [nextProp]
* @return {void}
*/
const forEachChildren = (parent, callback, reverse, prevProp = '_prev', nextProp = '_next') => {
let next = parent._head;
let adjustedNextProp = nextProp;
if (reverse) {
next = parent._tail;
adjustedNextProp = prevProp;
}
while (next) {
const currentNext = next[adjustedNextProp];
callback(next);
next = currentNext;
}
};
/**
* @param {Object} parent
* @param {Object} child
* @param {String} [prevProp]
* @param {String} [nextProp]
* @return {void}
*/
const removeChild = (parent, child, prevProp = '_prev', nextProp = '_next') => {
const prev = child[prevProp];
const next = child[nextProp];
prev ? prev[nextProp] = next : parent._head = next;
next ? next[prevProp] = prev : parent._tail = prev;
child[prevProp] = null;
child[nextProp] = null;
};
/**
* @param {Object} parent
* @param {Object} child
* @param {Function} [sortMethod]
* @param {String} prevProp
* @param {String} nextProp
* @return {void}
*/
const addChild = (parent, child, sortMethod, prevProp = '_prev', nextProp = '_next') => {
let prev = parent._tail;
while (prev && sortMethod && sortMethod(prev, child)) prev = prev[prevProp];
const next = prev ? prev[nextProp] : parent._head;
prev ? prev[nextProp] = child : parent._head = child;
next ? next[prevProp] = child : parent._tail = child;
child[prevProp] = prev;
child[nextProp] = next;
};
/**
* @param {(...args: any[]) => Tickable | ((...args: any[]) => void)} constructor
* @return {(...args: any[]) => Tickable | ((...args: any[]) => void)}
*/
const createRefreshable = constructor => {
/** @type {Tickable} */
let tracked;
return (...args) => {
let currentIteration, currentIterationProgress, reversed, alternate;
if (tracked) {
currentIteration = tracked.currentIteration;
currentIterationProgress = tracked.iterationProgress;
reversed = tracked.reversed;
alternate = tracked._alternate;
tracked.revert();
}
const cleanup = constructor(...args);
if (cleanup && !isFnc(cleanup) && cleanup.revert) tracked = cleanup;
if (!isUnd(currentIterationProgress)) {
/** @type {Tickable} */(tracked).currentIteration = currentIteration;
/** @type {Tickable} */(tracked).iterationProgress = (alternate ? !(currentIteration % 2) ? reversed : !reversed : reversed) ? 1 - currentIterationProgress : currentIterationProgress;
}
return cleanup || noop;
}
};
/*
* Base class to control framerate and playback rate.
* Inherited by Engine, Timer, Animation and Timeline.
*/
class Clock {
/** @param {Number} [initTime] */
constructor(initTime = 0) {
/** @type {Number} */
this.deltaTime = 0;
/** @type {Number} */
this._currentTime = initTime;
/** @type {Number} */
this._elapsedTime = initTime;
/** @type {Number} */
this._startTime = initTime;
/** @type {Number} */
this._lastTime = initTime;
/** @type {Number} */
this._scheduledTime = 0;
/** @type {Number} */
this._frameDuration = round(K / maxFps, 0);
/** @type {Number} */
this._fps = maxFps;
/** @type {Number} */
this._speed = 1;
/** @type {Boolean} */
this._hasChildren = false;
/** @type {Tickable|Tween} */
this._head = null;
/** @type {Tickable|Tween} */
this._tail = null;
}
get fps() {
return this._fps;
}
set fps(frameRate) {
const previousFrameDuration = this._frameDuration;
const fr = +frameRate;
const fps = fr < minValue ? minValue : fr;
const frameDuration = round(K / fps, 0);
this._fps = fps;
this._frameDuration = frameDuration;
this._scheduledTime += frameDuration - previousFrameDuration;
}
get speed() {
return this._speed;
}
set speed(playbackRate) {
const pbr = +playbackRate;
this._speed = pbr < minValue ? minValue : pbr;
}
/**
* @param {Number} time
* @return {tickModes}
*/
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 means not enough time has passed to hit one frameDuration
// so skip that frame
if (elapsedTime < scheduledTime) return tickModes.NONE;
const frameDuration = this._frameDuration;
const frameDelta = elapsedTime - 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;
return tickModes.AUTO;
}
/**
* @param {Number} time
* @return {Number}
*/
computeDeltaTime(time) {
const delta = time - this._lastTime;
this.deltaTime = delta;
this._lastTime = time;
return delta;
}
}
/**
* @param {Tickable} tickable
* @param {Number} time
* @param {Number} muteCallbacks
* @param {Number} internalRender
* @param {tickModes} tickMode
* @return {Number}
*/
const render = (tickable, time, muteCallbacks, internalRender, tickMode) => {
const parent = tickable.parent;
const duration = tickable.duration;
const completed = tickable.completed;
const iterationDuration = tickable.iterationDuration;
const iterationCount = tickable.iterationCount;
const _currentIteration = tickable._currentIteration;
const _loopDelay = tickable._loopDelay;
const _reversed = tickable._reversed;
const _alternate = tickable._alternate;
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);
const tickableCurrentTime = clamp(tickableAbsoluteTime, -tickableDelay, duration);
const deltaTime = tickableAbsoluteTime - tickablePrevAbsoluteTime;
const isCurrentTimeAboveZero = tickableCurrentTime > 0;
const isCurrentTimeEqualOrAboveDuration = tickableCurrentTime >= duration;
const isSetter = duration <= minValue;
const forcedTick = tickMode === tickModes.FORCE;
let isOdd = 0;
let iterationElapsedTime = tickableAbsoluteTime;
// Render checks
// Used to also check if the children have rendered in order to trigger the onRender callback on the parent timer
let hasRendered = 0;
// Execute the "expensive" iterations calculations only when necessary
if (iterationCount > 1) {
// bitwise NOT operator seems to be generally faster than Math.floor() across browsers
const currentIteration = ~~(tickableCurrentTime / (iterationDuration + (isCurrentTimeEqualOrAboveDuration ? 0 : _loopDelay)));
tickable._currentIteration = clamp(currentIteration, 0, iterationCount);
// Prevent the iteration count to go above the max iterations when reaching the end of the animation
if (isCurrentTimeEqualOrAboveDuration) tickable._currentIteration--;
isOdd = tickable._currentIteration % 2;
iterationElapsedTime = tickableCurrentTime % (iterationDuration + _loopDelay) || 0;
}
// Checks if exactly one of _reversed and (_alternate && isOdd) is true
const isReversed = _reversed ^ (_alternate && isOdd);
const _ease = /** @type {Renderable} */(tickable)._ease;
let iterationTime = isCurrentTimeEqualOrAboveDuration ? isReversed ? 0 : duration : isReversed ? iterationDuration - iterationElapsedTime : iterationElapsedTime;
if (_ease) iterationTime = iterationDuration * _ease(iterationTime / iterationDuration) || 0;
const isRunningBackwards = (parent ? parent.backwards : tickableAbsoluteTime < tickablePrevAbsoluteTime) ? !isReversed : !!isReversed;
tickable._currentTime = tickableAbsoluteTime;
tickable._iterationTime = iterationTime;
tickable.backwards = isRunningBackwards;
if (isCurrentTimeAboveZero && !tickable.began) {
tickable.began = true;
if (!muteCallbacks && !(parent && (isRunningBackwards || !parent.began))) {
tickable.onBegin(/** @type {CallbackArgument} */(tickable));
}
} else if (tickableAbsoluteTime <= 0) {
tickable.began = false;
}
// Only triggers onLoop for tickable without children, otherwise call the the onLoop callback in the tick function
// Make sure to trigger the onLoop before rendering to allow .refresh() to pickup the current values
if (!muteCallbacks && !_hasChildren && isCurrentTimeAboveZero && tickable._currentIteration !== _currentIteration) {
tickable.onLoop(/** @type {CallbackArgument} */(tickable));
}
if (
forcedTick ||
tickMode === tickModes.AUTO && (
time >= tickableDelay && time <= tickableEndTime || // Normal render
time <= tickableDelay && tickablePrevTime > tickableDelay || // Playhead is before the animation start time so make sure the animation is at its initial state
time >= tickableEndTime && tickablePrevTime !== duration // Playhead is after the animation end time so make sure the animation is at its end state
) ||
iterationTime >= tickableEndTime && tickablePrevTime !== duration ||
iterationTime <= tickableDelay && tickablePrevTime > 0 ||
time <= tickablePrevTime && tickablePrevTime === duration && completed || // Force a render if a seek occurs on an completed animation
isCurrentTimeEqualOrAboveDuration && !completed && isSetter // This prevents 0 duration tickables to be skipped
) {
if (isCurrentTimeAboveZero) {
// Trigger onUpdate callback before rendering
tickable.computeDeltaTime(tickablePrevTime);
if (!muteCallbacks) tickable.onBeforeUpdate(/** @type {CallbackArgument} */(tickable));
}
// Start tweens rendering
if (!_hasChildren) {
// Time has jumped more than globals.tickThreshold so consider this tick manual
const forcedRender = forcedTick || (isRunningBackwards ? deltaTime * -1 : deltaTime) >= globals.tickThreshold;
const absoluteTime = tickable._offset + (parent ? parent._offset : 0) + tickableDelay + iterationTime;
// Only Animation can have tweens, Timer returns undefined
let tween = /** @type {Tween} */(/** @type {JSAnimation} */(tickable)._head);
let tweenTarget;
let tweenStyle;
let tweenTargetTransforms;
let tweenTargetTransformsProperties;
let tweenTransformsNeedUpdate = 0;
while (tween) {
const tweenComposition = tween._composition;
const tweenCurrentTime = tween._currentTime;
const tweenChangeDuration = tween._changeDuration;
const tweenAbsEndTime = tween._absoluteStartTime + tween._changeDuration;
const tweenNextRep = tween._nextRep;
const tweenPrevRep = tween._prevRep;
const tweenHasComposition = tweenComposition !== compositionTypes.none;
if ((forcedRender || (
(tweenCurrentTime !== tweenChangeDuration || absoluteTime <= tweenAbsEndTime + (tweenNextRep ? tweenNextRep._delay : 0)) &&
(tweenCurrentTime !== 0 || absoluteTime >= tween._absoluteStartTime)
)) && (!tweenHasComposition || (
!tween._isOverridden &&
(!tween._isOverlapped || absoluteTime <= tweenAbsEndTime) &&
(!tweenNextRep || (tweenNextRep._isOverridden || absoluteTime <= tweenNextRep._absoluteStartTime)) &&
(!tweenPrevRep || (tweenPrevRep._isOverridden || (absoluteTime >= (tweenPrevRep._absoluteStartTime + tweenPrevRep._changeDuration) + tween._delay)))
))
) {
const tweenNewTime = tween._currentTime = clamp(iterationTime - tween._startTime, 0, tweenChangeDuration);
const tweenProgress = tween._ease(tweenNewTime / tween._updateDuration);
const tweenModifier = tween._modifier;
const tweenValueType = tween._valueType;
const tweenType = tween._tweenType;
const tweenIsObject = tweenType === tweenTypes.OBJECT;
const tweenIsNumber = tweenValueType === valueTypes.NUMBER;
// Only round the in-between frames values if the final value is a string
const tweenPrecision = (tweenIsNumber && tweenIsObject) || tweenProgress === 0 || tweenProgress === 1 ? -1 : globals.precision;
// Recompose tween value
/** @type {String|Number} */
let value;
/** @type {Number} */
let number;
if (tweenIsNumber) {
value = number = /** @type {Number} */(tweenModifier(round(interpolate(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision )));
} else if (tweenValueType === valueTypes.UNIT) {
// Rounding the values speed up string composition
number = /** @type {Number} */(tweenModifier(round(interpolate(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(interpolate(fn[0], tn[0], tweenProgress))), 0, 255), 0);
const g = round(clamp(/** @type {Number} */(tweenModifier(interpolate(fn[1], tn[1], tweenProgress))), 0, 255), 0);
const b = round(clamp(/** @type {Number} */(tweenModifier(interpolate(fn[2], tn[2], tweenProgress))), 0, 255), 0);
const a = clamp(/** @type {Number} */(tweenModifier(round(interpolate(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;
}
} 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(interpolate(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;
}
}
}
// For additive tweens and Animatables
if (tweenHasComposition) {
tween._number = number;
}
if (!internalRender && tweenComposition !== compositionTypes.blend) {
const tweenProperty = tween.property;
tweenTarget = tween.target;
if (tweenIsObject) {
tweenTarget[tweenProperty] = value;
} else if (tweenType === tweenTypes.ATTRIBUTE) {
/** @type {DOMTarget} */(tweenTarget).setAttribute(tweenProperty, /** @type {String} */(value));
} else {
tweenStyle = /** @type {DOMTarget} */(tweenTarget).style;
if (tweenType === tweenTypes.TRANSFORM) {
if (tweenTarget !== tweenTargetTransforms) {
tweenTargetTransforms = tweenTarget;
// NOTE: Referencing the cachedTransforms in the tween property directly can be a little bit faster but appears to increase memory usage.
tweenTargetTransformsProperties = tweenTarget[transformsSymbol];
}
tweenTargetTransformsProperties[tweenProperty] = value;
tweenTransformsNeedUpdate = 1;
} else if (tweenType === tweenTypes.CSS) {
tweenStyle[tweenProperty] = value;
} else if (tweenType === tweenTypes.CSS_VAR) {
tweenStyle.setProperty(tweenProperty,/** @type {String} */(value));
}
}
if (isCurrentTimeAboveZero) hasRendered = 1;
} else {
// Used for composing timeline tweens without having to do a real render
tween._value = value;
}
}
// 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;
tweenTransformsNeedUpdate = 0;
}
tween = tween._next;
}
if (!muteCallbacks && hasRendered) {
/** @type {JSAnimation} */(tickable).onRender(/** @type {JSAnimation} */(tickable));
}
}
if (!muteCallbacks && isCurrentTimeAboveZero) {
tickable.onUpdate(/** @type {CallbackArgument} */(tickable));
}
}
// End tweens rendering
// Handle setters on timeline differently and allow re-trigering the onComplete callback when seeking backwards
if (parent && isSetter) {
if (!muteCallbacks && (
(parent.began && !isRunningBackwards && tickableAbsoluteTime >= duration && !completed) ||
(isRunningBackwards && tickableAbsoluteTime <= minValue && completed)
)) {
tickable.onComplete(/** @type {CallbackArgument} */(tickable));
tickable.completed = !isRunningBackwards;
}
// If currentTime is both above 0 and at least equals to duration, handles normal onComplete or infinite loops
} else if (isCurrentTimeAboveZero && isCurrentTimeEqualOrAboveDuration) {
if (iterationCount === Infinity) {
// Offset the tickable _startTime with its duration to reset _currentTime to 0 and continue the infinite timer
tickable._startTime += tickable.duration;
} else if (tickable._currentIteration >= iterationCount - 1) {
// By setting paused to true, we tell the engine loop to not render this tickable and removes it from the list on the next tick
tickable.paused = true;
if (!completed && !_hasChildren) {
// If the tickable has children, triggers onComplete() only when all children have completed in the tick function
tickable.completed = true;
if (!muteCallbacks && !(parent && (isRunningBackwards || !parent.began))) {
tickable.onComplete(/** @type {CallbackArgument} */(tickable));
tickable._resolve(/** @type {CallbackArgument} */(tickable));
}
}
}
// Otherwise set the completed flag to false
} else {
tickable.completed = false;
}
// NOTE: hasRendered * direction (negative for backwards) this way we can remove the tickable.backwards property completly ?
return hasRendered;
};
/**
* @param {Tickable} tickable
* @param {Number} time
* @param {Number} muteCallbacks
* @param {Number} internalRender
* @param {Number} tickMode
* @return {void}
*/
const tick = (tickable, time, muteCallbacks, internalRender, tickMode) => {
const _currentIteration = tickable._currentIteration;
render(tickable, time, muteCallbacks, internalRender, tickMode);
if (tickable._hasChildren) {
const tl = /** @type {Timeline} */(tickable);
const tlIsRunningBackwards = tl.backwards;
const tlChildrenTime = internalRender ? time : tl._iterationTime;
const tlCildrenTickTime = now();
let tlChildrenHasRendered = 0;
let tlChildrenHaveCompleted = true;
// If the timeline has looped forward, we need to manually triggers children skipped callbacks
if (!internalRender && tl._currentIteration !== _currentIteration) {
const tlIterationDuration = tl.iterationDuration;
forEachChildren(tl, (/** @type {JSAnimation} */child) => {
if (!tlIsRunningBackwards) {
// Force an internal render to trigger the callbacks if the child has not completed on loop
if (!child.completed && !child.backwards && child._currentTime < child.iterationDuration) {
render(child, tlIterationDuration, muteCallbacks, 1, tickModes.FORCE);
}
// Reset their began and completed flags to allow retrigering callbacks on the next iteration
child.began = false;
child.completed = false;
} else {
const childDuration = child.duration;
const childStartTime = child._offset + child._delay;
const childEndTime = childStartTime + childDuration;
// Triggers the onComplete callback on reverse for children on the edges of the timeline
if (!muteCallbacks && childDuration <= minValue && (!childStartTime || childEndTime === tlIterationDuration)) {
child.onComplete(child);
}
}
});
if (!muteCallbacks) tl.onLoop(/** @type {CallbackArgument} */(tl));
}
forEachChildren(tl, (/** @type {JSAnimation} */child) => {
const childTime = round((tlChildrenTime - child._offset) * child._speed, 12); // Rounding is needed when using seconds
const childTickMode = child._fps < tl._fps ? child.requestTick(tlCildrenTickTime) : tickMode;
tlChildrenHasRendered += render(child, childTime, muteCallbacks, internalRender, childTickMode);
if (!child.completed && tlChildrenHaveCompleted) tlChildrenHaveCompleted = false;
}, tlIsRunningBackwards);
// 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
tl.paused = true;
if (!tl.completed) {
tl.completed = true;
if (!muteCallbacks) {
tl.onComplete(/** @type {CallbackArgument} */(tl));
tl._resolve(/** @type {CallbackArgument} */(tl));
}
}
}
}
};
const additive = {
animation: null,
update: noop,
};
/**
* @typedef AdditiveAnimation
* @property {Number} duration
* @property {Number} _offset
* @property {Number} _delay
* @property {Tween} _head
* @property {Tween} _tail
*/
/**
* @param {TweenAdditiveLookups} lookups
* @return {AdditiveAnimation}
*/
const addAdditiveAnimation = lookups => {
let animation = additive.animation;
if (!animation) {
animation = {
duration: minValue,
computeDeltaTime: noop,
_offset: 0,
_delay: 0,
_head: null,
_tail: null,
};
additive.animation = animation;
additive.update = () => {
lookups.forEach(propertyAnimation => {
for (let propertyName in propertyAnimation) {
const tweens = propertyAnimation[propertyName];
const lookupTween = tweens._head;
if (lookupTween) {
const valueType = lookupTween._valueType;
const additiveValues = valueType === valueTypes.COMPLEX || valueType === valueTypes.COLOR ? cloneArray(lookupTween._fromNumbers) : null;
let additiveValue = lookupTween._fromNumber;
let tween = tweens._tail;
while (tween && tween !== lookupTween) {
if (additiveValues) {
for (let i = 0, l = tween._numbers.length; i < l; i++) additiveValues[i] += tween._numbers[i];
} else {
additiveValue += tween._number;
}
tween = tween._prevAdd;
}
lookupTween._toNumber = additiveValue;
lookupTween._toNumbers = additiveValues;
}
}
});
// TODO: Avoid polymorphism here, idealy the additive animation should be a regular animation with a higher priority in the render loop
render(animation, 1, 1, 0, tickModes.FORCE);
};
}
return animation;
};
const engineTickMethod = /*#__PURE__*/ (() => isBrowser ? requestAnimationFrame : setImmediate)();
const engineCancelMethod = /*#__PURE__*/ (() => isBrowser ? cancelAnimationFrame : clearImmediate)();
class Engine extends Clock {
/** @param {Number} [initTime] */
constructor(initTime) {
super(initTime);
this.useDefaultMainLoop = true;
this.pauseOnDocumentHidden = true;
/** @type {DefaultsParams} */
this.defaults = defaults;
this.paused = isBrowser && doc.hidden ? true : false;
/** @type {Number|NodeJS.Immediate} */
this.reqId = null;
}
update() {
const time = this._currentTime = now();
if (this.requestTick(time)) {
this.computeDeltaTime(time);
const engineSpeed = this._speed;
const engineFps = this._fps;
let activeTickable = /** @type {Tickable} */(this._head);
while (activeTickable) {
const nextTickable = activeTickable._next;
if (!activeTickable.paused) {
tick(
activeTickable,
(time - activeTickable._startTime) * activeTickable._speed * engineSpeed,
0, // !muteCallbacks
0, // !internalRender
activeTickable._fps < engineFps ? activeTickable.requestTick(time) : tickModes.AUTO
);
} else {
removeChild(this, activeTickable);
this._hasChildren = !!this._tail;
activeTickable._running = false;
if (activeTickable.completed && !activeTickable._cancelled) {
activeTickable.cancel();
}
}
activeTickable = nextTickable;
}
additive.update();
}
}
wake() {
if (this.useDefaultMainLoop && !this.reqId && !this.paused) {
this.reqId = engineTickMethod(tickEngine);
}
return this;
}
pause() {
this.paused = true;
return killEngine();
}
resume() {
if (!this.paused) return;
this.paused = false;
forEachChildren(this, (/** @type {Tickable} */child) => child.resetTime());
return this.wake();
}
// Getter and setter for speed
get speed() {
return this._speed * (globals.timeScale === 1 ? 1 : K);
}
set speed(playbackRate) {
this._speed = playbackRate * globals.timeScale;
forEachChildren(this, (/** @type {Tickable} */child) => child.speed = child._speed);
}
// Getter and setter for timeUnit
get timeUnit() {
return globals.timeScale === 1 ? 'ms' : 's';
};
set timeUnit(unit) {
const secondsScale = 0.001;
const isSecond = unit === 's';
const newScale = isSecond ? secondsScale : 1;
if (globals.timeScale !== newScale) {
globals.timeScale = newScale;
globals.tickThreshold = 200 * newScale;
const scaleFactor = isSecond ? secondsScale : K;
/** @type {Number} */
(this.defaults.duration) *= scaleFactor;
this._speed *= scaleFactor;
}
}
// Getter and setter for precision
get precision() {
return globals.precision;
}
set precision(precision) {
globals.precision = precision;
}
}
const engine = /*#__PURE__*/(() => {
const engine = new Engine(now());
if (isBrowser) {
globalVersions.engine = engine;
doc.addEventListener('visibilitychange', () => {
if (!engine.pauseOnDocumentHidden) return;
doc.hidden ? engine.pause() : engine.resume();
});
}
return engine;
})();
const tickEngine = () => {
if (engine._head) {
engine.reqId = engineTickMethod(tickEngine);
engine.update();
} else {
engine.reqId = 0;
}
};
const killEngine = () => {
engineCancelMethod(/** @type {NodeJS.Immediate & Number} */(engine.reqId));
engine.reqId = 0;
return engine;
};
/**
* @param {DOMTarget} target
* @param {String} propName
* @param {Object} animationInlineStyles
* @return {String}
*/
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;
}
}
}
}
return inlineTransforms && !isUnd(inlinedStylesPropertyValue) ? inlinedStylesPropertyValue :
stringStartsWith(propName, 'scale') ? '1' :
stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew') ? '0deg' : '0px';
};
/**
* @param {DOMTargetsParam|TargetsParam} v
* @return {NodeList|HTMLCollection}
*/
function getNodeList(v) {
const n = isStr(v) ? scope.root.querySelectorAll(v) : v;
if (n instanceof NodeList || n instanceof HTMLCollection) return n;
}
/**
* @overload
* @param {DOMTargetsParam} targets
* @return {DOMTargetsArray}
*
* @overload
* @param {JSTargetsParam} targets
* @return {JSTargetsArray}
*
* @overload
* @param {TargetsParam} targets
* @return {TargetsArray}
*
* @param {DOMTargetsParam|JSTargetsParam|TargetsParam} targets
*/
function parseTargets(targets) {
if (isNil(targets)) return /** @type {TargetsArray} */([]);
if (isArr(targets)) {
const flattened = targets.flat(Infinity);
/** @type {TargetsArray} */
const parsed = [];
for (let i = 0, l = flattened.length; i < l; i++) {
const item = flattened[i];
if (!isNil(item)) {
const nodeList = getNodeList(item);
if (nodeList) {
for (let j = 0, jl = nodeList.length; j < jl; j++) {
const subItem = nodeList[j];
if (!isNil(subItem)) {
let isDuplicate = false;
for (let k = 0, kl = parsed.length; k < kl; k++) {
if (parsed[k] === subItem) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
parsed.push(subItem);
}
}
}
} else {
let isDuplicate = false;
for (let j = 0, jl = parsed.length; j < jl; j++) {
if (parsed[j] === item) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
parsed.push(item);
}
}
}
}
return parsed;
}
if (!isBrowser) return /** @type {JSTargetsArray} */([targets]);
const nodeList = getNodeList(targets);
if (nodeList) return /** @type {DOMTargetsArray} */(Array.from(nodeList));
return /** @type {TargetsArray} */([targets]);
}
/**
* @overload
* @param {DOMTargetsParam} targets
* @return {DOMTargetsArray}
*
* @overload
* @param {JSTargetsParam} targets
* @return {JSTargetsArray}
*
* @overload
* @param {TargetsParam} targets
* @return {TargetsArray}
*
* @param {DOMTargetsParam|JSTargetsParam|TargetsParam} targets
*/
function registerTargets(targets) {
const parsedTargetsArray = parseTargets(targets);
const parsedTargetsLength = parsedTargetsArray.length;
if (parsedTargetsLength) {
for (let i = 0; i < parsedTargetsLength; i++) {
const target = parsedTargetsArray[i];
if (!target[isRegisteredTargetSymbol]) {
target[isRegisteredTargetSymbol] = true;
const isSvgType = isSvg(target);
const isDom = /** @type {DOMTarget} */(target).nodeType || isSvgType;
if (isDom) {
target[isDomSymbol] = true;
target[isSvgSymbol] = isSvgType;
target[transformsSymbol] = {};
}
}
}
}
return parsedTargetsArray;
}
/**
* @param {TargetsParam} path
* @return {SVGGeometryElement|undefined}
*/
const getPath = path => {
const parsedTargets = parseTargets(path);
const $parsedSvg = /** @type {SVGGeometryElement} */(parsedTargets[0]);
if (!$parsedSvg || !isSvg($parsedSvg)) return;
return $parsedSvg;
};
/**
* @param {TargetsParam} path2
* @param {Number} [precision]
* @return {FunctionValue}
*/
const morphTo = (path2, precision = .33) => ($path1) => {
const $path2 = /** @type {SVGGeometryElement} */(getPath(path2));
if (!$path2) return;
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(pointOnPath1.x, 3) + separator + pointOnPath1.y + ' ';
v2 += prefix + round(pointOnPath2.x, 3) + separator + pointOnPath2.y + ' ';
}
}
$path1[morphPointsSymbol] = v2;
return [v1, v2];
};
/**
* @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 * -1e3 * 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<DrawableSVGGeometry>} - 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
));
};
// Motion path animation
/**
* @param {SVGGeometryElement} $path
* @param {Number} progress
* @param {Number}lookup
* @return {DOMPoint}
*/
const getPathPoint = ($path, progress, lookup = 0) => {
return $path.getPointAtLength(progress + lookup >= 1 ? progress + lookup : 0);
};
/**
* @param {SVGGeometryElement} $path
* @param {String} pathProperty
* @return {FunctionValue}
*/
const getPathProgess = ($path, pathProperty) => {
return $el => {
const totalLength = +($path.getTotalLength());
const inSvg = $el[isSvgSymbol];
const ctm = $path.getCTM();
/** @type {TweenObjectValue} */
return {
from: 0,
to: totalLength,
/** @type {TweenModifier} */
modifier: progress => {
if (pathProperty === 'a') {
const p0 = getPathPoint($path, progress, -1);
const p1 = getPathPoint($path, progress, 1);
return atan2(p1.y - p0.y, p1.x - p0.x) * 180 / PI;
} else {
const p = getPathPoint($path, progress, 0);
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
*/
const createMotionPath = path => {
const $path = getPath(path);
if (!$path) return;
return {
translateX: getPathProgess($path, 'x'),
translateY: getPathProgess($path, 'y'),
rotate: getPathProgess($path, 'a'),
}
};
// Check for valid SVG attribute
const cssReservedProperties = ['opacity', 'rotate', 'overflow', 'color'];
/**
* @param {Target} el
* @param {String} propertyName
* @return {Boolean}
*/
const isValidSVGAttribute = (el, propertyName) => {
// Return early and use CSS opacity animation instead (already better default values (opacity: 1 instead of 0)) and rotate should be considered a transform
if (cssReservedProperties.includes(propertyName)) return false;
if (el.getAttribute(propertyName) || propertyName in el) {
if (propertyName === 'scale') { // Scale
const elParentNode = /** @type {SVGGeometryElement} */(/** @type {DOMTarget} */(el).parentNode);
// Only consider scale as a valid SVG attribute on filter element
return elParentNode && elParentNode.tagName === 'filter';
}
return true;
}
};
const svg = {
morphTo,
createMotionPath,
createDrawable,
};
/**
* RGB / RGBA Color value string -> RGBA values array
* @param {String} rgbValue
* @return {ColorArray}
*/
const rgbToRgba = rgbValue => {
const rgba = rgbExecRgx.exec(rgbValue) || rgbaExecRgx.exec(rgbValue);
const a = !isUnd(rgba[4]) ? +rgba[4] : 1;
return [
+rgba[1],
+rgba[2],
+rgba[3],
a
]
};
/**
* HEX3 / HEX3A / HEX6 / HEX6A Color value string -> RGBA values array
* @param {String} hexValue
* @return {ColorArray}
*/
const hexToRgba = hexValue => {
const hexLength = hexValue.length;
const isShort = hexLength === 4 || hexLength === 5;
return [
+('0x' + hexValue[1] + hexValue[isShort ? 1 : 2]),
+('0x' + hexValue[isShort ? 2 : 3] + hexValue[isShort ? 2 : 4]),
+('0x' + hexValue[isShort ? 3 : 5] + hexValue[isShort ? 3 : 6]),
((hexLength === 5 || hexLength === 9) ? +(+('0x' + hexValue[isShort ? 4 : 7] + hexValue[isShort ? 4 : 8]) / 255).toFixed(3) : 1)
]
};
/**
* @param {Number} p
* @param {Number} q
* @param {Number} t
* @return {Number}
*/
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
return t < 1 / 6 ? p + (q - p) * 6 * t :
t < 1 / 2 ? q :
t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 :
p;
};
/**
* HSL / HSLA Color value string -> RGBA values array
* @param {String} hslValue
* @return {ColorArray}
*/
const hslToRgba = hslValue => {
const hsla = hslExecRgx.exec(hslValue) || hslaExecRgx.exec(hslValue);
const h = +hsla[1] / 360;
const s = +hsla[2] / 100;
const l = +hsla[3] / 100;
const a = !isUnd(hsla[4]) ? +hsla[4] : 1;
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const q = l < .5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = round(hue2rgb(p, q, h + 1 / 3) * 255, 0);
g = round(hue2rgb(p, q, h) * 255, 0);
b = round(hue2rgb(p, q, h - 1 / 3) * 255, 0);
}
return [r, g, b, a];
};
/**
* All in one color converter that converts a color string value into an array of RGBA values
* @param {String} colorString
* @return {ColorArray}
*/
const convertColorStringValuesToRgbaArray = colorString => {
return isRgb(colorString) ? rgbToRgba(colorString) :
isHex(colorString) ? hexToRgba(colorString) :
isHsl(colorString) ? hslToRgba(colorString) :
[0, 0, 0, 1];
};
/**
* @template T, D
* @param {T|undefined} targetValue
* @param {D} defaultValue
* @return {T|D}
*/
const setValue = (targetValue, defaultValue) => {
return isUnd(targetValue) ? defaultValue : targetValue;
};
/**
* @param {TweenPropValue} value
* @param {Target} target
* @param {Number} index
* @param {Number} total
* @param {Object} [store]
* @return {any}
*/
const getFunctionValue = (value, target, index, total, store) => {
if (isFnc(value)) {
const func = () => {
const computed = /** @type {Function} */(value)(target, index, total);
// Fallback to 0 if the function returns undefined / NaN / null / false / 0
return !isNaN(+computed) ? +computed : computed || 0;
};
if (store) {
store.func = func;
}
return func();
} else {
return value;
}
};
/**
* @param {Target} target
* @param {String} prop
* @return {tweenTypes}
*/
const getTweenType = (target, prop) => {
return !target[isDomSymbol] ? tweenTypes.OBJECT :
// Handle SVG attributes
target[isSvgSymbol] && isValidSVGAttribute(target, prop) ? tweenTypes.ATTRIBUTE :
// Handle CSS Transform properties differently than CSS to allow individual animations
validTransforms.includes(prop) || shortTransforms.get(prop) ? tweenTypes.TRANSFORM :
// CSS variables
stringStartsWith(prop, '--') ? tweenTypes.CSS_VAR :
// All other CSS properties
prop in /** @type {DOMTarget} */(target).style ? tweenTypes.CSS :
// Handle other DOM Attributes
prop in target ? tweenTypes.OBJECT :
tweenTypes.ATTRIBUTE;
};
/**
* @param {DOMTarget} target
* @param {String} propName
* @param {Object} animationInlineStyles
* @return {String}
*/
const getCSSValue = (target, propName, animationInlineStyles) => {
const inlineStyles = target.style[propName];
if (inlineStyles && animationInlineStyles) {
animationInlineStyles[propName] = inlineStyles;
}
const value = inlineStyles || getComputedStyle(target[proxyTargetSymbol] || target).getPropertyValue(propName);
return value === 'auto' ? '0' : value;
};
/**
* @param {Target} target
* @param {String} propName
* @param {tweenTypes} [tweenType]
* @param {Object|void} [animationInlineStyles]
* @return {String|Number}
*/
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) :
type === tweenTypes.CSS_VAR ? getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles).trimStart() :
getCSSValue(/** @type {DOMTarget} */(target), propName, animationInlineStyles);
};
/**
* @param {Number} x
* @param {Number} y
* @param {String} operator
* @return {Number}
*/
const getRelativeValue = (x, y, operator) => {
return operator === '-' ? x - y :
operator === '+' ? x + y :
x * y;
};
/** @return {TweenDecomposedValue} */
const createDecomposedValueTargetObject = () => {
return {
/** @type {valueTypes} */
t: valueTypes.NUMBER,
n: 0,
u: null,
o: null,
d: null,
s: null,
}
};
/**
* @param {String|Number} rawValue
* @param {TweenDecomposedValue} targetObject
* @return {TweenDecomposedValue}
*/
const decomposeRawValue = (rawValue, targetObject) => {
/** @type {valueTypes} */
targetObject.t = valueTypes.NUMBER;
targetObject.n = 0;
targetObject.u = null;
targetObject.o = null;
targetObject.d = null;
targetObject.s = null;
if (!rawValue) return targetObject;
const num = +rawValue;
if (!isNaN(num)) {
// It's a number
targetObject.n = num;
return targetObject;
} else {
// let str = /** @type {String} */(rawValue).trim();
let str = /** @type {String} */(rawValue);
// Parsing operators (+=, -=, *=) manually is much faster than using regex here
if (str[1] === '=') {
targetObject.o = str[0];
str = str.slice(2);
}
// Skip exec regex if the value type is complex or color to avoid long regex backtracking
const unitMatch = str.includes(' ') ? false : unitsExecRgx.exec(str);
if (unitMatch) {
// Has a number and a unit
targetObject.t = valueTypes.UNIT;
targetObject.n = +unitMatch[1];
targetObject.u = unitMatch[2];
return targetObject;
} else if (targetObject.o) {
// Has an operator (+=, -=, *=)
targetObject.n = +str;
return targetObject;
} else if (isCol(str)) {
// Is a color
targetObject.t = valueTypes.COLOR;
targetObject.d = convertColorStringValuesToRgbaArray(str);
return targetObject;
} else {
// Is a more complex string (generally svg coords, calc() or filters CSS values)
const matchedNumbers = str.match(digitWithExponentRgx);
targetObject.t = valueTypes.COMPLEX;
targetObject.d = matchedNumbers ? matchedNumbers.map(Number) : [];
targetObject.s = str.split(digitWithExponentRgx) || [];
return targetObject;
}
}
};
/**
* @param {Tween} tween
* @param {TweenDecomposedValue} targetObject
* @return {TweenDecomposedValue}
*/
const decomposeTweenValue = (tween, targetObject) => {
targetObject.t = tween._valueType;
targetObject.n = tween._toNumber;
targetObject.u = tween._unit;
targetObject.o = null;
targetObject.d = cloneArray(tween._toNumbers);
targetObject.s = cloneArray(tween._strings);
return targetObject;
};
const decomposedOriginalValue = createDecomposedValueTargetObject();
const lookups = {
/** @type {TweenReplaceLookups} */
_rep: new WeakMap(),
/** @type {TweenAdditiveLookups} */
_add: new Map(),
};
/**
* @param {Target} target
* @param {String} property
* @param {String} lookup
* @return {TweenPropertySiblings}
*/
const getTweenSiblings = (target, property, lookup = '_rep') => {
const lookupMap = lookups[lookup];
let targetLookup = lookupMap.get(target);
if (!targetLookup) {
targetLookup = {};
lookupMap.set(target, targetLookup);
}
return targetLookup[property] ? targetLookup[property] : targetLookup[property] = {
_head: null,
_tail: null,
}
};
/**
* @param {Tween} p
* @param {Tween} c
* @return {Number|Boolean}
*/
const addTweenSortMethod = (p, c) => {
return p._isOverridden || p._absoluteStartTime > c._absoluteStartTime;
};
/**
* @param {Tween} tween
*/
const overrideTween = tween => {
tween._isOverlapped = 1;
tween._isOverridden = 1;
tween._changeDuration = minValue;
tween._currentTime = minValue;
};
/**
* @param {Tween} tween
* @param {TweenPropertySiblings} siblings
* @return {Tween}
*/
const composeTween = (tween, siblings) => {
const tweenCompositionType = tween._composition;
// Handle replaced tweens
if (tweenCompositionType === compositionTypes.replace) {
const tweenAbsStartTime = tween._absoluteStartTime;
addChild(siblings, tween, addTweenSortMethod, '_prevRep', '_nextRep');
const prevSibling = tween._prevRep;
// Update the previous siblings for composition replace tweens
if (prevSibling) {
const prevParent = prevSibling.parent;
const prevAbsEndTime = prevSibling._absoluteStartTime + prevSibling._changeDuration;
// Handle looped animations tween
if (
// Check if the previous tween is from a different animation
tween.parent.id !== prevParent.id &&
// Check if the animation has loops
prevParent.iterationCount> 1 &&
// Check if _absoluteChangeEndTime of last loop overlaps the current tween
prevAbsEndTime + (prevParent.duration - prevParent.iterationDuration) > tweenAbsStartTime
) {
// TODO: Find a way to only override the iterations overlapping with the tween
overrideTween(prevSibling);
let prevPrevSibling = prevSibling._prevRep;
// If the tween was part of a set of keyframes, override its siblings
while (prevPrevSibling && prevPrevSibling.parent.id === prevParent.id) {
overrideTween(prevPrevSibling);
prevPrevSibling = prevPrevSibling._prevRep;
}
}
const absoluteUpdateStartTime = tweenAbsStartTime - tween._delay;
if (prevAbsEndTime > absoluteUpdateStartTime) {
const prevChangeStartTime = prevSibling._startTime;
const prevTLOffset = prevAbsEndTime - (prevChangeStartTime + prevSibling._updateDuration);
prevSibling._changeDuration = absoluteUpdateStartTime - prevTLOffset - prevChangeStartTime;
prevSibling._currentTime = prevSibling._changeDuration;
prevSibling._isOverlapped = 1;
if (prevSibling._changeDuration < minValue) {
overrideTween(prevSibling);
}
}
// Pause (and cancel) the parent if it only contains overlapped tweens
let pausePrevParentAnimation = true;
forEachChildren(prevParent, (/** @type Tween */t) => {
if (!t._isOverlapped) pausePrevParentAnimation = false;
});
if (pausePrevParentAnimation) {
const prevParentTL = prevParent.parent;
if (prevParentTL) {
let pausePrevParentTL = true;
forEachChildren(prevParentTL, (/** @type JSAnimation */a) => {
if (a !== prevParent) {
forEachChildren(a, (/** @type Tween */t) => {
if (!t._isOverlapped) pausePrevParentTL = false;
});
}
});
if (pausePrevParentTL) {
prevParentTL.cancel();
}
} else {
prevParent.cancel();
// Previously, calling .cancel() on a timeline child would affect the render order of other children
// Worked around this by marking it as .completed and using .pause() for safe removal in the engine loop
// This is no longer needed since timeline tween composition is now handled separatly
// Keeping this here for reference
// prevParent.completed = true;
// prevParent.pause();
}
}
}
// let nextSibling = tween._nextRep;
// // All the next siblings are automatically overridden
// if (nextSibling && nextSibling._absoluteStartTime >= tweenAbsStartTime) {
// while (nextSibling) {
// overrideTween(nextSibling);
// nextSibling = nextSibling._nextRep;
// }
// }
// if (nextSibling && nextSibling._absoluteStartTime < tweenAbsStartTime) {
// while (nextSibling) {
// overrideTween(nextSibling);
// console.log(tween.id, nextSibling.id);
// nextSibling = nextSibling._nextRep;
// }
// }
// Handle additive tweens composition
} else if (tweenCompositionType === compositionTypes.blend) {
const additiveTweenSiblings = getTweenSiblings(tween.target, tween.property, '_add');
const additiveAnimation = addAdditiveAnimation(lookups._add);
let lookupTween = additiveTweenSiblings._head;
if (!lookupTween) {
lookupTween = { ...tween };
lookupTween._composition = compositionTypes.replace;
lookupTween._updateDuration = minValue;
lookupTween._startTime = 0;
lookupTween._numbers = cloneArray(tween._fromNumbers);
lookupTween._number = 0;
lookupTween._next = null;
lookupTween._prev = null;
addChild(additiveTweenSiblings, lookupTween);
addChild(additiveAnimation, lookupTween);
}
// Convert the values of TO to FROM and set TO to 0
const toNumber = tween._toNumber;
tween._fromNumber = lookupTween._fromNumber - toNumber;
tween._toNumber = 0;
tween._numbers = cloneArray(tween._fromNumbers);
tween._number = 0;
lookupTween._fromNumber = toNumber;
if (tween._toNumbers) {
const toNumbers = cloneArray(tween._toNumbers);
if (toNumbers) {
toNumbers.forEach((value, i) => {
tween._fromNumbers[i] = lookupTween._fromNumbers[i] - value;
tween._toNumbers[i] = 0;
});
}
lookupTween._fromNumbers = toNumbers;
}
addChild(additiveTweenSiblings, tween, null, '_prevAdd', '_nextAdd');
}
return tween;
};
/**
* @param {Tween} tween
* @return {Tween}
*/
const removeTweenSliblings = tween => {
const tweenComposition = tween._composition;
if (tweenComposition !== compositionTypes.none) {
const tweenTarget = tween.target;
const tweenProperty = tween.property;
const replaceTweensLookup = lookups._rep;
const replaceTargetProps = replaceTweensLookup.get(tweenTarget);
const tweenReplaceSiblings = replaceTargetProps[tweenProperty];
removeChild(tweenReplaceSiblings, tween, '_prevRep', '_nextRep');
if (tweenComposition === compositionTypes.blend) {
const addTweensLookup = lookups._add;
const addTargetProps = addTweensLookup.get(tweenTarget);
if (!addTargetProps) return;
const additiveTweenSiblings = addTargetProps[tweenProperty];
const additiveAnimation = additive.animation;
removeChild(additiveTweenSiblings, tween, '_prevAdd', '_nextAdd');
// If only one tween is left in the additive lookup, it's the tween lookup
const lookupTween = additiveTweenSiblings._head;
if (lookupTween && lookupTween === additiveTweenSiblings._tail) {
removeChild(additiveTweenSiblings, lookupTween, '_prevAdd', '_nextAdd');
removeChild(additiveAnimation, lookupTween);
let shouldClean = true;
for (let prop in addTargetProps) {
if (addTargetProps[prop]._head) {
shouldClean = false;
break;
}
}
if (shouldClean) {
addTweensLookup.delete(tweenTarget);
}
}
}
}
return tween;
};
/**
* @param {Timer} timer
* @return {Timer}
*/
const resetTimerProperties = timer => {
timer.paused = true;
timer.began = false;
timer.completed = false;
return timer;
};
/**
* @param {Timer} timer
* @return {Timer}
*/
const reviveTimer = timer => {
if (!timer._cancelled) return timer;
if (timer._hasChildren) {
forEachChildren(timer, reviveTimer);
} else {
forEachChildren(timer, (/** @type {Tween} tween*/tween) => {
if (tween._composition !== compositionTypes.none) {
composeTween(tween, getTweenSiblings(tween.target, tween.property));
}
});
}
timer._cancelled = 0;
return timer;
};
let timerId = 0;
/**
* Base class used to create Timers, Animations and Timelines
*/
class Timer extends Clock {
/**
* @param {TimerParams} [parameters]
* @param {Timeline} [parent]
* @param {Number} [parentPosition]
*/
constructor(parameters = {}, parent = null, parentPosition = 0) {
super(0);
const {
id,
delay,
duration,
reversed,
alternate,
loop,
loopDelay,
autoplay,
frameRate,
playbackRate,
onComplete,
onLoop,
onPause,
onBegin,
onBeforeUpdate,
onUpdate,
} = parameters;
if (scope.current) scope.current.register(this);
const timerInitTime = parent ? 0 : engine._elapsedTime;
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 offsetPosition = 0;
if (parent) {
offsetPosition = parentPosition;
} else {
let startTime = now();
// Make sure to tick the engine once if suspended to avoid big gaps with the following offsetPosition calculation
if (engine.paused) {
engine.requestTick(startTime);
startTime = engine._elapsedTime;
}
offsetPosition = startTime - engine._startTime;
}
// Timer's parameters
this.id = !isUnd(id) ? id : ++timerId;
/** @type {Timeline} */
this.parent = parent;
// Total duration of the timer
this.duration = clampInfinity(((timerDuration + timerLoopDelay) * timerIterationCount) - timerLoopDelay) || minValue;
/** @type {Boolean} */
this.backwards = false;
/** @type {Boolean} */
this.paused = true;
/** @type {Boolean} */
this.began = false;
/** @type {Boolean} */
this.completed = false;
/** @type {Callback<this>} */
this.onBegin = onBegin || timerDefaults.onBegin;
/** @type {Callback<this>} */
this.onBeforeUpdate = onBeforeUpdate || timerDefaults.onBeforeUpdate;
/** @type {Callback<this>} */
this.onUpdate = onUpdate || timerDefaults.onUpdate;
/** @type {Callback<this>} */
this.onLoop = onLoop || timerDefaults.onLoop;
/** @type {Callback<this>} */
this.onPause = onPause || timerDefaults.onPause;
/** @type {Callback<this>} */
this.onComplete = onComplete || timerDefaults.onComplete;
/** @type {Number} */
this.iterationDuration = timerDuration; // Duration of one loop
/** @type {Number} */
this.iterationCount = timerIterationCount; // Number of loops
/** @type {Boolean|ScrollObserver} */
this._autoplay = parent ? false : setValue(autoplay, timerDefaults.autoplay);
/** @type {Number} */
this._offset = offsetPosition;
/** @type {Number} */
this._delay = timerDelay;
/** @type {Number} */
this._loopDelay = timerLoopDelay;
/** @type {Number} */
this._iterationTime = 0;
/** @type {Number} */
this._currentIteration = 0; // Current loop index
/** @type {Function} */
this._resolve = noop; // Used by .then()
/** @type {Boolean} */
this._running = false;
/** @type {Number} */
this._reversed = +setValue(reversed, timerDefaults.reversed);
/** @type {Number} */
this._reverse = this._reversed;
/** @type {Number} */
this._cancelled = 0;
/** @type {Boolean} */
this._alternate = setValue(alternate, timerDefaults.alternate);
/** @type {Renderable} */
this._prev = null;
/** @type {Renderable} */
this._next = null;
// Clock's parameters
/** @type {Number} */
this._elapsedTime = timerInitTime;
/** @type {Number} */
this._startTime = timerInitTime;
/** @type {Number} */
this._lastTime = timerInitTime;
/** @type {Number} */
this._fps = setValue(frameRate, timerDefaults.frameRate);
/** @type {Number} */
this._speed = setValue(playbackRate, timerDefaults.playbackRate);
}
get cancelled() {
return !!this._cancelled;
}
/** @param {Boolean} cancelled */
set cancelled(cancelled) {
cancelled ? this.cancel() : this.reset(1).play();
}
get currentTime() {
return clamp(round(this._currentTime, globals.precision), -this._delay, this.duration);
}
/** @param {Number} time */
set currentTime(time) {
const paused = this.paused;
// Pausing the timer is necessary to avoid time jumps on a running instance
this.pause().seek(+time);
if (!paused) this.resume();
}
get iterationCurrentTime() {
return round(this._iterationTime, globals.precision);
}
/** @param {Number} time */
set iterationCurrentTime(time) {
this.currentTime = (this.iterationDuration * this._currentIteration) + time;
}
get progress() {
return clamp(round(this._currentTime / this.duration, 10), 0, 1);
}
/** @param {Number} progress */
set progress(progress) {
this.currentTime = this.duration * progress;
}
get iterationProgress() {
return clamp(round(this._iterationTime / this.iterationDuration, 10), 0, 1);
}
/** @param {Number} progress */
set iterationProgress(progress) {
const iterationDuration = this.iterationDuration;
this.currentTime = (iterationDuration * this._currentIteration) + (iterationDuration * progress);
}
get currentIteration() {
return this._currentIteration;
}
/** @param {Number} iterationCount */
set currentIteration(iterationCount) {
this.currentTime = (this.iterationDuration * clamp(+iterationCount, 0, this.iterationCount - 1));
}
get reversed() {
return !!this._reversed;
}
/** @param {Boolean} reverse */
set reversed(reverse) {
reverse ? this.reverse() : this.play();
}
get speed() {
return super.speed;
}
/** @param {Number} playbackRate */
set speed(playbackRate) {
super.speed = playbackRate;
this.resetTime();
}
/**
* @param {Number} internalRender
* @return {this}
*/
reset(internalRender = 0) {
// If cancelled, revive the timer before rendering in order to have propertly composed tweens siblings
reviveTimer(this);
if (this._reversed && !this._reverse) this.reversed = false;
// Rendering before updating the completed flag to prevent skips and to make sure the properties are not overridden
// Setting the iterationTime at the end to force the rendering to happend backwards, otherwise calling .reset() on Timelines might not render children in the right order
// NOTE: This is only required for Timelines and might be better to move to the Timeline class?
this._iterationTime = this.iterationDuration;
// Set tickMode to tickModes.FORCE to force rendering
tick(this, 0, 1, internalRender, tickModes.FORCE);
// Reset timer properties after revive / render to make sure the props are not updated again
resetTimerProperties(this);
// Also reset children properties
if (this._hasChildren) {
forEachChildren(this, resetTimerProperties);
}
return this;
}
/**
* @param {Number} internalRender
* @return {this}
*/
init(internalRender = 0) {
this.fps = this._fps;
this.speed = this._speed;
// Manually calling .init() on timelines should render all children intial state
// Forces all children to render once then render to 0 when reseted
if (!internalRender && this._hasChildren) {
tick(this, this.duration, 1, internalRender, tickModes.FORCE);
}
this.reset(internalRender);
// Make sure to set autoplay to false to child timers so it doesn't attempt to autoplay / link
const autoplay = this._autoplay;
if (autoplay === true) {
this.resume();
} else if (autoplay && !isUnd(/** @type {ScrollObserver} */(autoplay).linked)) {
/** @type {ScrollObserver} */(autoplay).link(this);
}
return this;
}
/** @return {this} */
resetTime() {
const timeScale = 1 / (this._speed * engine._speed);
this._startTime = now() - (this._currentTime + this._delay) * timeScale;
return this;
}
/** @return {this} */
pause() {
if (this.paused) return this;
this.paused = true;
this.onPause(this);
return this;
}
/** @return {this} */
resume() {
if (!this.paused) return this;
this.paused = false;
// We can safely imediatly render a timer that has no duration and no children
if (this.duration <= minValue && !this._hasChildren) {
tick(this, minValue, 0, 0, tickModes.FORCE);
} else {
if (!this._running) {
addChild(engine, this);
engine._hasChildren = true;
this._running = true;
}
this.resetTime();
// Forces the timer to advance by at least one frame when the next tick occurs
this._startTime -= 12;
engine.wake();
}
return this;
}
/** @return {this} */
restart() {
return this.reset(0).resume();
}
/**
* @param {Number} time
* @param {Boolean|Number} [muteCallbacks]
* @param {Boolean|Number} [internalRender]
* @return {this}
*/
seek(time, muteCallbacks = 0, internalRender = 0) {
// Recompose the tween siblings in case the timer has been cancelled
reviveTimer(this);
// If you seek a completed animation, otherwise the next play will starts at 0
this.completed = false;
const isPaused = this.paused;
this.paused = true;
// timer, time, muteCallbacks, internalRender, tickMode
tick(this, time + this._delay, ~~muteCallbacks, ~~internalRender, tickModes.AUTO);
return isPaused ? this : this.resume();
}
/** @return {this} */
alternate() {
const reversed = this._reversed;
const count = this.iterationCount;
const duration = this.iterationDuration;
// Calculate the maximum iterations possible given the iteration duration
const iterations = count === Infinity ? floor(maxValue / duration) : count;
this._reversed = +(this._alternate && !(iterations % 2) ? reversed : !reversed);
if (count === Infinity) {
// Handle infinite loops to loop on themself
this.iterationProgress = this._reversed ? 1 - this.iterationProgress : this.iterationProgress;
} else {
this.seek((duration * iterations) - this._currentTime);
}
this.resetTime();
return this;
}
/** @return {this} */
play() {
if (this._reversed) this.alternate();
return this.resume();
}
/** @return {this} */
reverse() {
if (!this._reversed) this.alternate();
return this.resume();
}
// TODO: Move all the animation / tweens / children related code to Animation / Timeline
/** @return {this} */
cancel() {
if (this._hasChildren) {
forEachChildren(this, (/** @type {Renderable} */child) => child.cancel(), true);
} else {
forEachChildren(this, removeTweenSliblings);
}
this._cancelled = 1;
// Pausing the timer removes it from the engine
return this.pause();
}
/**
* @param {Number} newDuration
* @return {this}
*/
stretch(newDuration) {
const currentDuration = this.duration;
const normlizedDuration = normalizeTime(newDuration);
if (currentDuration === normlizedDuration) return this;
const timeScale = newDuration / currentDuration;
const isSetter = newDuration <= minValue;
this.duration = isSetter ? minValue : normlizedDuration;
this.iterationDuration = isSetter ? minValue : normalizeTime(this.iterationDuration * timeScale);
this._offset *= timeScale;
this._delay *= timeScale;
this._loopDelay *= timeScale;
return this;
}
/**
* Cancels the timer by seeking it back to 0 and reverting the attached scroller if necessary
* @return {this}
*/
revert() {
tick(this, 0, 1, 0, tickModes.AUTO);
const ap = /** @type {ScrollObserver} */(this._autoplay);
if (ap && ap.linked && ap.linked === this) ap.revert();
return this.cancel();
}
/**
* Imediatly completes the timer, cancels it and triggers the onComplete callback
* @return {this}
*/
complete() {
return this.seek(this.duration).cancel();
}
/**
* @param {Callback<this>} [callback]
* @return {Promise}
*/
then(callback = noop) {
const then = this.then;
const onResolve = () => {
// this.then = null prevents infinite recursion if returned by an async function
// https://github.com/juliangarnierorg/anime-beta/issues/26
this.then = null;
callback(this);
this.then = then;
this._resolve = noop;
};
return new Promise(r => {
this._resolve = () => r(onResolve());
// Make sure to resolve imediatly if the timer has already completed
if (this.completed) this._resolve();
return this;
});
}
}
/**
* @param {TimerParams} [parameters]
* @return {Timer}
*/
const createTimer = parameters => new Timer(parameters, null, 0).init();
/** @type {EasingFunction} */
const none = t => t;
// Cubic Bezier solver adapted from https://github.com/gre/bezier-ease © Gaëtan Renaudeau
/**
* @param {Number} aT
* @param {Number} aA1
* @param {Number} aA2
* @return {Number}
*/
const calcBezier = (aT, aA1, aA2) => (((1 - 3 * aA2 + 3 * aA1) * aT + (3 * aA2 - 6 * aA1)) * aT + (3 * aA1)) * aT;
/**
* @param {Number} aX
* @param {Number} mX1
* @param {Number} mX2
* @return {Number}
*/
const binarySubdivide = (aX, mX1, mX2) => {
let aA = 0, aB = 1, currentX, currentT, i = 0;
do {
currentT = aA + (aB - aA) / 2;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0) {
aB = currentT;
} else {
aA = currentT;
}
} while (abs(currentX) > .0000001 && ++i < 100);
return currentT;
};
/**
* @param {Number} [mX1]
* @param {Number} [mY1]
* @param {Number} [mX2]
* @param {Number} [mY2]
* @return {EasingFunction}
*/
const cubicBezier = (mX1 = 0.5, mY1 = 0.0, mX2 = 0.5, mY2 = 1.0) => (mX1 === mY1 && mX2 === mY2) ? none :
t => t === 0 || t === 1 ? t :
calcBezier(binarySubdivide(t, mX1, mX2), mY1, mY2);
/**
* Steps ease implementation https://developer.mozilla.org/fr/docs/Web/CSS/transition-timing-function
* Only covers 'end' and 'start' jumpterms
* @param {Number} steps
* @param {Boolean} [fromStart]
* @return {EasingFunction}
*/
const steps = (steps = 10, fromStart) => {
const roundMethod = fromStart ? ceil : floor;
return t => roundMethod(clamp(t, 0, 1) * steps) * (1 / steps);
};
/**
* Without parameters, the linear function creates a non-eased transition.
* Parameters, if used, creates a piecewise linear easing by interpolating linearly between the specified points.
* @param {...String|Number} [args] - Points
* @return {EasingFunction}
*/
const linear = (...args) => {
const argsLength = args.length;
if (!argsLength) return none;
const totalPoints = argsLength - 1;
const firstArg = args[0];
const lastArg = args[totalPoints];
const xPoints = [0];
const yPoints = [parseNumber(firstArg)];
for (let i = 1; i < totalPoints; i++) {
const arg = args[i];
const splitValue = isStr(arg) ?
/** @type {String} */(arg).trim().split(' ') :
[arg];
const value = splitValue[0];
const percent = splitValue[1];
xPoints.push(!isUnd(percent) ? parseNumber(percent) / 100 : i / totalPoints);
yPoints.push(parseNumber(value));
}
yPoints.push(parseNumber(lastArg));
xPoints.push(1);
return function easeLinear(t) {
for (let i = 1, l = xPoints.length; i < l; i++) {
const currentX = xPoints[i];
if (t <= currentX) {
const prevX = xPoints[i - 1];
const prevY = yPoints[i - 1];
return prevY + (yPoints[i] - prevY) * (t - prevX) / (currentX - prevX);
}
}
return yPoints[yPoints.length - 1];
}
};
/**
* Generate random steps
* @param {Number} [length] - The number of steps
* @param {Number} [randomness] - How strong the randomness is
* @return {EasingFunction}
*/
const irregular = (length = 10, randomness = 1) => {
const values = [0];
const total = length - 1;
for (let i = 1; i < total; i++) {
const previousValue = values[i - 1];
const spacing = i / total;
const segmentEnd = (i + 1) / total;
const randomVariation = spacing + (segmentEnd - spacing) * Math.random();
// Mix the even spacing and random variation based on the randomness parameter
const randomValue = spacing * (1 - randomness) + randomVariation * randomness;
values.push(clamp(randomValue, previousValue, 1));
}
values.push(1);
return linear(...values);
};
// Easing functions adapted from http://www.robertpenner.com/ease © Robert Penner
/**
* @callback PowerEasing
* @param {Number|String} [power=1.675]
* @return {EasingFunction}
*/
/**
* @callback BackEasing
* @param {Number|String} [overshoot=1.70158]
* @return {EasingFunction}
*/
/**
* @callback ElasticEasing
* @param {Number|String} [amplitude=1]
* @param {Number|String} [period=.3]
* @return {EasingFunction}
*/
/**
* @callback EaseFactory
* @param {Number|String} [paramA]
* @param {Number|String} [paramB]
* @return {EasingFunction|Number}
*/
/** @typedef {PowerEasing|BackEasing|ElasticEasing} EasesFactory */
const halfPI = PI / 2;
const doublePI = PI * 2;
/** @type {PowerEasing} */
const easeInPower = (p = 1.68) => t => pow(t, +p);
/** @type {Record<String, EasesFactory|EasingFunction>} */
const easeInFunctions = {
[emptyString]: easeInPower,
Quad: easeInPower(2),
Cubic: easeInPower(3),
Quart: easeInPower(4),
Quint: easeInPower(5),
/** @type {EasingFunction} */
Sine: t => 1 - cos(t * halfPI),
/** @type {EasingFunction} */
Circ: t => 1 - sqrt(1 - t * t),
/** @type {EasingFunction} */
Expo: t => t ? pow(2, 10 * t - 10) : 0,
/** @type {EasingFunction} */
Bounce: t => {
let pow2, b = 4;
while (t < ((pow2 = pow(2, --b)) - 1) / 11);
return 1 / pow(4, 3 - b) - 7.5625 * pow((pow2 * 3 - 2) / 22 - t, 2);
},
/** @type {BackEasing} */
Back: (overshoot = 1.70158) => t => (+overshoot + 1) * t * t * t - +overshoot * t * t,
/** @type {ElasticEasing} */
Elastic: (amplitude = 1, period = .3) => {
const a = clamp(+amplitude, 1, 10);
const p = clamp(+period, minValue, 2);
const s = (p / doublePI) * asin(1 / a);
const e = doublePI / p;
return t => t === 0 || t === 1 ? t : -a * pow(2, -10 * (1 - t)) * sin(((1 - t) - s) * e);
}
};
/**
* @callback EaseType
* @param {EasingFunction} Ease
* @return {EasingFunction}
*/
/** @type {Record<String, EaseType>} */
const easeTypes = {
in: easeIn => t => easeIn(t),
out: easeIn => t => 1 - easeIn(1 - t),
inOut: easeIn => t => t < .5 ? easeIn(t * 2) / 2 : 1 - easeIn(t * -2 + 2) / 2,
outIn: easeIn => t => t < .5 ? (1 - easeIn(1 - t * 2)) / 2 : (easeIn(t * 2 - 1) + 1) / 2,
};
/**
* @param {String} string
* @param {Record<String, EasesFactory|EasingFunction>} easesFunctions
* @param {Object} easesLookups
* @return {EasingFunction}
*/
const parseEaseString = (string, easesFunctions, easesLookups) => {
if (easesLookups[string]) return easesLookups[string];
if (string.indexOf('(') <= -1) {
const hasParams = easeTypes[string] || string.includes('Back') || string.includes('Elastic');
const parsedFn = /** @type {EasingFunction} */(hasParams ? /** @type {EasesFactory} */(easesFunctions[string])() : easesFunctions[string]);
return parsedFn ? easesLookups[string] = parsedFn : none;
} else {
const split = string.slice(0, -1).split('(');
const parsedFn = /** @type {EasesFactory} */(easesFunctions[split[0]]);
return parsedFn ? easesLookups[string] = parsedFn(...split[1].split(',')) : none;
}
};
/**
* @typedef {Object} EasesFunctions
* @property {typeof linear} linear
* @property {typeof irregular} irregular
* @property {typeof steps} steps
* @property {typeof cubicBezier} cubicBezier
* @property {PowerEasing} in
* @property {PowerEasing} out
* @property {PowerEasing} inOut
* @property {PowerEasing} outIn
* @property {EasingFunction} inQuad
* @property {EasingFunction} outQuad
* @property {EasingFunction} inOutQuad
* @property {EasingFunction} outInQuad
* @property {EasingFunction} inCubic
* @property {EasingFunction} outCubic
* @property {EasingFunction} inOutCubic
* @property {EasingFunction} outInCubic
* @property {EasingFunction} inQuart
* @property {EasingFunction} outQuart
* @property {EasingFunction} inOutQuart
* @property {EasingFunction} outInQuart
* @property {EasingFunction} inQuint
* @property {EasingFunction} outQuint
* @property {EasingFunction} inOutQuint
* @property {EasingFunction} outInQuint
* @property {EasingFunction} inSine
* @property {EasingFunction} outSine
* @property {EasingFunction} inOutSine
* @property {EasingFunction} outInSine
* @property {EasingFunction} inCirc
* @property {EasingFunction} outCirc
* @property {EasingFunction} inOutCirc
* @property {EasingFunction} outInCirc
* @property {EasingFunction} inExpo
* @property {EasingFunction} outExpo
* @property {EasingFunction} inOutExpo
* @property {EasingFunction} outInExpo
* @property {EasingFunction} inBounce
* @property {EasingFunction} outBounce
* @property {EasingFunction} inOutBounce
* @property {EasingFunction} outInBounce
* @property {BackEasing} inBack
* @property {BackEasing} outBack
* @property {BackEasing} inOutBack
* @property {BackEasing} outInBack
* @property {ElasticEasing} inElastic
* @property {ElasticEasing} outElastic
* @property {ElasticEasing} inOutElastic
* @property {ElasticEasing} outInElastic
*/
const eases = (/*#__PURE__*/ (() => {
const list = { linear, irregular, steps, cubicBezier };
for (let type in easeTypes) {
for (let name in easeInFunctions) {
const easeIn = easeInFunctions[name];
const easeType = easeTypes[type];
list[type + name] = /** @type {EasesFactory|EasingFunction} */(
name === emptyString || name === 'Back' || name === 'Elastic' ?
(a, b) => easeType(/** @type {EasesFactory} */(easeIn)(a, b)) :
easeType(/** @type {EasingFunction} */(easeIn))
);
}
}
return /** @type {EasesFunctions} */(list);
})());
/** @type {Record<String, EasingFunction>} */
const JSEasesLookups = { linear: none };
/**
* @param {EasingParam} ease
* @return {EasingFunction}
*/
const parseEasings = ease => isFnc(ease) ? ease :
isStr(ease) ? parseEaseString(/** @type {String} */(ease), eases, JSEasesLookups) :
none;
const propertyNamesCache = {};
/**
* @param {String} propertyName
* @param {Target} target
* @param {tweenTypes} tweenType
* @return {String}
*/
const sanitizePropertyName = (propertyName, target, tweenType) => {
if (tweenType === tweenTypes.TRANSFORM) {
const t = shortTransforms.get(propertyName);
return t ? t : propertyName;
} else if (
tweenType === tweenTypes.CSS ||
// Handle special cases where properties like "strokeDashoffset" needs to be set as "stroke-dashoffset"
// but properties like "baseFrequency" should stay in lowerCamelCase
(tweenType === tweenTypes.ATTRIBUTE && (isSvg(target) && propertyName in /** @type {DOMTarget} */(target).style))
) {
const cachedPropertyName = propertyNamesCache[propertyName];
if (cachedPropertyName) {
return cachedPropertyName;
} else {
const lowerCaseName = propertyName ? toLowerCase(propertyName) : propertyName;
propertyNamesCache[propertyName] = lowerCaseName;
return lowerCaseName;
}
} else {
return propertyName;
}
};
const angleUnitsMap = { 'deg': 1, 'rad': 180 / PI, 'turn': 360 };
const convertedValuesCache = {};
/**
* @param {DOMTarget} el
* @param {TweenDecomposedValue} decomposedValue
* @param {String} unit
* @param {Boolean} [force]
* @return {TweenDecomposedValue}
*/
const convertValueUnit = (el, decomposedValue, unit, force = false) => {
const currentUnit = decomposedValue.u;
const currentNumber = decomposedValue.n;
if (decomposedValue.t === valueTypes.UNIT && currentUnit === unit) { // TODO: Check if checking against the same unit string is necessary
return decomposedValue;
}
const cachedKey = currentNumber + currentUnit + unit;
const cached = convertedValuesCache[cachedKey];
if (!isUnd(cached) && !force) {
decomposedValue.n = cached;
} else {
let convertedValue;
if (currentUnit in angleUnitsMap) {
convertedValue = currentNumber * angleUnitsMap[currentUnit] / angleUnitsMap[unit];
} else {
const baseline = 100;
const tempEl = /** @type {DOMTarget} */(el.cloneNode());
const parentNode = el.parentNode;
const parentEl = (parentNode && (parentNode !== doc)) ? parentNode : doc.body;
parentEl.appendChild(tempEl);
const elStyle = tempEl.style;
elStyle.width = baseline + currentUnit;
const currentUnitWidth = /** @type {HTMLElement} */(tempEl).offsetWidth || baseline;
elStyle.width = baseline + unit;
const newUnitWidth = /** @type {HTMLElement} */(tempEl).offsetWidth || baseline;
const factor = currentUnitWidth / newUnitWidth;
parentEl.removeChild(tempEl);
convertedValue = factor * currentNumber;
}
decomposedValue.n = convertedValue;
convertedValuesCache[cachedKey] = convertedValue;
}
decomposedValue.t === valueTypes.UNIT;
decomposedValue.u = unit;
return decomposedValue;
};
/**
* @template {Renderable} T
* @param {T} renderable
* @return {T}
*/
const cleanInlineStyles = renderable => {
// Allow cleanInlineStyles() to be called on timelines
if (renderable._hasChildren) {
forEachChildren(renderable, cleanInlineStyles, 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 = animation._inlineStyles[tweenProperty];
if (tween._tweenType === tweenTypes.TRANSFORM) {
const cachedTransforms = tweenTarget[transformsSymbol];
if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) {
delete cachedTransforms[tweenProperty];
} else {
cachedTransforms[tweenProperty] = originalInlinedValue;
}
if (tween._renderTransforms) {
if (!Object.keys(cachedTransforms).length) {
targetStyle.removeProperty('transform');
} else {
let str = emptyString;
for (let key in cachedTransforms) {
str += transformsFragmentStrings[key] + cachedTransforms[key] + ') ';
}
targetStyle.transform = str;
}
}
} else {
if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) {
targetStyle.removeProperty(tweenProperty);
} else {
targetStyle[tweenProperty] = originalInlinedValue;
}
}
if (animation._tail === tween) {
animation.targets.forEach(t => {
if (t.getAttribute && t.getAttribute('style') === emptyString) {
t.removeAttribute('style');
} });
}
}
});
}
return renderable;
};
// Defines decomposed values target objects only once and mutate their properties later to avoid GC
// TODO: Maybe move the objects creation to values.js and use the decompose function to create the base object
const fromTargetObject = createDecomposedValueTargetObject();
const toTargetObject = createDecomposedValueTargetObject();
const toFunctionStore = { func: null };
const keyframesTargetArray = [null];
const fastSetValuesArray = [null, null];
/** @type {TweenKeyValue} */
const keyObjectTarget = { to: null };
let tweenId = 0;
let keyframes;
/** @type {TweenParamsOptions & TweenValues} */
let key;
/**
* @param {DurationKeyframes | PercentageKeyframes} keyframes
* @param {AnimationParams} parameters
* @return {AnimationParams}
*/
const generateKeyframes = (keyframes, parameters) => {
/** @type {AnimationParams} */
const properties = {};
if (isArr(keyframes)) {
const propertyNames = [].concat(.../** @type {DurationKeyframes} */(keyframes).map(key => Object.keys(key))).filter(isKey);
for (let i = 0, l = propertyNames.length; i < l; i++) {
const propName = propertyNames[i];
const propArray = /** @type {DurationKeyframes} */(keyframes).map(key => {
/** @type {TweenKeyValue} */
const newKey = {};
for (let p in key) {
const keyValue = /** @type {TweenPropValue} */(key[p]);
if (isKey(p)) {
if (p === propName) {
newKey.to = keyValue;
}
} else {
newKey[p] = keyValue;
}
}
return newKey;
});
properties[propName] = /** @type {ArraySyntaxValue} */(propArray);
}
} else {
const totalDuration = /** @type {Number} */(setValue(parameters.duration, globals.defaults.duration));
const keys = Object.keys(keyframes)
.map(key => { return {o: parseFloat(key) / 100, p: keyframes[key]} })
.sort((a, b) => a.o - b.o);
keys.forEach(key => {
const offset = key.o;
const prop = key.p;
for (let name in prop) {
if (isKey(name)) {
let propArray = /** @type {Array} */(properties[name]);
if (!propArray) propArray = properties[name] = [];
const duration = offset * totalDuration;
let length = propArray.length;
let prevKey = propArray[length - 1];
const keyObj = { to: prop[name] };
let durProgress = 0;
for (let i = 0; i < length; i++) {
durProgress += propArray[i].duration;
}
if (length === 1) {
keyObj.from = prevKey.to;
}
if (prop.ease) {
keyObj.ease = prop.ease;
}
keyObj.duration = duration - (length ? durProgress : 0);
propArray.push(keyObj);
}
}
return key;
});
for (let name in properties) {
const propArray = /** @type {Array} */(properties[name]);
let prevEase;
// let durProgress = 0
for (let i = 0, l = propArray.length; i < l; i++) {
const prop = propArray[i];
// Emulate WAPPI easing parameter position
const currentEase = prop.ease;
prop.ease = prevEase ? prevEase : undefined;
prevEase = currentEase;
// durProgress += prop.duration;
// if (i === l - 1 && durProgress !== totalDuration) {
// propArray.push({ from: prop.to, ease: prop.ease, duration: totalDuration - durProgress })
// }
}
if (!propArray[0].duration) {
propArray.shift();
}
}
}
return properties;
};
class JSAnimation extends Timer {
/**
* @param {TargetsParam} targets
* @param {AnimationParams} parameters
* @param {Timeline} [parent]
* @param {Number} [parentPosition]
* @param {Boolean} [fastSet=false]
* @param {Number} [index=0]
* @param {Number} [length=0]
*/
constructor(
targets,
parameters,
parent,
parentPosition,
fastSet = false,
index = 0,
length = 0
) {
super(/** @type {TimerParams&AnimationParams} */(parameters), parent, parentPosition);
const parsedTargets = registerTargets(targets);
const targetsLength = parsedTargets.length;
// If the parameters object contains a "keyframes" property, convert all the keyframes values to regular properties
const kfParams = /** @type {AnimationParams} */(parameters).keyframes;
const params = /** @type {AnimationParams} */(kfParams ? mergeObjects(generateKeyframes(/** @type {DurationKeyframes} */(kfParams), parameters), parameters) : parameters);
const {
delay,
duration,
ease,
playbackEase,
modifier,
composition,
onRender,
} = params;
const animDefaults = parent ? parent.defaults : globals.defaults;
const animaPlaybackEase = setValue(playbackEase, animDefaults.playbackEase);
const animEase = animaPlaybackEase ? parseEasings(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).duration : 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
const tComposition = isUnd(composition) && targetsLength >= K ? compositionTypes.none : !isUnd(composition) ? composition : animDefaults.composition;
// TODO: Do not create an empty object until we know the animation will generate inline styles
const animInlineStyles = {};
// const absoluteOffsetTime = this._offset;
const absoluteOffsetTime = this._offset + (parent ? parent._offset : 0);
let iterationDuration = NaN;
let iterationDelay = NaN;
let animationAnimationLength = 0;
let shouldTriggerRender = 0;
for (let targetIndex = 0; targetIndex < targetsLength; targetIndex++) {
const target = parsedTargets[targetIndex];
const ti = index || targetIndex;
const tl = length || targetsLength;
let lastTransformGroupIndex = NaN;
let lastTransformGroupLength = NaN;
for (let p in params) {
if (isKey(p)) {
const tweenType = getTweenType(target, p);
const propName = sanitizePropertyName(p, target, tweenType);
let propValue = params[p];
const isPropValueArray = isArr(propValue);
if (fastSet && !isPropValueArray) {
fastSetValuesArray[0] = propValue;
fastSetValuesArray[1] = propValue;
propValue = fastSetValuesArray;
}
// TODO: Allow nested keyframes inside ObjectValue value (prop: { to: [.5, 1, .75, 2, 3] })
// Normalize property values to valid keyframe syntax:
// [x, y] to [{to: [x, y]}] or {to: x} to [{to: x}] or keep keys syntax [{}, {}, {}...]
// const keyframes = isArr(propValue) ? propValue.length === 2 && !isObj(propValue[0]) ? [{ to: propValue }] : propValue : [propValue];
if (isPropValueArray) {
const arrayLength = /** @type {Array} */(propValue).length;
const isNotObjectValue = !isObj(propValue[0]);
// Convert [x, y] to [{to: [x, y]}]
if (arrayLength === 2 && isNotObjectValue) {
keyObjectTarget.to = /** @type {TweenParamValue} */(/** @type {unknown} */(propValue));
keyframesTargetArray[0] = keyObjectTarget;
keyframes = keyframesTargetArray;
// Convert [x, y, z] to [[x, y], z]
} else if (arrayLength > 2 && isNotObjectValue) {
keyframes = [];
/** @type {Array.<Number>} */(propValue).forEach((v, i) => {
if (!i) {
fastSetValuesArray[0] = v;
} else if (i === 1) {
fastSetValuesArray[1] = v;
keyframes.push(fastSetValuesArray);
} else {
keyframes.push(v);
}
});
} else {
keyframes = /** @type {Array.<TweenKeyValue>} */(propValue);
}
} else {
keyframesTargetArray[0] = propValue;
keyframes = keyframesTargetArray;
}
let siblings = null;
let prevTween = null;
let firstTweenChangeStartTime = NaN;
let lastTweenChangeEndTime = 0;
let tweenIndex = 0;
for (let l = keyframes.length; tweenIndex < l; tweenIndex++) {
const keyframe = keyframes[tweenIndex];
if (isObj(keyframe)) {
key = keyframe;
} else {
keyObjectTarget.to = /** @type {TweenParamValue} */(keyframe);
key = keyObjectTarget;
}
toFunctionStore.func = null;
const computedToValue = getFunctionValue(key.to, target, ti, tl, toFunctionStore);
let tweenToValue;
// Allows function based values to return an object syntax value ({to: v})
if (isObj(computedToValue) && !isUnd(computedToValue.to)) {
key = computedToValue;
tweenToValue = computedToValue.to;
} else {
tweenToValue = computedToValue;
}
const tweenFromValue = getFunctionValue(key.from, target, ti, tl);
const keyEasing = key.ease;
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;
// Calculate default individual keyframe duration by dividing the tl of keyframes
const tweenDuration = hasSpring ? /** @type {Spring} */(keyEasing).duration : getFunctionValue(setValue(key.duration, (l > 1 ? getFunctionValue(tDuration, target, ti, tl) / l : tDuration)), target, ti, tl);
// 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];
// 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);
const hasToValue = !isUnd(tweenToValue);
const isFromToArray = isArr(tweenToValue);
const isFromToValue = isFromToArray || (hasFromvalue && hasToValue);
const tweenStartTime = prevTween ? lastTweenChangeEndTime + tweenDelay : tweenDelay;
const absoluteStartTime = absoluteOffsetTime + tweenStartTime;
// Force a onRender callback if the animation contains at least one from value and autoplay is set to false
if (!shouldTriggerRender && (hasFromvalue || isFromToArray)) shouldTriggerRender = 1;
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) {
prevSibling = nextSibling;
nextSibling = nextSibling._nextRep;
// Overrides all the next siblings if the next sibling starts at the same time of after as the new tween start time
if (nextSibling && nextSibling._absoluteStartTime >= absoluteStartTime) {
while (nextSibling) {
overrideTween(nextSibling);
// This will ends both the current while loop and the upper one once all the next sibllings have been overriden
nextSibling = nextSibling._nextRep;
}
}
}
}
// 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);
if (fromTargetObject.t === valueTypes.NUMBER) {
if (prevSibling) {
if (prevSibling._valueType === valueTypes.UNIT) {
fromTargetObject.t = valueTypes.UNIT;
fromTargetObject.u = prevSibling._unit;
}
} else {
decomposeRawValue(
getOriginalAnimatableValue(target, propName, tweenType, animInlineStyles),
decomposedOriginalValue
);
if (decomposedOriginalValue.t === valueTypes.UNIT) {
fromTargetObject.t = valueTypes.UNIT;
fromTargetObject.u = decomposedOriginalValue.u;
}
}
}
} else {
if (hasToValue) {
decomposeRawValue(tweenToValue, toTargetObject);
} else {
if (prevTween) {
decomposeTweenValue(prevTween, toTargetObject);
} else {
// No need to get and parse the original value if the tween is part of a timeline and has a previous sibling part of the same timeline
decomposeRawValue(parent && prevSibling && prevSibling.parent.parent === parent ? prevSibling._value :
getOriginalAnimatableValue(target, propName, tweenType, animInlineStyles), toTargetObject);
}
}
if (hasFromvalue) {
decomposeRawValue(tweenFromValue, fromTargetObject);
} else {
if (prevTween) {
decomposeTweenValue(prevTween, fromTargetObject);
} else {
decomposeRawValue(parent && prevSibling && prevSibling.parent.parent === parent ? prevSibling._value :
// No need to get and parse the original value if the tween is part of a timeline and has a previous sibling part of the same timeline
getOriginalAnimatableValue(target, propName, tweenType, animInlineStyles), fromTargetObject);
}
}
}
// Apply operators
if (fromTargetObject.o) {
fromTargetObject.n = getRelativeValue(
!prevSibling ? decomposeRawValue(
getOriginalAnimatableValue(target, propName, tweenType, animInlineStyles),
decomposedOriginalValue
).n : prevSibling._toNumber,
fromTargetObject.n,
fromTargetObject.o
);
}
if (toTargetObject.o) {
toTargetObject.n = getRelativeValue(fromTargetObject.n, toTargetObject.n, toTargetObject.o);
}
// Values omogenisation in cases of type difference between "from" and "to"
if (fromTargetObject.t !== toTargetObject.t) {
if (fromTargetObject.t === valueTypes.COMPLEX || toTargetObject.t === valueTypes.COMPLEX) {
const complexValue = fromTargetObject.t === valueTypes.COMPLEX ? fromTargetObject : toTargetObject;
const notComplexValue = fromTargetObject.t === valueTypes.COMPLEX ? toTargetObject : fromTargetObject;
notComplexValue.t = valueTypes.COMPLEX;
notComplexValue.s = cloneArray(complexValue.s);
notComplexValue.d = complexValue.d.map(() => notComplexValue.n);
} else if (fromTargetObject.t === valueTypes.UNIT || toTargetObject.t === valueTypes.UNIT) {
const unitValue = fromTargetObject.t === valueTypes.UNIT ? fromTargetObject : toTargetObject;
const notUnitValue = fromTargetObject.t === valueTypes.UNIT ? toTargetObject : fromTargetObject;
notUnitValue.t = valueTypes.UNIT;
notUnitValue.u = unitValue.u;
} else if (fromTargetObject.t === valueTypes.COLOR || toTargetObject.t === valueTypes.COLOR) {
const colorValue = fromTargetObject.t === valueTypes.COLOR ? fromTargetObject : toTargetObject;
const notColorValue = fromTargetObject.t === valueTypes.COLOR ? toTargetObject : fromTargetObject;
notColorValue.t = valueTypes.COLOR;
notColorValue.s = colorValue.s;
notColorValue.d = [0, 0, 0, 1];
}
}
// Unit conversion
if (fromTargetObject.u !== toTargetObject.u) {
let valueToConvert = toTargetObject.u ? fromTargetObject : toTargetObject;
valueToConvert = convertValueUnit(/** @type {DOMTarget} */(target), valueToConvert, toTargetObject.u ? toTargetObject.u : fromTargetObject.u, false);
// TODO:
// convertValueUnit(target, to.u ? from : to, to.u ? to.u : from.u);
}
// Fill in non existing complex values
if (toTargetObject.d && fromTargetObject.d && (toTargetObject.d.length !== fromTargetObject.d.length)) {
const longestValue = fromTargetObject.d.length > toTargetObject.d.length ? fromTargetObject : toTargetObject;
const shortestValue = longestValue === fromTargetObject ? toTargetObject : fromTargetObject;
// TODO: Check if n should be used instead of 0 for default complex values
shortestValue.d = longestValue.d.map((_, i) => isUnd(shortestValue.d[i]) ? 0 : shortestValue.d[i]);
shortestValue.s = cloneArray(longestValue.s);
}
// Tween factory
// Rounding is necessary here to minimize floating point errors
const tweenUpdateDuration = round(+tweenDuration || minValue, 12);
/** @type {Tween} */
const tween = {
parent: this,
id: tweenId++,
property: propName,
target: target,
_value: null,
_func: toFunctionStore.func,
_ease: parseEasings(tweenEasing),
_fromNumbers: cloneArray(fromTargetObject.d),
_toNumbers: cloneArray(toTargetObject.d),
_strings: cloneArray(toTargetObject.s),
_fromNumber: fromTargetObject.n,
_toNumber: toTargetObject.n,
_numbers: cloneArray(fromTargetObject.d), // For additive tween and animatables
_number: fromTargetObject.n, // For additive tween and animatables
_unit: toTargetObject.u,
_modifier: tweenModifier,
_currentTime: 0,
_startTime: tweenStartTime,
_delay: +tweenDelay,
_updateDuration: tweenUpdateDuration,
_changeDuration: tweenUpdateDuration,
_absoluteStartTime: absoluteStartTime,
// NOTE: Investigate bit packing to stores ENUM / BOOL
_tweenType: tweenType,
_valueType: toTargetObject.t,
_composition: tweenComposition,
_isOverlapped: 0,
_isOverridden: 0,
_renderTransforms: 0,
_prevRep: null, // For replaced tween
_nextRep: null, // For replaced tween
_prevAdd: null, // For additive tween
_nextAdd: null, // For additive tween
_prev: null,
_next: null,
};
if (tweenComposition !== compositionTypes.none) {
composeTween(tween, siblings);
}
if (isNaN(firstTweenChangeStartTime)) {
firstTweenChangeStartTime = tween._startTime;
}
// Rounding is necessary here to minimize floating point errors
lastTweenChangeEndTime = round(tweenStartTime + tweenUpdateDuration, 12);
prevTween = tween;
animationAnimationLength++;
addChild(this, tween);
}
// Update animation timings with the added tweens properties
if (isNaN(iterationDelay) || firstTweenChangeStartTime < iterationDelay) {
iterationDelay = firstTweenChangeStartTime;
}
if (isNaN(iterationDuration) || lastTweenChangeEndTime > iterationDuration) {
iterationDuration = lastTweenChangeEndTime;
}
// TODO: Find a way to inline tween._renderTransforms = 1 here
if (tweenType === tweenTypes.TRANSFORM) {
lastTransformGroupIndex = animationAnimationLength - tweenIndex;
lastTransformGroupLength = animationAnimationLength;
}
}
}
// Set _renderTransforms to last transform property to correctly render the transforms list
if (!isNaN(lastTransformGroupIndex)) {
let i = 0;
forEachChildren(this, (/** @type {Tween} */tween) => {
if (i >= lastTransformGroupIndex && i < lastTransformGroupLength) {
tween._renderTransforms = 1;
if (tween._composition === compositionTypes.blend) {
forEachChildren(additive.animation, (/** @type {Tween} */additiveTween) => {
if (additiveTween.id === tween.id) {
additiveTween._renderTransforms = 1;
}
});
}
}
i++;
});
}
}
if (!targetsLength) {
console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`);
}
if (iterationDelay) {
forEachChildren(this, (/** @type {Tween} */tween) => {
// If (startTime - delay) equals 0, this means the tween is at the begining of the animation so we need to trim the delay too
if (!(tween._startTime - tween._delay)) {
tween._delay -= iterationDelay;
}
tween._startTime -= iterationDelay;
});
iterationDuration -= iterationDelay;
} else {
iterationDelay = 0;
}
// Prevents iterationDuration to be NaN if no valid animatable props have been provided
// Prevents _iterationCount to be NaN if no valid animatable props have been provided
if (!iterationDuration) {
iterationDuration = minValue;
this.iterationCount = 0;
}
/** @type {TargetsArray} */
this.targets = parsedTargets;
/** @type {Number} */
this.duration = iterationDuration === minValue ? minValue : clampInfinity(((iterationDuration + this._loopDelay) * this.iterationCount) - this._loopDelay) || minValue;
/** @type {Callback<this>} */
this.onRender = onRender || animDefaults.onRender;
/** @type {EasingFunction} */
this._ease = animEase;
/** @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.
// this._delay = parent ? 0 : iterationDelay;
// this._offset += parent ? iterationDelay : 0;
/** @type {Number} */
this.iterationDuration = iterationDuration;
/** @type {{}} */
this._inlineStyles = animInlineStyles;
if (!this._autoplay && shouldTriggerRender) this.onRender(this);
}
/**
* @param {Number} newDuration
* @return {this}
*/
stretch(newDuration) {
const currentDuration = this.duration;
if (currentDuration === normalizeTime(newDuration)) return this;
const timeScale = newDuration / currentDuration;
// NOTE: Find a better way to handle the stretch of an animation after stretch = 0
forEachChildren(this, (/** @type {Tween} */tween) => {
// Rounding is necessary here to minimize floating point errors
tween._updateDuration = normalizeTime(tween._updateDuration * timeScale);
tween._changeDuration = normalizeTime(tween._changeDuration * timeScale);
tween._currentTime *= timeScale;
tween._startTime *= timeScale;
tween._absoluteStartTime *= timeScale;
});
return super.stretch(newDuration);
}
/**
* @return {this}
*/
refresh() {
forEachChildren(this, (/** @type {Tween} */tween) => {
const tweenFunc = tween._func;
if (tweenFunc) {
const ogValue = getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType);
decomposeRawValue(ogValue, decomposedOriginalValue);
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;
}
});
return this;
}
/**
* Cancel the animation and revert all the values affected by this animation to their original state
* @return {this}
*/
revert() {
super.revert();
return cleanInlineStyles(this);
}
/**
* @param {Callback<this>} [callback]
* @return {Promise}
*/
then(callback) {
return super.then(callback);
}
}
/**
* @param {TargetsParam} targets
* @param {AnimationParams} parameters
* @return {JSAnimation}
*/
const animate = (targets, parameters) => new JSAnimation(targets, parameters, null, 0, false).init();
/**
* 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(fn(i / samples));
return `linear(${points.join(', ')})`;
};
const WAAPIEasesLookups = {
in: 'ease-in',
out: 'ease-out',
inOut: 'ease-in-out',
};
const WAAPIeases = /*#__PURE__*/(() => {
const list = {};
for (let type in easeTypes) list[type] = a => easeTypes[type](easeInPower(a));
return /** @type {Record<String, EasingFunction>} */(list);
})();
/**
* @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, WAAPIeases, WAAPIEasesLookups);
if (isFnc(parsed)) parsedEase = parsed === none ? 'linear' : easingToLinear(parsed);
}
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 {String|Number|Array<String>|Array<Number>} WAAPITweenValue
*/
/**
* @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}
*/
/**
* @typedef {WAAPITweenValue|WAAPIFunctionvalue|Array<String|Number|WAAPIFunctionvalue>} WAAPIKeyframeValue
*/
/**
* @typedef {(animation: WAAPIAnimation) => void} WAAPICallback
*/
/**
* @typedef {Object} WAAPITweenOptions
* @property {WAAPIKeyframeValue} [to]
* @property {WAAPIKeyframeValue} [from]
* @property {Number|WAAPIFunctionvalue} [duration]
* @property {Number|WAAPIFunctionvalue} [delay]
* @property {EasingParam} [ease]
* @property {CompositeOperation} [composition]
*/
/**
* @typedef {Object} WAAPIAnimationOptions
* @property {Number|Boolean} [loop]
* @property {Boolean} [Reversed]
* @property {Boolean} [Alternate]
* @property {Boolean|ScrollObserver} [autoplay]
* @property {Number} [playbackRate]
* @property {Number|WAAPIFunctionvalue} [duration]
* @property {Number|WAAPIFunctionvalue} [delay]
* @property {EasingParam} [ease]
* @property {CompositeOperation} [composition]
* @property {WAAPICallback} [onComplete]
*/
/**
* @typedef {Record<String, WAAPIKeyframeValue | WAAPIAnimationOptions | Boolean | ScrollObserver | WAAPICallback | EasingParam | WAAPITweenOptions> & WAAPIAnimationOptions} WAAPIAnimationParams
*/
const transformsShorthands = ['x', 'y', 'z'];
const commonDefaultPXProperties = [
'perspective',
'width',
'height',
'margin',
'padding',
'top',
'right',
'bottom',
'left',
'borderWidth',
'fontSize',
'borderRadius',
...transformsShorthands
];
const validIndividualTransforms = /*#__PURE__*/ (() => [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))])();
let transformsPropertiesRegistered = null;
const WAAPIAnimationsLookups = {
_head: null,
_tail: null,
};
/**
* @param {DOMTarget} $el
* @param {String} [property]
* @param {WAAPIAnimation} [parent]
*/
const removeWAAPIAnimation = ($el, property, parent) => {
let nextLookup = WAAPIAnimationsLookups._head;
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) {
const 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;
if (!lookupParent.muteCallbacks) {
lookupParent.paused = true;
lookupParent.onComplete(lookupParent);
lookupParent._resolve(lookupParent);
}
}
}
}
nextLookup = next;
}
};
/**
* @param {WAAPIAnimation} parent
* @param {DOMTarget} $el
* @param {String} property
* @param {PropertyIndexedKeyframes} keyframes
* @param {KeyframeAnimationOptions} params
* @retun {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.onremove = handleRemove;
animation.onfinish = handleRemove;
return animation;
};
/**
* @param {String} propName
* @param {WAAPIKeyframeValue} value
* @param {DOMTarget} $el
* @param {Number} i
* @param {Number} targetsLength
* @return {String}
*/
const normalizeTweenValue = (propName, value, $el, i, targetsLength) => {
let v = 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;
};
class WAAPIAnimation {
/**
* @param {DOMTargetsParam} targets
* @param {WAAPIAnimationParams} params
*/
constructor(targets, params) {
if (scope.current) scope.current.register(this);
// 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 ? '<angle>' : isScale ? "<number>" : isTranslate ? "<length-percentage>" : "*";
try {
CSS.registerProperty({
name: '--' + t,
syntax,
inherits: false,
initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0',
});
} catch {} });
transformsPropertiesRegistered = true;
}
}
const parsedTargets = registerTargets(targets);
const targetsLength = parsedTargets.length;
if (!targetsLength) {
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;
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 = 'forwards';
/** @type {String} */
const easing = parseWAAPIEasing(ease);
const timeScale = (globals.timeScale === 1 ? 1 : K);
/** @type {DOMTargetsArray}] */
this.targets = parsedTargets;
/** @type {Array<globalThis.Animation>}] */
this.animations = [];
/** @type {globalThis.Animation}] */
this.controlAnimation = null;
/** @type {Callback<this>} */
this.onComplete = params.onComplete || noop;
/** @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|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<Object>}] */
this._inlineStyles = parsedTargets.map($el => $el.getAttribute('style'));
parsedTargets.forEach(($el, i) => {
const cachedTransforms = $el[transformsSymbol];
const hasIndividualTransforms = validIndividualTransforms.some(t => params.hasOwnProperty(t));
/** @type {Number} */
const duration = (spring ? /** @type {Spring} */(spring).duration : 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'));
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;
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).duration : 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);
}
addWAAPIAnimation(this, $el, name, keyframes, tweenParams);
if (!isUnd(from)) {
if (!individualTransformProperty) {
$el.style[name] = keyframes[name][0];
} else {
const key = `--${individualTransformProperty}`;
$el.style.setProperty(key, keyframes[key][0]);
}
}
} 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;
} 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})) `;
}
$el.style.transform = transforms;
}
});
if (scroll) {
/** @type {ScrollObserver} */(this.autoplay).link(this);
}
}
/**
* @callback forEachCallback
* @param {globalThis.Animation} animation
*/
/**
* @param {forEachCallback|String} callback
* @return {this}
*/
forEach(callback) {
const cb = isStr(callback) ? a => a[callback]() : callback;
this.animations.forEach(cb);
return this;
}
get speed() {
return this._speed;
}
/** @param {Number} 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;
}
/** @param {Number} time */
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
if (t >= this.duration) anim.play();
anim.currentTime = t;
});
}
get progress() {
return this.currentTime / this.duration;
}
/** @param {Number} progress */
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.forEach('cancel');
return this.pause();
}
revert() {
this.cancel();
this.targets.forEach(($el, i) => $el.setAttribute('style', this._inlineStyles[i]) );
return this;
}
/**
* @param {WAAPICallback} [callback]
* @return {Promise}
*/
then(callback = noop) {
const then = this.then;
const onResolve = () => {
this.then = null;
callback(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}
*/
animate: (targets, params) => new WAAPIAnimation(targets, params),
convertEase: easingToLinear
};
/**
* @param {Callback<Timer>} [callback]
* @return {Timer}
*/
const sync = (callback = noop) => {
return new Timer({ duration: 1 * globals.timeScale, onComplete: callback }, null, 0).resume();
};
/**
* @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 getTargetValue(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(convertedValue.n, globals.precision)}${convertedValue.u}`;
}
}
}
}
/**
* @param {TargetsParam} targets
* @param {AnimationParams} parameters
* @return {JSAnimation}
*/
const setTargetValues = (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 {TargetsArray} targetsArray
* @param {JSAnimation} animation
* @param {String} [propertyName]
* @return {Boolean}
*/
const removeTargetsFromAnimation = (targetsArray, animation, propertyName) => {
let tweensMatchesTargets = false;
forEachChildren(animation, (/**@type {Tween} */tween) => {
const tweenTarget = tween.target;
if (targetsArray.includes(tweenTarget)) {
const tweenName = tween.property;
const tweenType = tween._tweenType;
const normalizePropName = sanitizePropertyName(propertyName, tweenTarget, tweenType);
if (!normalizePropName || normalizePropName && normalizePropName === tweenName) {
// Make sure to flag the previous CSS transform tween to renderTransform
if (tween.parent._tail === tween &&
tween._tweenType === tweenTypes.TRANSFORM &&
tween._prev &&
tween._prev._tweenType === tweenTypes.TRANSFORM
) {
tween._prev._renderTransforms = 1;
}
// Removes the tween from the selected animation
removeChild(animation, tween);
// Detach the tween from its siblings to make sure blended tweens are correctlly removed
removeTweenSliblings(tween);
tweensMatchesTargets = true;
}
}
}, true);
return tweensMatchesTargets;
};
/**
* @param {TargetsParam} targets
* @param {Renderable|WAAPIAnimation} [renderable]
* @param {String} [propertyName]
* @return {TargetsArray}
*/
const remove = (targets, renderable, propertyName) => {
const targetsArray = parseTargets(targets);
const parent = /** @type {Renderable|typeof engine} **/(renderable ? renderable : engine);
const waapiAnimation = renderable && /** @type {WAAPIAnimation} */(renderable).controlAnimation && /** @type {WAAPIAnimation} */(renderable);
for (let i = 0, l = targetsArray.length; i < l; i++) {
const $el = /** @type {DOMTarget} */(targetsArray[i]);
removeWAAPIAnimation($el, propertyName, waapiAnimation);
}
let removeMatches;
if (parent._hasChildren) {
let iterationDuration = 0;
forEachChildren(parent, (/** @type {Renderable} */child) => {
if (!child._hasChildren) {
removeMatches = removeTargetsFromAnimation(targetsArray, /** @type {JSAnimation} */(child), propertyName);
// Remove the child from its parent if no tweens and no children left after the removal
if (removeMatches && !child._head) {
child.cancel();
removeChild(parent, child);
} else {
// Calculate the new iterationDuration value to handle onComplete with last child in render()
const childTLOffset = child._offset + child._delay;
const childDur = childTLOffset + child.duration;
if (childDur > iterationDuration) {
iterationDuration = childDur;
}
}
}
// Make sure to also remove engine's children targets
// NOTE: Avoid recursion?
if (child._head) {
remove(targets, child, propertyName);
} else {
child._hasChildren = false;
}
}, true);
// Update iterationDuration value to handle onComplete with last child in render()
if (!isUnd(/** @type {Renderable} */(parent).iterationDuration)) {
/** @type {Renderable} */(parent).iterationDuration = iterationDuration;
}
} else {
removeMatches = removeTargetsFromAnimation(
targetsArray,
/** @type {JSAnimation} */(parent),
propertyName
);
}
if (removeMatches && !parent._head) {
parent._hasChildren = false;
// Cancel the parent if there are no tweens and no children left after the removal
// We have to check if the .cancel() method exist to handle cases where the parent is the engine itself
if (/** @type {Renderable} */(parent).cancel) /** @type {Renderable} */(parent).cancel();
}
return targetsArray;
};
/**
* @param {(...args: any[]) => Tickable} constructor
* @return {(...args: any[]) => Tickable}
*/
const keepTime = createRefreshable;
/**
* @param {String|Array} items
* @return {any}
*/
const randomPick = items => items[random(0, items.length - 1)];
/**
* @param {Number|String} v
* @param {Number} decimalLength
* @return {String}
*/
const roundPad = (v, decimalLength) => (+v).toFixed(decimalLength);
/**
* @param {Number} v
* @param {Number} totalLength
* @param {String} padString
* @return {String}
*/
const padStart = (v, totalLength, padString) => `${v}`.padStart(totalLength, padString);
/**
* @param {Number} v
* @param {Number} totalLength
* @param {String} padString
* @return {String}
*/
const padEnd = (v, totalLength, padString) => `${v}`.padEnd(totalLength, padString);
/**
* @param {Number} v
* @param {Number} min
* @param {Number} max
* @return {Number}
*/
const wrap = (v, min, max) => (((v - min) % (max - min) + (max - min)) % (max - min)) + min;
/**
* @param {Number} value
* @param {Number} inLow
* @param {Number} inHigh
* @param {Number} outLow
* @param {Number} outHigh
* @return {Number}
*/
const mapRange = (value, inLow, inHigh, outLow, outHigh) => outLow + ((value - inLow) / (inHigh - inLow)) * (outHigh - outLow);
/**
* @param {Number} degrees
* @return {Number}
*/
const degToRad = degrees => degrees * PI / 180;
/**
* @param {Number} radians
* @return {Number}
*/
const radToDeg = radians => radians * 180 / PI;
/**
* https://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/
* @param {Number} start
* @param {Number} end
* @param {Number} amount
* @param {Renderable|Boolean} [renderable]
* @return {Number}
*/
const lerp = (start, end, amount, renderable) => {
let dt = K / globals.defaults.frameRate;
if (renderable !== false) {
const ticker = /** @type Renderable */
(renderable) ||
(engine._hasChildren && engine);
if (ticker && ticker.deltaTime) {
dt = ticker.deltaTime;
}
}
const t = 1 - Math.exp(-amount * dt * .1);
return !amount ? start : amount === 1 ? end : (1 - t) * start + t * end;
};
// Chain-able utilities
/**
* @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) => chain(/**@param {...Number|String} nextArgs */(...nextArgs) => {
const nextResult = utils[prop](...nextArgs);
return (/**@type {Number|String} */v) => nextResult(result(v));
})
});
}
};
/**
* @param {UtilityFunction} fn
* @param {Number} [right]
* @return {function(...(Number|String)): UtilityFunction}
*/
const makeChainable = (fn, right = 0) => (...args) => (args.length < fn.length ? chain(curry(fn, right)) : fn)(...args);
/**
* @callback ChainedUtilsResult
* @param {Number} value
* @return {Number}
*
* @typedef {Object} ChainableUtils
* @property {ChainedClamp} clamp
* @property {ChainedRound} round
* @property {ChainedSnap} snap
* @property {ChainedWrap} wrap
* @property {ChainedInterpolate} interpolate
* @property {ChainedMapRange} mapRange
* @property {ChainedRoundPad} roundPad
* @property {ChainedPadStart} padStart
* @property {ChainedPadEnd} padEnd
* @property {ChainedDegToRad} degToRad
* @property {ChainedRadToDeg} radToDeg
*
* @typedef {ChainableUtils & ChainedUtilsResult} ChainableUtil
*
* @callback ChainedClamp
* @param {Number} min
* @param {Number} max
* @return {ChainableUtil}
*
* @callback ChainedRound
* @param {Number} decimalLength
* @return {ChainableUtil}
*
* @callback ChainedSnap
* @param {Number} increment
* @return {ChainableUtil}
*
* @callback ChainedWrap
* @param {Number} min
* @param {Number} max
* @return {ChainableUtil}
*
* @callback ChainedInterpolate
* @param {Number} start
* @param {Number} end
* @return {ChainableUtil}
*
* @callback ChainedMapRange
* @param {Number} inLow
* @param {Number} inHigh
* @param {Number} outLow
* @param {Number} outHigh
* @return {ChainableUtil}
*
* @callback ChainedRoundPad
* @param {Number} decimalLength
* @return {ChainableUtil}
*
* @callback ChainedPadStart
* @param {Number} totalLength
* @param {String} padString
* @return {ChainableUtil}
*
* @callback ChainedPadEnd
* @param {Number} totalLength
* @param {String} padString
* @return {ChainableUtil}
*
* @callback ChainedDegToRad
* @return {ChainableUtil}
*
* @callback ChainedRadToDeg
* @return {ChainableUtil}
*/
const utils = {
$: registerTargets,
get: getTargetValue,
set: setTargetValues,
remove,
cleanInlineStyles,
random,
randomPick,
shuffle,
lerp,
sync,
keepTime,
clamp: /** @type {typeof clamp & ChainedClamp} */(makeChainable(clamp)),
round: /** @type {typeof round & ChainedRound} */(makeChainable(round)),
snap: /** @type {typeof snap & ChainedSnap} */(makeChainable(snap)),
wrap: /** @type {typeof wrap & ChainedWrap} */(makeChainable(wrap)),
interpolate: /** @type {typeof interpolate & ChainedInterpolate} */(makeChainable(interpolate, 1)),
mapRange: /** @type {typeof mapRange & ChainedMapRange} */(makeChainable(mapRange)),
roundPad: /** @type {typeof roundPad & ChainedRoundPad} */(makeChainable(roundPad)),
padStart: /** @type {typeof padStart & ChainedPadStart} */(makeChainable(padStart)),
padEnd: /** @type {typeof padEnd & ChainedPadEnd} */(makeChainable(padEnd)),
degToRad: /** @type {typeof degToRad & ChainedDegToRad} */(makeChainable(degToRad)),
radToDeg: /** @type {typeof radToDeg & ChainedRadToDeg} */(makeChainable(radToDeg)),
};
/**
* @typedef {Number|String|Function} TimePosition
*/
/**
* 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 {TimePosition} [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;
}
/**
* @overload
* @param {TimerParams} childParams
* @param {Timeline} tl
* @param {Number} timePosition
* @return {Timeline}
*
* @overload
* @param {AnimationParams} childParams
* @param {Timeline} tl
* @param {Number} timePosition
* @param {TargetsParam} targets
* @param {Number} [index]
* @param {Number} [length]
* @return {Timeline}
*
* @param {TimerParams|AnimationParams} childParams
* @param {Timeline} tl
* @param {Number} timePosition
* @param {TargetsParam} [targets]
* @param {Number} [index]
* @param {Number} [length]
*/
function addTlChild(childParams, tl, timePosition, targets, index, length) {
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);
const tlChild = targets ?
new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, length) :
new Timer(/** @type {TimerParams} */(childParams), tl, adjustedPosition);
tlChild.init(1);
// TODO: Might be better to insert at a position relative to startTime?
addChild(tl, tlChild);
forEachChildren(tl, (/** @type {Renderable} */child) => {
const childTLOffset = child._offset + child._delay;
const childDur = childTLOffset + child.duration;
if (childDur > tl.iterationDuration) tl.iterationDuration = childDur;
});
tl.duration = getTimelineTotalDuration(tl);
return tl;
}
class Timeline extends Timer {
/**
* @param {TimelineParams} [parameters]
*/
constructor(parameters = {}) {
super(/** @type {TimerParams&TimelineParams} */(parameters), null, 0);
/** @type {Number} */
this.duration = 0; // TL duration starts at 0 and grows when adding children
/** @type {Record<String, Number>} */
this.labels = {};
const defaultsParams = parameters.defaults;
const globalDefaults = globals.defaults;
/** @type {DefaultsParams} */
this.defaults = defaultsParams ? mergeObjects(defaultsParams, globalDefaults) : globalDefaults;
/** @type {Callback<this>} */
this.onRender = parameters.onRender || globalDefaults.onRender;
const tlPlaybackEase = setValue(parameters.playbackEase, globalDefaults.playbackEase);
this._ease = tlPlaybackEase ? parseEasings(tlPlaybackEase) : null;
/** @type {Number} */
this.iterationDuration = 0;
}
/**
* @overload
* @param {TargetsParam} a1
* @param {AnimationParams} a2
* @param {TimePosition} [a3]
* @return {this}
*
* @overload
* @param {TimerParams} a1
* @param {TimePosition} [a2]
* @return {this}
*
* @param {TargetsParam|TimerParams} a1
* @param {AnimationParams|TimePosition} a2
* @param {TimePosition} [a3]
*/
add(a1, a2, a3) {
const isAnim = isObj(a2);
const isTimer = isObj(a1);
if (isAnim || isTimer) {
this._hasChildren = true;
if (isAnim) {
const childParams = /** @type {AnimationParams} */(a2);
// Check for function for children stagger positions
if (isFnc(a3)) {
const staggeredPosition = /** @type {Function} */(a3);
const parsedTargetsArray = parseTargets(/** @type {TargetsParam} */(a1));
// Store initial duration before adding new children that will change the duration
const tlDuration = this.duration;
// Store initial _iterationDuration before adding new children that will change the duration
const tlIterationDuration = this.iterationDuration;
// Store the original id in order to add specific indexes to the new animations ids
const id = childParams.id;
let i = 0;
const parsedLength = parsedTargetsArray.length;
parsedTargetsArray.forEach((/** @type {Target} */target) => {
// Create a new parameter object for each staggered children
const staggeredChildParams = { ...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;
addTlChild(
staggeredChildParams,
this,
staggeredPosition(target, i, parsedLength, this),
target,
i,
parsedLength
);
i++;
});
} else {
addTlChild(
childParams,
this,
parseTimelinePosition(this, a3),
/** @type {TargetsParam} */(a1),
);
}
} else {
// It's a Timer
addTlChild(
/** @type TimerParams */(a1),
this,
parseTimelinePosition(this,/** @type TimePosition */(a2)),
);
}
return this.init(1); // 1 = internalRender
}
}
/**
* @overload
* @param {Tickable} [synced]
* @param {TimePosition} [position]
* @return {this}
*
* @overload
* @param {globalThis.Animation} [synced]
* @param {TimePosition} [position]
* @return {this}
*
* @overload
* @param {WAAPIAnimation} [synced]
* @param {TimePosition} [position]
* @return {this}
*
* @param {Tickable|WAAPIAnimation|globalThis.Animation} [synced]
* @param {TimePosition} [position]
*/
sync(synced, position) {
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);
}
/**
* @param {TargetsParam} targets
* @param {AnimationParams} parameters
* @param {TimePosition} [position]
* @return {this}
*/
set(targets, parameters, position) {
if (isUnd(parameters)) return this;
parameters.duration = minValue;
parameters.composition = compositionTypes.replace;
return this.add(targets, parameters, position);
}
/**
* @param {Callback<Timer>} callback
* @param {TimePosition} [position]
* @return {this}
*/
call(callback, position) {
if (isUnd(callback) || callback && !isFnc(callback)) return this;
return this.add({ duration: 0, onComplete: () => callback(this) }, position);
}
/**
* @param {String} labelName
* @param {TimePosition} [position]
* @return {this}
*
*/
label(labelName, position) {
if (isUnd(labelName) || labelName && !isStr(labelName)) return this;
this.labels[labelName] = parseTimelinePosition(this,/** @type TimePosition */(position));
return this;
}
/**
* @param {TargetsParam} targets
* @param {String} [propertyName]
* @return {this}
*/
remove(targets, propertyName) {
remove(targets, this, propertyName);
return this;
}
/**
* @param {Number} newDuration
* @return {this}
*/
stretch(newDuration) {
const currentDuration = this.duration;
if (currentDuration === normalizeTime(newDuration)) return this;
const timeScale = newDuration / currentDuration;
const labels = this.labels;
forEachChildren(this, (/** @type {JSAnimation} */child) => child.stretch(child.duration * timeScale));
for (let labelName in labels) labels[labelName] *= timeScale;
return super.stretch(newDuration);
}
/**
* @return {this}
*/
refresh() {
forEachChildren(this, (/** @type {JSAnimation} */child) => {
if (child.refresh) child.refresh();
});
return this;
}
/**
* @return {this}
*/
revert() {
super.revert();
forEachChildren(this, (/** @type {JSAnimation} */child) => child.revert, true);
return cleanInlineStyles(this);
}
/**
* @param {Callback<this>} [callback]
* @return {Promise}
*/
then(callback) {
return super.then(callback);
}
}
/**
* @param {TimelineParams} [parameters]
* @return {Timeline}
*/
const createTimeline = parameters => new Timeline(parameters).init();
class Animatable {
/**
* @param {TargetsParam} targets
* @param {AnimatableParams} parameters
*/
constructor(targets, parameters) {
if (scope.current) scope.current.register(this);
/** @type {AnimationParams} */
const globalParams = {};
const properties = {};
this.targets = [];
this.animations = {};
if (isUnd(targets) || isUnd(parameters)) return;
for (let propName in parameters) {
const paramValue = parameters[propName];
if (isKey(propName)) {
properties[propName] = paramValue;
} else {
globalParams[propName] = paramValue;
}
}
for (let propName in properties) {
const propValue = properties[propName];
const isObjValue = isObj(propValue);
/** @type {TweenParamsOptions} */
let propParams = {};
let to = '+=0';
if (isObjValue) {
const unit = propValue.unit;
if (isStr(unit)) to += unit;
} else {
propParams.duration = propValue;
}
propParams[propName] = isObjValue ? mergeObjects({ to }, propValue) : to;
const animParams = mergeObjects(globalParams, propParams);
animParams.composition = compositionTypes.replace;
animParams.autoplay = false;
const animation = this.animations[propName] = new JSAnimation(targets, animParams, null, 0, false).init();
if (!this.targets.length) this.targets.push(...animation.targets);
/** @type {AnimatableProperty} */
this[propName] = (to, duration, ease) => {
const tween = /** @type {Tween} */(animation._head);
if (isUnd(to) && tween) {
const numbers = tween._numbers;
if (numbers && numbers.length) {
return numbers;
} else {
return tween._modifier(tween._number);
}
} else {
forEachChildren(animation, (/** @type {Tween} */tween) => {
if (isArr(to)) {
for (let i = 0, l = /** @type {Array} */(to).length; i < l; i++) {
if (!isUnd(tween._numbers[i])) {
tween._fromNumbers[i] = /** @type {Number} */(tween._modifier(tween._numbers[i]));
tween._toNumbers[i] = to[i];
}
}
} else {
tween._fromNumber = /** @type {Number} */(tween._modifier(tween._number));
tween._toNumber = /** @type {Number} */(to);
}
if (!isUnd(ease)) tween._ease = parseEasings(ease);
tween._currentTime = 0;
});
if (!isUnd(duration)) animation.stretch(duration);
animation.reset(1).resume();
return this;
}
};
}
}
revert() {
for (let propName in this.animations) {
this[propName] = noop;
this.animations[propName].revert();
}
this.animations = {};
this.targets.length = 0;
return this;
}
}
/**
* @param {TargetsParam} targets
* @param {AnimatableParams} parameters
* @return {AnimatableObject}
*/
const createAnimatable = (targets, parameters) => /** @type {AnimatableObject} */(new Animatable(targets, parameters));
/*
* Spring ease solver adapted from https://webkit.org/demos/spring/spring.js
* Webkit Copyright © 2016 Apple Inc
*/
/**
* @typedef {Object} SpringParams
* @property {Number} [mass=1] - Mass, default 1
* @property {Number} [stiffness=100] - Stiffness, default 100
* @property {Number} [damping=10] - Damping, default 10
* @property {Number} [velocity=0] - Initial velocity, default 0
*/
class Spring {
/**
* @param {SpringParams} [parameters]
*/
constructor(parameters = {}) {
this.timeStep = .02; // Interval fed to the solver to calculate duration
this.restThreshold = .0005; // Values below this threshold are considered resting position
this.restDuration = 200; // Duration in ms used to check if the spring is resting after reaching restThreshold
this.maxDuration = 60000; // The maximum allowed spring duration in ms (default 1 min)
this.maxRestSteps = this.restDuration / this.timeStep / K; // How many steps allowed after reaching restThreshold before stopping the duration calculation
this.maxIterations = this.maxDuration / this.timeStep / K; // Calculate the maximum iterations allowed based on maxDuration
this.m = clamp(setValue(parameters.mass, 1), 0, K);
this.s = clamp(setValue(parameters.stiffness, 100), 1, K);
this.d = clamp(setValue(parameters.damping, 10), .1, K);
this.v = clamp(setValue(parameters.velocity, 0), -1e3, K);
this.w0 = 0;
this.zeta = 0;
this.wd = 0;
this.b = 0;
this.solverDuration = 0;
this.duration = 0;
this.compute();
/** @type {EasingFunction} */
this.ease = t => t === 0 || t === 1 ? t : this.solve(t * this.solverDuration);
}
/** @type {EasingFunction} */
solve(time) {
const { zeta, w0, wd, b } = this;
let t = time;
if (zeta < 1) {
t = exp(-t * zeta * w0) * (1 * cos(wd * t) + b * sin(wd * t));
} else {
t = (1 + b * t) * exp(-t * w0);
}
return 1 - t;
}
compute() {
const { maxRestSteps, maxIterations, restThreshold, timeStep, m, d, s, v } = this;
const w0 = this.w0 = clamp(sqrt(s / m), minValue, K);
const zeta = this.zeta = d / (2 * sqrt(s * m));
const wd = this.wd = zeta < 1 ? w0 * sqrt(1 - zeta * zeta) : 0;
this.b = zeta < 1 ? (zeta * w0 + -v) / wd : -v + w0;
let solverTime = 0;
let restSteps = 0;
let iterations = 0;
while (restSteps < maxRestSteps && iterations < maxIterations) {
if (abs(1 - this.solve(solverTime)) < restThreshold) {
restSteps++;
} else {
restSteps = 0;
}
this.solverDuration = solverTime;
solverTime += timeStep;
iterations++;
}
this.duration = round(this.solverDuration * K, 0) * globals.timeScale;
}
get mass() {
return this.m;
}
set mass(v) {
this.m = clamp(setValue(v, 1), 0, K);
this.compute();
}
get stiffness() {
return this.s;
}
set stiffness(v) {
this.s = clamp(setValue(v, 100), 1, K);
this.compute();
}
get damping() {
return this.d;
}
set damping(v) {
this.d = clamp(setValue(v, 10), .1, K);
this.compute();
}
get velocity() {
return this.v;
}
set velocity(v) {
this.v = clamp(setValue(v, 0), -1e3, K);
this.compute();
}
}
/**
* @param {SpringParams} [parameters]
* @returns {Spring}
*/
const createSpring = (parameters) => new Spring(parameters);
/**
* @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);
}
/**
* @callback TraverseParentsCallback
* @param {DOMTarget} $el
* @param {Number} i
*/
/**
* @param {TraverseParentsCallback} cb
*/
traverseUp(cb) {
let $el = /** @type {DOMTarget|Document} */(this.$el.parentElement), i = 0;
while ($el && $el !== doc) {
cb(/** @type {DOMTarget} */($el), i);
$el = /** @type {DOMTarget} */($el.parentElement);
i++;
}
}
getMatrix() {
const matrix = new DOMMatrix();
this.traverseUp($el => {
const transformValue = getComputedStyle($el).transform;
if (transformValue) {
const elMatrix = new DOMMatrix(transformValue);
matrix.preMultiplySelf(elMatrix);
}
});
return matrix;
}
remove() {
this.traverseUp(($el, i) => {
this.inlineTransforms[i] = $el.style.transform;
$el.style.transform = 'none';
});
}
revert() {
this.traverseUp(($el, i) => {
const ct = this.inlineTransforms[i];
if (ct === '') {
$el.style.removeProperty('transform');
} else {
$el.style.transform = ct;
}
});
}
}
/**
* @template {Array<Number>|DOMTargetSelector|String|Number|Boolean|Function|DraggableCursorParams} T
* @param {T | ((draggable: Draggable) => T)} value
* @param {Draggable} draggable
* @return {T}
*/
const parseDraggableFunctionParameter = (value, draggable) => value && isFnc(value) ? /** @type {Function} */(value)(draggable) : value;
let zIndex = 0;
class Draggable {
/**
* @param {TargetsParam} target
* @param {DraggableParams} [parameters]
*/
constructor(target, parameters = {}) {
if (!target) return;
if (scope.current) scope.current.register(this);
const paramX = parameters.x;
const paramY = parameters.y;
const trigger = parameters.trigger;
const modifier = parameters.modifier;
const ease = parameters.releaseEase;
const customEase = ease && parseEasings(ease);
const hasSpring = !isUnd(ease) && !isUnd(/** @type {Spring} */(ease).ease);
const xProp = /** @type {String} */(isObj(paramX) && !isUnd(/** @type {Object} */(paramX).mapTo) ? /** @type {Object} */(paramX).mapTo : 'translateX');
const yProp = /** @type {String} */(isObj(paramY) && !isUnd(/** @type {Object} */(paramY).mapTo) ? /** @type {Object} */(paramY).mapTo : 'translateY');
const container = parseDraggableFunctionParameter(parameters.container, this);
this.containerArray = isArr(container) ? container : null;
this.$container = /** @type {HTMLElement} */(container && !this.containerArray ? parseTargets(/** @type {DOMTarget} */(container))[0] : doc.body);
this.useWin = this.$container === doc.body;
/** @type {Window | HTMLElement} */
this.$scrollContainer = this.useWin ? win : this.$container;
this.$target = /** @type {HTMLElement} */(isObj(target) ? new DOMProxy(target) : parseTargets(target)[0]);
this.$trigger = /** @type {HTMLElement} */(parseTargets(trigger ? trigger : target)[0]);
this.fixed = getTargetValue(this.$target, 'position') === 'fixed';
// Refreshable parameters
this.isFinePointer = true;
/** @type {[Number, Number, Number, Number]} */
this.containerPadding = [0, 0, 0, 0];
/** @type {Number} */
this.containerFriction = 0;
/** @type {Number} */
this.releaseContainerFriction = 0;
/** @type {Number|Array<Number>} */
this.snapX = 0;
/** @type {Number|Array<Number>} */
this.snapY = 0;
/** @type {Number} */
this.scrollSpeed = 0;
/** @type {Number} */
this.scrollThreshold = 0;
/** @type {Number} */
this.dragSpeed = 0;
/** @type {Number} */
this.maxVelocity = 0;
/** @type {Number} */
this.minVelocity = 0;
/** @type {Number} */
this.velocityMultiplier = 0;
/** @type {Boolean|DraggableCursorParams} */
this.cursor = false;
/** @type {Spring} */
this.releaseXSpring = hasSpring ? /** @type {Spring} */(ease) : createSpring({
mass: setValue(parameters.releaseMass, 1),
stiffness: setValue(parameters.releaseStiffness, 80),
damping: setValue(parameters.releaseDamping, 20),
});
/** @type {Spring} */
this.releaseYSpring = hasSpring ? /** @type {Spring} */(ease) : createSpring({
mass: setValue(parameters.releaseMass, 1),
stiffness: setValue(parameters.releaseStiffness, 80),
damping: setValue(parameters.releaseDamping, 20),
});
/** @type {EasingFunction} */
this.releaseEase = customEase || eases.outQuint;
/** @type {Boolean} */
this.hasReleaseSpring = hasSpring;
/** @type {Callback<this>} */
this.onGrab = parameters.onGrab || noop;
/** @type {Callback<this>} */
this.onDrag = parameters.onDrag || noop;
/** @type {Callback<this>} */
this.onRelease = parameters.onRelease || noop;
/** @type {Callback<this>} */
this.onUpdate = parameters.onUpdate || noop;
/** @type {Callback<this>} */
this.onSettle = parameters.onSettle || noop;
/** @type {Callback<this>} */
this.onSnap = parameters.onSnap || noop;
/** @type {Callback<this>} */
this.onResize = parameters.onResize || noop;
/** @type {Callback<this>} */
this.onAfterResize = parameters.onAfterResize || noop;
/** @type {[Number, Number]} */
this.disabled = [0, 0];
/** @type {AnimatableParams} */
const animatableParams = {};
if (modifier) animatableParams.modifier = modifier;
if (isUnd(paramX) || paramX === true) {
animatableParams[xProp] = 0;
} else if (isObj(paramX)) {
const paramXObject = /** @type {DraggableAxisParam} */(paramX);
const animatableXParams = {};
if (paramXObject.modifier) animatableXParams.modifier = paramXObject.modifier;
if (paramXObject.composition) animatableXParams.composition = paramXObject.composition;
animatableParams[xProp] = animatableXParams;
} else if (paramX === false) {
animatableParams[xProp] = 0;
this.disabled[0] = 1;
}
if (isUnd(paramY) || paramY === true) {
animatableParams[yProp] = 0;
} else if (isObj(paramY)) {
const paramYObject = /** @type {DraggableAxisParam} */(paramY);
const animatableYParams = {};
if (paramYObject.modifier) animatableYParams.modifier = paramYObject.modifier;
if (paramYObject.composition) animatableYParams.composition = paramYObject.composition;
animatableParams[yProp] = animatableYParams;
} else if (paramY === false) {
animatableParams[yProp] = 0;
this.disabled[1] = 1;
}
/** @type {AnimatableObject} */
this.animate = /** @type {AnimatableObject} */(new Animatable(this.$target, animatableParams));
// Internal props
this.xProp = xProp;
this.yProp = yProp;
this.destX = 0;
this.destY = 0;
this.deltaX = 0;
this.deltaY = 0;
this.scroll = {x: 0, y: 0};
/** @type {[Number, Number, Number, Number]} */
this.coords = [this.x, this.y, 0, 0]; // x, y, temp x, temp y
/** @type {[Number, Number]} */
this.snapped = [0, 0]; // x, y
/** @type {[Number, Number, Number, Number, Number, Number, Number, Number]} */
this.pointer = [0, 0, 0, 0, 0, 0, 0, 0]; // x1, y1, x2, y2, temp x1, temp y1, temp x2, temp y2
/** @type {[Number, Number]} */
this.scrollView = [0, 0]; // w, h
/** @type {[Number, Number, Number, Number]} */
this.dragArea = [0, 0, 0, 0]; // x, y, w, h
/** @type {[Number, Number, Number, Number]} */
this.containerBounds = [-1e12, maxValue, maxValue, -1e12]; // t, r, b, l
/** @type {[Number, Number, Number, Number]} */
this.scrollBounds = [0, 0, 0, 0]; // t, r, b, l
/** @type {[Number, Number, Number, Number]} */
this.targetBounds = [0, 0, 0, 0]; // t, r, b, l
/** @type {[Number, Number]} */
this.window = [0, 0]; // w, h
/** @type {[Number, Number, Number]} */
this.velocityStack = [0, 0, 0];
/** @type {Number} */
this.velocityStackIndex = 0;
/** @type {Number} */
this.velocityTime = now();
/** @type {Number} */
this.velocity = 0;
/** @type {Number} */
this.angle = 0;
/** @type {JSAnimation} */
this.cursorStyles = null;
/** @type {JSAnimation} */
this.triggerStyles = null;
/** @type {JSAnimation} */
this.bodyStyles = null;
/** @type {JSAnimation} */
this.targetStyles = null;
/** @type {JSAnimation} */
this.touchActionStyles = null;
this.transforms = new Transforms(this.$target);
this.overshootCoords = { x: 0, y: 0 };
this.overshootXTicker = new Timer({ autoplay: false }, null, 0).init();
this.overshootYTicker = new Timer({ autoplay: false }, null, 0).init();
this.updateTicker = new Timer({ autoplay: false }, null, 0).init();
this.overshootXTicker.onUpdate = () => {
if (this.disabled[0]) return;
this.updated = true;
this.manual = true;
this.animate[this.xProp](this.overshootCoords.x, 0);
};
this.overshootXTicker.onComplete = () => {
if (this.disabled[0]) return;
this.manual = false;
this.animate[this.xProp](this.overshootCoords.x, 0);
};
this.overshootYTicker.onUpdate = () => {
if (this.disabled[1]) return;
this.updated = true;
this.manual = true;
this.animate[this.yProp](this.overshootCoords.y, 0);
};
this.overshootYTicker.onComplete = () => {
if (this.disabled[1]) return;
this.manual = false;
this.animate[this.yProp](this.overshootCoords.y, 0);
};
this.updateTicker.onUpdate = () => this.update();
this.contained = !isUnd(container);
this.manual = false;
this.grabbed = false;
this.dragged = false;
this.updated = false;
this.released = false;
this.canScroll = false;
this.enabled = false;
this.initialized = false;
this.activeProp = this.disabled[1] ? xProp : yProp;
this.animate.animations[this.activeProp].onRender = () => {
const hasUpdated = this.updated;
const hasMoved = this.grabbed && hasUpdated;
const hasReleased = !hasMoved && this.released;
const x = this.x;
const y = this.y;
const dx = x - this.coords[2];
const dy = y - this.coords[3];
this.deltaX = dx;
this.deltaY = dy;
this.coords[2] = x;
this.coords[3] = y;
// Check if dx or dy are not 0 to check if the draggable has actually moved https://github.com/juliangarnier/anime/issues/1032
if (hasUpdated && (dx || dy)) {
this.onUpdate(this);
}
if (!hasReleased) {
this.updated = false;
} else {
this.computeVelocity(dx, dy);
this.angle = atan2(dy, dx);
}
};
this.animate.animations[this.activeProp].onComplete = () => {
if ((!this.grabbed && this.released)) {
// Set eleased to false before calling onSettle to avoid recursion
this.released = false;
}
if (!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 Timer({
autoplay: false,
duration: 150 * globals.timeScale,
onComplete: () => {
this.onResize(this);
this.refresh();
this.onAfterResize(this);
},
}).init();
this.parameters = parameters;
this.resizeObserver = new ResizeObserver(() => {
if (this.initialized) {
this.resizeTicker.restart();
} else {
this.initialized = true;
}
});
this.enable();
this.refresh();
this.resizeObserver.observe(this.$container);
if (!isObj(target)) this.resizeObserver.observe(this.$target);
}
/**
* @param {Number} dx
* @param {Number} dy
* @return {Number}
*/
computeVelocity(dx, dy) {
const prevTime = this.velocityTime;
const curTime = now();
const elapsed = curTime - prevTime;
if (elapsed < 17) return this.velocity;
this.velocityTime = curTime;
const velocityStack = this.velocityStack;
const vMul = this.velocityMultiplier;
const minV = this.minVelocity;
const maxV = this.maxVelocity;
const vi = this.velocityStackIndex;
velocityStack[vi] = round(clamp((sqrt(dx * dx + dy * dy) / elapsed) * vMul, minV, maxV), 5);
const velocity = max(velocityStack[0], velocityStack[1], velocityStack[2]);
this.velocity = velocity;
this.velocityStackIndex = (vi + 1) % 3;
return velocity;
}
/**
* @param {Number} x
* @param {Boolean} [muteUpdateCallback]
* @return {this}
*/
setX(x, muteUpdateCallback = false) {
if (this.disabled[0]) return;
const v = round(x, 5);
this.overshootXTicker.pause();
this.manual = true;
this.updated = !muteUpdateCallback;
this.destX = v;
this.snapped[0] = snap(v, this.snapX);
this.animate[this.xProp](v, 0);
this.manual = false;
return this;
}
/**
* @param {Number} y
* @param {Boolean} [muteUpdateCallback]
* @return {this}
*/
setY(y, muteUpdateCallback = false) {
if (this.disabled[1]) return;
const v = round(y, 5);
this.overshootYTicker.pause();
this.manual = true;
this.updated = !muteUpdateCallback;
this.destY = v;
this.snapped[1] = snap(v, this.snapY);
this.animate[this.yProp](v, 0);
this.manual = false;
return this;
}
get x() {
return round(/** @type {Number} */(this.animate[this.xProp]()), globals.precision);
}
set x(x) {
this.setX(x, false);
}
get y() {
return round(/** @type {Number} */(this.animate[this.yProp]()), globals.precision);
}
set y(y) {
this.setY(y, false);
}
get progressX() {
return mapRange(this.x, this.containerBounds[3], this.containerBounds[1], 0, 1);
}
set progressX(x) {
this.setX(mapRange(x, 0, 1, this.containerBounds[3], this.containerBounds[1]), false);
}
get progressY() {
return mapRange(this.y, this.containerBounds[0], this.containerBounds[2], 0, 1);
}
set progressY(y) {
this.setY(mapRange(y, 0, 1, this.containerBounds[0], this.containerBounds[2]), false);
}
updateScrollCoords() {
const sx = round(this.useWin ? win.scrollX : this.$container.scrollLeft, 0);
const sy = round(this.useWin ? win.scrollY : this.$container.scrollTop, 0);
const [ cpt, cpr, cpb, cpl ] = this.containerPadding;
const threshold = this.scrollThreshold;
this.scroll.x = sx;
this.scroll.y = sy;
this.scrollBounds[0] = sy - this.targetBounds[0] + cpt - threshold;
this.scrollBounds[1] = sx - this.targetBounds[1] - cpr + threshold;
this.scrollBounds[2] = sy - this.targetBounds[2] - cpb + threshold;
this.scrollBounds[3] = sx - this.targetBounds[3] + cpl - threshold;
}
updateBoundingValues() {
const $container = this.$container;
const cx = this.x;
const cy = this.y;
const cx2 = this.coords[2];
const cy2 = this.coords[3];
// Prevents interfering with the scroll area in cases the target is outside of the container
// Make sure the temp coords are also adjuset to prevents wrong delta calculation on updates
this.coords[2] = 0;
this.coords[3] = 0;
this.setX(0, true);
this.setY(0, true);
this.transforms.remove();
const iw = this.window[0] = win.innerWidth;
const ih = this.window[1] = win.innerHeight;
const uw = this.useWin;
const sw = $container.scrollWidth;
const sh = $container.scrollHeight;
const fx = this.fixed;
const transformContainerRect = $container.getBoundingClientRect();
const [ cpt, cpr, cpb, cpl ] = this.containerPadding;
this.dragArea[0] = uw ? 0 : transformContainerRect.left;
this.dragArea[1] = uw ? 0 : transformContainerRect.top;
this.scrollView[0] = uw ? clamp(sw, iw, sw) : sw;
this.scrollView[1] = uw ? clamp(sh, ih, sh) : sh;
this.updateScrollCoords();
const { width, height, left, top, right, bottom } = $container.getBoundingClientRect();
this.dragArea[2] = round(uw ? clamp(width, iw, iw) : width, 0);
this.dragArea[3] = round(uw ? clamp(height, ih, ih) : height, 0);
const containerOverflow = getTargetValue($container, 'overflow');
const visibleOverflow = containerOverflow === 'visible';
const hiddenOverflow = containerOverflow === 'hidden';
this.canScroll = fx ? false :
this.contained &&
(($container === doc.body && visibleOverflow) || (!hiddenOverflow && !visibleOverflow)) &&
(sw > this.dragArea[2] + cpl - cpr || sh > this.dragArea[3] + cpt - cpb) &&
(!this.containerArray || (this.containerArray && !isArr(this.containerArray)));
if (this.contained) {
const sx = this.scroll.x;
const sy = this.scroll.y;
const canScroll = this.canScroll;
const targetRect = this.$target.getBoundingClientRect();
const hiddenLeft = canScroll ? uw ? 0 : $container.scrollLeft : 0;
const hiddenTop = canScroll ? uw ? 0 : $container.scrollTop : 0;
const hiddenRight = canScroll ? this.scrollView[0] - hiddenLeft - width : 0;
const hiddenBottom = canScroll ? this.scrollView[1] - hiddenTop - height : 0;
this.targetBounds[0] = round((targetRect.top + sy) - (uw ? 0 : top), 0);
this.targetBounds[1] = round((targetRect.right + sx) - (uw ? iw : right), 0);
this.targetBounds[2] = round((targetRect.bottom + sy) - (uw ? ih : bottom), 0);
this.targetBounds[3] = round((targetRect.left + sx) - (uw ? 0 : left), 0);
if (this.containerArray) {
this.containerBounds[0] = this.containerArray[0] + cpt;
this.containerBounds[1] = this.containerArray[1] - cpr;
this.containerBounds[2] = this.containerArray[2] - cpb;
this.containerBounds[3] = this.containerArray[3] + cpl;
} else {
this.containerBounds[0] = -round(targetRect.top - (fx ? clamp(top, 0, ih) : top) + hiddenTop - cpt, 0);
this.containerBounds[1] = -round(targetRect.right - (fx ? clamp(right, 0, iw) : right) - hiddenRight + cpr, 0);
this.containerBounds[2] = -round(targetRect.bottom - (fx ? clamp(bottom, 0, ih) : bottom) - hiddenBottom + cpb, 0);
this.containerBounds[3] = -round(targetRect.left - (fx ? clamp(left, 0, iw) : left) + hiddenLeft - cpl, 0);
}
}
this.transforms.revert();
// Restore coordinates
this.coords[2] = cx2;
this.coords[3] = cy2;
this.setX(cx, true);
this.setY(cy, true);
}
/**
* Returns 0 if not OB, 1 if x is OB, 2 if y is OB, 3 if both x and y are OB
*
* @param {Array} bounds
* @param {Number} x
* @param {Number} y
* @return {Number}
*/
isOutOfBounds(bounds, x, y) {
if (!this.contained) return 0;
const [ bt, br, bb, bl ] = bounds;
const [ dx, dy ] = this.disabled;
const obx = !dx && x < bl || !dx && x > br;
const oby = !dy && y < bt || !dy && y > bb;
return obx && !oby ? 1 : !obx && oby ? 2 : obx && oby ? 3 : 0;
}
refresh() {
const params = this.parameters;
const paramX = params.x;
const paramY = params.y;
const container = parseDraggableFunctionParameter(params.container, this);
const cp = parseDraggableFunctionParameter(params.containerPadding, this) || 0;
const containerPadding = /** @type {[Number, Number, Number, Number]} */(isArr(cp) ? cp : [cp, cp, cp, cp]);
const cx = this.x;
const cy = this.y;
const parsedCursorStyles = parseDraggableFunctionParameter(params.cursor, this);
const cursorStyles = { onHover: 'grab', onGrab: 'grabbing' };
if (parsedCursorStyles) {
const { onHover, onGrab } = /** @type {DraggableCursorParams} */(parsedCursorStyles);
if (onHover) cursorStyles.onHover = onHover;
if (onGrab) cursorStyles.onGrab = onGrab;
}
this.containerArray = isArr(container) ? container : null;
this.$container = /** @type {HTMLElement} */(container && !this.containerArray ? parseTargets(/** @type {DOMTarget} */(container))[0] : doc.body);
this.useWin = this.$container === doc.body;
/** @type {Window | HTMLElement} */
this.$scrollContainer = this.useWin ? win : this.$container;
this.isFinePointer = matchMedia('(pointer:fine)').matches;
this.containerPadding = setValue(containerPadding, [0, 0, 0, 0]);
this.containerFriction = clamp(setValue(parseDraggableFunctionParameter(params.containerFriction, this), .8), 0, 1);
this.releaseContainerFriction = clamp(setValue(parseDraggableFunctionParameter(params.releaseContainerFriction, this), this.containerFriction), 0, 1);
this.snapX = parseDraggableFunctionParameter(isObj(paramX) && !isUnd(paramX.snap) ? paramX.snap : params.snap, this);
this.snapY = parseDraggableFunctionParameter(isObj(paramY) && !isUnd(paramY.snap) ? paramY.snap : params.snap, this);
this.scrollSpeed = setValue(parseDraggableFunctionParameter(params.scrollSpeed, this), 1.5);
this.scrollThreshold = setValue(parseDraggableFunctionParameter(params.scrollThreshold, this), 20);
this.dragSpeed = setValue(parseDraggableFunctionParameter(params.dragSpeed, this), 1);
this.minVelocity = setValue(parseDraggableFunctionParameter(params.minVelocity, this), 0);
this.maxVelocity = setValue(parseDraggableFunctionParameter(params.maxVelocity, this), 50);
this.velocityMultiplier = setValue(parseDraggableFunctionParameter(params.velocityMultiplier, this), 1);
this.cursor = parsedCursorStyles === false ? false : cursorStyles;
this.updateBoundingValues();
// const ob = this.isOutOfBounds(this.containerBounds, this.x, this.y);
// if (ob === 1 || ob === 3) this.progressX = px;
// if (ob === 2 || ob === 3) this.progressY = py;
// if (this.initialized && this.contained) {
// if (this.progressX !== px) this.progressX = px;
// if (this.progressY !== py) this.progressY = py;
// }
const [ bt, br, bb, bl ] = this.containerBounds;
this.setX(clamp(cx, bl, br), true);
this.setY(clamp(cy, bt, bb), true);
}
update() {
this.updateScrollCoords();
if (this.canScroll) {
const [ cpt, cpr, cpb, cpl ] = this.containerPadding;
const [ sw, sh ] = this.scrollView;
const daw = this.dragArea[2];
const dah = this.dragArea[3];
const csx = this.scroll.x;
const csy = this.scroll.y;
const nsw = this.$container.scrollWidth;
const nsh = this.$container.scrollHeight;
const csw = this.useWin ? clamp(nsw, this.window[0], nsw) : nsw;
const csh = this.useWin ? clamp(nsh, this.window[1], nsh) : nsh;
const swd = sw - csw;
const shd = sh - csh;
// Handle cases where the scrollarea dimensions changes during drag
if (this.dragged && swd > 0) {
this.coords[0] -= swd;
this.scrollView[0] = csw;
}
if (this.dragged && shd > 0) {
this.coords[1] -= shd;
this.scrollView[1] = csh;
}
// Handle autoscroll when target is at the edges of the scroll bounds
const s = this.scrollSpeed * 10;
const threshold = this.scrollThreshold;
const [ x, y ] = this.coords;
const [ st, sr, sb, sl ] = this.scrollBounds;
const t = round(clamp((y - st + cpt) / threshold, -1, 0) * s, 0);
const r = round(clamp((x - sr - cpr) / threshold, 0, 1) * s, 0);
const b = round(clamp((y - sb - cpb) / threshold, 0, 1) * s, 0);
const l = round(clamp((x - sl + cpl) / threshold, -1, 0) * s, 0);
if (t || b || l || r) {
const [nx, ny] = this.disabled;
let scrollX = csx;
let scrollY = csy;
if (!nx) {
scrollX = round(clamp(csx + (l || r), 0, sw - daw), 0);
this.coords[0] -= csx - scrollX;
}
if (!ny) {
scrollY = round(clamp(csy + (t || b), 0, sh - dah), 0);
this.coords[1] -= csy - scrollY;
}
// Note: Safari mobile requires to use different scroll methods depending if using the window or not
if (this.useWin) {
this.$scrollContainer.scrollBy(-(csx - scrollX), -(csy - scrollY));
} else {
this.$scrollContainer.scrollTo(scrollX, scrollY);
}
}
}
const [ ct, cr, cb, cl ] = this.containerBounds;
const [ px1, py1, px2, py2, px3, py3 ] = this.pointer;
this.coords[0] += (px1 - px3) * this.dragSpeed;
this.coords[1] += (py1 - py3) * this.dragSpeed;
this.pointer[4] = px1;
this.pointer[5] = py1;
const [ cx, cy ] = this.coords;
const [ sx, sy ] = this.snapped;
const cf = (1 - this.containerFriction) * this.dragSpeed;
this.setX(cx > cr ? cr + (cx - cr) * cf : cx < cl ? cl + (cx - cl) * cf : cx, false);
this.setY(cy > cb ? cb + (cy - cb) * cf : cy < ct ? ct + (cy - ct) * cf : cy, false);
this.computeVelocity(px1 - px3, py1 - py3);
this.angle = atan2(py1 - py2, px1 - px2);
const [ nsx, nsy ] = this.snapped;
if (nsx !== sx && this.snapX || nsy !== sy && this.snapY) {
this.onSnap(this);
}
}
stop() {
this.updateTicker.pause();
this.overshootXTicker.pause();
this.overshootYTicker.pause();
// Pauses the in bounds onRelease animations
for (let prop in this.animate.animations) this.animate.animations[prop].pause();
remove(this, null, 'x');
remove(this, null, 'y');
remove(this, null, 'progressX');
remove(this, null, 'progressY');
remove(this.scroll); // Removes any active animations on the container scroll
remove(this.overshootCoords); // Removes active overshoot animations
return this;
}
/**
* @param {Number} [duration]
* @param {Number} [gap]
* @param {EasingParam} [ease]
* @return {this}
*/
scrollInView(duration, gap = 0, ease = eases.inOutQuad) {
this.updateScrollCoords();
const x = this.destX;
const y = this.destY;
const scroll = this.scroll;
const scrollBounds = this.scrollBounds;
const canScroll = this.canScroll;
if (!this.containerArray && this.isOutOfBounds(scrollBounds, x, y)) {
const [ st, sr, sb, sl ] = scrollBounds;
const t = round(clamp(y - st, -1e12, 0), 0);
const r = round(clamp(x - sr, 0, maxValue), 0);
const b = round(clamp(y - sb, 0, maxValue), 0);
const l = round(clamp(x - sl, -1e12, 0), 0);
new JSAnimation(scroll, {
x: round(scroll.x + (l ? l - gap : r ? r + gap : 0), 0),
y: round(scroll.y + (t ? t - gap : b ? b + gap : 0), 0),
duration: isUnd(duration) ? 350 * globals.timeScale : duration,
ease,
onUpdate: () => {
this.canScroll = false;
this.$scrollContainer.scrollTo(scroll.x, scroll.y);
}
}).init().then(() => {
this.canScroll = canScroll;
});
}
return this;
}
handleHover() {
if (this.isFinePointer && this.cursor && !this.cursorStyles) {
this.cursorStyles = setTargetValues(this.$trigger, {
cursor: /** @type {DraggableCursorParams} */(this.cursor).onHover
});
}
}
/**
* @param {Number} [duration]
* @param {Number} [gap]
* @param {EasingParam} [ease]
* @return {this}
*/
animateInView(duration, gap = 0, ease = eases.inOutQuad) {
this.stop();
this.updateBoundingValues();
const x = this.x;
const y = this.y;
const [ cpt, cpr, cpb, cpl ] = this.containerPadding;
const bt = this.scroll.y - this.targetBounds[0] + cpt + gap;
const br = this.scroll.x - this.targetBounds[1] - cpr - gap;
const bb = this.scroll.y - this.targetBounds[2] - cpb - gap;
const bl = this.scroll.x - this.targetBounds[3] + cpl + gap;
const ob = this.isOutOfBounds([bt, br, bb, bl], x, y);
if (ob) {
const [ disabledX, disabledY ] = this.disabled;
const destX = clamp(snap(x, this.snapX), bl, br);
const destY = clamp(snap(y, this.snapY), bt, bb);
const dur = isUnd(duration) ? 350 * globals.timeScale : duration;
if (!disabledX && (ob === 1 || ob === 3)) this.animate[this.xProp](destX, dur, ease);
if (!disabledY && (ob === 2 || ob === 3)) this.animate[this.yProp](destY, dur, ease);
}
return this;
}
/**
* @param {MouseEvent|TouchEvent} e
*/
handleDown(e) {
const $eTarget = /** @type {HTMLElement} */(e.target);
if (this.grabbed || /** @type {HTMLInputElement} */($eTarget).type === 'range') return;
e.stopPropagation();
this.grabbed = true;
this.released = false;
this.stop();
this.updateBoundingValues();
const touches = /** @type {TouchEvent} */(e).changedTouches;
const eventX = touches ? touches[0].clientX : /** @type {MouseEvent} */(e).clientX;
const eventY = touches ? touches[0].clientY : /** @type {MouseEvent} */(e).clientY;
const { x, y } = this.transforms.normalizePoint(eventX, eventY);
const [ ct, cr, cb, cl ] = this.containerBounds;
const cf = (1 - this.containerFriction) * this.dragSpeed;
const cx = this.x;
const cy = this.y;
this.coords[0] = this.coords[2] = !cf ? cx : cx > cr ? cr + (cx - cr) / cf : cx < cl ? cl + (cx - cl) / cf : cx;
this.coords[1] = this.coords[3] = !cf ? cy : cy > cb ? cb + (cy - cb) / cf : cy < ct ? ct + (cy - ct) / cf : cy;
this.pointer[0] = x;
this.pointer[1] = y;
this.pointer[2] = x;
this.pointer[3] = y;
this.pointer[4] = x;
this.pointer[5] = y;
this.pointer[6] = x;
this.pointer[7] = y;
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.angle = 0;
if (this.targetStyles) {
this.targetStyles.revert();
this.targetStyles = null;
}
const z = /** @type {Number} */(getTargetValue(this.$target, 'zIndex', false));
zIndex = (z > zIndex ? z : zIndex) + 1;
this.targetStyles = setTargetValues(this.$target, { zIndex });
if (this.triggerStyles) {
this.triggerStyles.revert();
this.triggerStyles = null;
}
if (this.cursorStyles) {
this.cursorStyles.revert();
this.cursorStyles = null;
}
if (this.isFinePointer && this.cursor) {
this.bodyStyles = setTargetValues(doc.body, {
cursor: /** @type {DraggableCursorParams} */(this.cursor).onGrab
});
}
this.scrollInView(100, 0, eases.out(3));
this.onGrab(this);
doc.addEventListener('touchmove', this);
doc.addEventListener('touchend', this);
doc.addEventListener('touchcancel', this);
doc.addEventListener('mousemove', this);
doc.addEventListener('mouseup', this);
doc.addEventListener('selectstart', this);
}
/**
* @param {MouseEvent|TouchEvent} e
*/
handleMove(e) {
if (!this.grabbed) return;
const touches = /** @type {TouchEvent} */(e).changedTouches;
const eventX = touches ? touches[0].clientX : /** @type {MouseEvent} */(e).clientX;
const eventY = touches ? touches[0].clientY : /** @type {MouseEvent} */(e).clientY;
const { x, y } = this.transforms.normalizePoint(eventX, eventY);
const movedX = x - this.pointer[6];
const movedY = y - this.pointer[7];
let $parent = /** @type {HTMLElement} */(e.target);
let isAtTop = false;
let isAtBottom = false;
let canTouchScroll = false;
while (touches && $parent && $parent !== this.$trigger) {
const overflowY = getTargetValue($parent, 'overflow-y');
if (overflowY !== 'hidden' && overflowY !== 'visible') {
const { scrollTop, scrollHeight, clientHeight } = $parent;
if (scrollHeight > clientHeight) {
canTouchScroll = true;
isAtTop = scrollTop <= 3;
isAtBottom = scrollTop >= (scrollHeight - clientHeight) - 3;
break;
}
}
$parent = /** @type {HTMLElement} */($parent.parentNode);
}
if (canTouchScroll && ((!isAtTop && !isAtBottom) || (isAtTop && movedY < 0) || (isAtBottom && movedY > 0))) {
this.pointer[0] = x;
this.pointer[1] = y;
this.pointer[2] = x;
this.pointer[3] = y;
this.pointer[4] = x;
this.pointer[5] = y;
this.pointer[6] = x;
this.pointer[7] = y;
} else {
preventDefault(e);
// Needed to prevents click on handleUp
if (!this.triggerStyles) this.triggerStyles = setTargetValues(this.$trigger, { pointerEvents: 'none' });
// Needed to prevent page scroll while dragging on touch devvice
this.$trigger.addEventListener('touchstart', preventDefault, { passive: false });
this.$trigger.addEventListener('touchmove', preventDefault, { passive: false });
this.$trigger.addEventListener('touchend', preventDefault);
if ((!this.disabled[0] && abs(movedX) > 3) || (!this.disabled[1] && abs(movedY) > 3)) {
this.updateTicker.resume();
this.pointer[2] = this.pointer[0];
this.pointer[3] = this.pointer[1];
this.pointer[0] = x;
this.pointer[1] = y;
this.dragged = true;
this.released = false;
this.onDrag(this);
}
}
}
handleUp() {
if (!this.grabbed) return;
this.updateTicker.pause();
if (this.triggerStyles) {
this.triggerStyles.revert();
this.triggerStyles = null;
}
if (this.bodyStyles) {
this.bodyStyles.revert();
this.bodyStyles = null;
}
const [ disabledX, disabledY ] = this.disabled;
const [ px1, py1, px2, py2, px3, py3 ] = this.pointer;
const [ ct, cr, cb, cl ] = this.containerBounds;
const [ sx, sy ] = this.snapped;
const springX = this.releaseXSpring;
const springY = this.releaseYSpring;
const releaseEase = this.releaseEase;
const hasReleaseSpring = this.hasReleaseSpring;
const overshootCoords = this.overshootCoords;
const cx = this.x;
const cy = this.y;
const pv = this.computeVelocity(px1 - px3, py1 - py3);
const pa = this.angle = atan2(py1 - py2, px1 - px2);
const ds = pv * 150;
const cf = (1 - this.releaseContainerFriction) * this.dragSpeed;
const nx = cx + (cos(pa) * ds);
const ny = cy + (sin(pa) * ds);
const bx = nx > cr ? cr + (nx - cr) * cf : nx < cl ? cl + (nx - cl) * cf : nx;
const by = ny > cb ? cb + (ny - cb) * cf : ny < ct ? ct + (ny - ct) * cf : ny;
const dx = this.destX = clamp(round(snap(bx, this.snapX), 5), cl, cr);
const dy = this.destY = clamp(round(snap(by, this.snapY), 5), ct, cb);
const ob = this.isOutOfBounds(this.containerBounds, nx, ny);
let durationX = 0;
let durationY = 0;
let easeX = releaseEase;
let easeY = releaseEase;
let longestReleaseDuration = 0;
overshootCoords.x = cx;
overshootCoords.y = cy;
if (!disabledX) {
const directionX = dx === cr ? cx > cr ? -1 : 1 : cx < cl ? -1 : 1;
const distanceX = round(cx - dx, 0);
springX.velocity = disabledY && hasReleaseSpring ? distanceX ? (ds * directionX) / abs(distanceX) : 0 : pv;
const { ease, duration, restDuration } = springX;
durationX = cx === dx ? 0 : hasReleaseSpring ? duration : duration - (restDuration * globals.timeScale);
if (hasReleaseSpring) easeX = ease;
if (durationX > longestReleaseDuration) longestReleaseDuration = durationX;
}
if (!disabledY) {
const directionY = dy === cb ? cy > cb ? -1 : 1 : cy < ct ? -1 : 1;
const distanceY = round(cy - dy, 0);
springY.velocity = disabledX && hasReleaseSpring ? distanceY ? (ds * directionY) / abs(distanceY) : 0 : pv;
const { ease, duration, restDuration } = springY;
durationY = cy === dy ? 0 : hasReleaseSpring ? duration : duration - (restDuration * globals.timeScale);
if (hasReleaseSpring) easeY = ease;
if (durationY > longestReleaseDuration) longestReleaseDuration = durationY;
}
if (!hasReleaseSpring && ob && cf && (durationX || durationY)) {
const composition = compositionTypes.blend;
new JSAnimation(overshootCoords, {
x: { to: bx, duration: durationX * .65 },
y: { to: by, duration: durationY * .65 },
ease: releaseEase,
composition,
}).init();
new JSAnimation(overshootCoords, {
x: { to: dx, duration: durationX },
y: { to: dy, duration: durationY },
ease: releaseEase,
composition,
}).init();
this.overshootXTicker.stretch(durationX).restart();
this.overshootYTicker.stretch(durationY).restart();
} else {
if (!disabledX) this.animate[this.xProp](dx, durationX, easeX);
if (!disabledY) this.animate[this.yProp](dy, durationY, easeY);
}
this.scrollInView(longestReleaseDuration, this.scrollThreshold, releaseEase);
let hasSnapped = false;
if (dx !== sx) {
this.snapped[0] = dx;
if (this.snapX) hasSnapped = true;
}
if (dy !== sy && this.snapY) {
this.snapped[1] = dy;
if (this.snapY) hasSnapped = true;
}
if (hasSnapped) this.onSnap(this);
this.grabbed = false;
this.dragged = false;
this.updated = true;
this.released = true;
// It's important to trigger the callback after the release animations to be able to cancel them
this.onRelease(this);
this.$trigger.removeEventListener('touchstart', preventDefault);
this.$trigger.removeEventListener('touchmove', preventDefault);
this.$trigger.removeEventListener('touchend', preventDefault);
doc.removeEventListener('touchmove', this);
doc.removeEventListener('touchend', this);
doc.removeEventListener('touchcancel', this);
doc.removeEventListener('mousemove', this);
doc.removeEventListener('mouseup', this);
doc.removeEventListener('selectstart', this);
}
reset() {
this.stop();
this.resizeTicker.pause();
this.grabbed = false;
this.dragged = false;
this.updated = false;
this.released = false;
this.canScroll = false;
this.setX(0, true);
this.setY(0, true);
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;
return this;
}
enable() {
if (!this.enabled) {
this.enabled = true;
this.$target.classList.remove('is-disabled');
this.touchActionStyles = setTargetValues(this.$trigger, {
touchAction: this.disabled[0] ? 'pan-x' : this.disabled[1] ? 'pan-y' : 'none'
});
this.$trigger.addEventListener('touchstart', this, { passive: true });
this.$trigger.addEventListener('mousedown', this, { passive: true });
this.$trigger.addEventListener('mouseenter', this);
}
return this;
}
disable() {
this.enabled = false;
this.grabbed = false;
this.dragged = false;
this.updated = false;
this.released = false;
this.canScroll = false;
this.touchActionStyles.revert();
if (this.cursorStyles) {
this.cursorStyles.revert();
this.cursorStyles = null;
}
if (this.triggerStyles) {
this.triggerStyles.revert();
this.triggerStyles = null;
}
if (this.bodyStyles) {
this.bodyStyles.revert();
this.bodyStyles = null;
}
if (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);
doc.removeEventListener('touchmove', this);
doc.removeEventListener('touchend', this);
doc.removeEventListener('touchcancel', this);
doc.removeEventListener('mousemove', this);
doc.removeEventListener('mouseup', this);
doc.removeEventListener('selectstart', this);
return this;
}
revert() {
this.reset();
this.disable();
this.$target.classList.remove('is-disabled');
this.updateTicker.revert();
this.overshootXTicker.revert();
this.overshootYTicker.revert();
this.resizeTicker.revert();
this.animate.revert();
this.resizeObserver.disconnect();
return this;
}
/**
* @param {Event} e
*/
handleEvent(e) {
switch (e.type) {
case 'mousedown':
this.handleDown(/** @type {MouseEvent} */(e));
break;
case 'touchstart':
this.handleDown(/** @type {TouchEvent} */(e));
break;
case 'mousemove':
this.handleMove(/** @type {MouseEvent} */(e));
break;
case 'touchmove':
this.handleMove(/** @type {TouchEvent} */(e));
break;
case 'mouseup':
this.handleUp();
break;
case 'touchend':
this.handleUp();
break;
case 'touchcancel':
this.handleUp();
break;
case 'mouseenter':
this.handleHover();
break;
case 'selectstart':
preventDefault(e);
break;
}
}
}
/**
* @param {TargetsParam} target
* @param {DraggableParams} [parameters]
* @return {Draggable}
*/
const createDraggable = (target, parameters) => new Draggable(target, parameters);
class Scope {
/** @param {ScopeParams} [parameters] */
constructor(parameters = {}) {
if (scope.current) scope.current.register(this);
const rootParam = parameters.root;
/** @type {Document|DOMTarget} */
let root = doc;
if (rootParam) {
root = /** @type {ReactRef} */(rootParam).current ||
/** @type {AngularRef} */(rootParam).nativeElement ||
parseTargets(/** @type {DOMTargetSelector} */(rootParam))[0] ||
doc;
}
const scopeDefaults = parameters.defaults;
const globalDefault = globals.defaults;
const mediaQueries = parameters.mediaQueries;
/** @type {DefaultsParams} */
this.defaults = scopeDefaults ? mergeObjects(scopeDefaults, globalDefault) : globalDefault;
/** @type {Document|DOMTarget} */
this.root = root;
/** @type {Array<ScopeConstructorCallback>} */
this.constructors = [];
/** @type {Array<ScopeCleanupCallback>} */
this.revertConstructors = [];
/** @type {Array<Revertible>} */
this.revertibles = [];
/** @type {Array<ScopeConstructorCallback | ((scope: this) => Tickable)>} */
this.constructorsOnce = [];
/** @type {Array<ScopeCleanupCallback>} */
this.revertConstructorsOnce = [];
/** @type {Array<Revertible>} */
this.revertiblesOnce = [];
/** @type {Boolean} */
this.once = false;
/** @type {Number} */
this.onceIndex = 0;
/** @type {Record<String, ScopeMethod>} */
this.methods = {};
/** @type {Record<String, Boolean>} */
this.matches = {};
/** @type {Record<String, MediaQueryList>} */
this.mediaQueryLists = {};
/** @type {Record<String, any>} */
this.data = {};
if (mediaQueries) {
for (let mq in mediaQueries) {
const _mq = win.matchMedia(mediaQueries[mq]);
this.mediaQueryLists[mq] = _mq;
_mq.addEventListener('change', this);
}
}
}
/**
* @param {Revertible} revertible
*/
register(revertible) {
const store = this.once ? this.revertiblesOnce : this.revertibles;
store.push(revertible);
}
/**
* @template T
* @param {ScopedCallback<T>} cb
* @return {T}
*/
execute(cb) {
let activeScope = scope.current;
let activeRoot = scope.root;
let activeDefaults = globals.defaults;
scope.current = this;
scope.root = this.root;
globals.defaults = this.defaults;
const mqs = this.mediaQueryLists;
for (let mq in mqs) this.matches[mq] = mqs[mq].matches;
const returned = cb(this);
scope.current = activeScope;
scope.root = activeRoot;
globals.defaults = activeDefaults;
return returned;
}
/**
* @return {this}
*/
refresh() {
this.onceIndex = 0;
this.execute(() => {
let i = this.revertibles.length;
let y = this.revertConstructors.length;
while (i--) this.revertibles[i].revert();
while (y--) this.revertConstructors[y](this);
this.revertibles.length = 0;
this.revertConstructors.length = 0;
this.constructors.forEach((/** @type {ScopeConstructorCallback} */constructor) => {
const revertConstructor = constructor(this);
if (isFnc(revertConstructor)) {
this.revertConstructors.push(revertConstructor);
}
});
});
return this;
}
/**
* @overload
* @param {String} a1
* @param {ScopeMethod} a2
* @return {this}
*
* @overload
* @param {ScopeConstructorCallback} a1
* @return {this}
*
* @param {String|ScopeConstructorCallback} a1
* @param {ScopeMethod} [a2]
*/
add(a1, a2) {
this.once = false;
if (isFnc(a1)) {
const constructor = /** @type {ScopeConstructorCallback} */(a1);
this.constructors.push(constructor);
this.execute(() => {
const revertConstructor = constructor(this);
if (isFnc(revertConstructor)) {
this.revertConstructors.push(revertConstructor);
}
});
} else {
this.methods[/** @type {String} */(a1)] = (/** @type {any} */...args) => this.execute(() => a2(...args));
}
return this;
}
/**
* @param {ScopeConstructorCallback} scopeConstructorCallback
* @return {this}
*/
addOnce(scopeConstructorCallback) {
this.once = true;
if (isFnc(scopeConstructorCallback)) {
const currentIndex = this.onceIndex++;
const tracked = this.constructorsOnce[currentIndex];
if (tracked) return this;
const constructor = /** @type {ScopeConstructorCallback} */(scopeConstructorCallback);
this.constructorsOnce[currentIndex] = constructor;
this.execute(() => {
const revertConstructor = constructor(this);
if (isFnc(revertConstructor)) {
this.revertConstructorsOnce.push(revertConstructor);
}
});
}
return this;
}
/**
* @param {(scope: this) => Tickable} cb
* @return {Tickable}
*/
keepTime(cb) {
this.once = true;
const currentIndex = this.onceIndex++;
const tracked = /** @type {(scope: this) => Tickable} */(this.constructorsOnce[currentIndex]);
if (isFnc(tracked)) return tracked(this);
const constructor = /** @type {(scope: this) => Tickable} */(createRefreshable(cb));
this.constructorsOnce[currentIndex] = constructor;
let trackedTickable;
this.execute(() => {
trackedTickable = constructor(this);
});
return trackedTickable;
}
/**
* @param {Event} e
*/
handleEvent(e) {
switch (e.type) {
case 'change':
this.refresh();
break;
}
}
revert() {
const revertibles = this.revertibles;
const revertConstructors = this.revertConstructors;
const revertiblesOnce = this.revertiblesOnce;
const revertConstructorsOnce = this.revertConstructorsOnce;
const mqs = this.mediaQueryLists;
let i = revertibles.length;
let j = revertConstructors.length;
let k = revertiblesOnce.length;
let l = revertConstructorsOnce.length;
while (i--) revertibles[i].revert();
while (j--) revertConstructors[j](this);
while (k--) revertiblesOnce[k].revert();
while (l--) revertConstructorsOnce[l](this);
for (let mq in mqs) mqs[mq].removeEventListener('change', this);
revertibles.length = 0;
revertConstructors.length = 0;
this.constructors.length = 0;
revertiblesOnce.length = 0;
revertConstructorsOnce.length = 0;
this.constructorsOnce.length = 0;
this.onceIndex = 0;
this.matches = {};
this.methods = {};
this.mediaQueryLists = {};
this.data = {};
}
}
/**
* @param {ScopeParams} [params]
* @return {Scope}
*/
const createScope = params => new Scope(params);
/**
* @typedef {String|Number} ScrollThresholdValue
*/
/**
* @return {Number}
*/
const getMaxViewHeight = () => {
const $el = doc.createElement('div');
doc.body.appendChild($el);
$el.style.height = '100lvh';
const height = $el.offsetHeight;
doc.body.removeChild($el);
return height;
};
/**
* @template {ScrollThresholdValue|String|Number|Boolean|Function|Object} T
* @param {T | ((observer: ScrollObserver) => T)} value
* @param {ScrollObserver} scroller
* @return {T}
*/
const parseScrollObserverFunctionParameter = (value, scroller) => value && isFnc(value) ? /** @type {Function} */(value)(scroller) : value;
const scrollContainers = new Map();
class ScrollContainer {
/**
* @param {HTMLElement} $el
*/
constructor($el) {
/** @type {HTMLElement} */
this.element = $el;
/** @type {Boolean} */
this.useWin = this.element === doc.body;
/** @type {Number} */
this.winWidth = 0;
/** @type {Number} */
this.winHeight = 0;
/** @type {Number} */
this.width = 0;
/** @type {Number} */
this.height = 0;
/** @type {Number} */
this.left = 0;
/** @type {Number} */
this.top = 0;
/** @type {Number} */
this.zIndex = 0;
/** @type {Number} */
this.scrollX = 0;
/** @type {Number} */
this.scrollY = 0;
/** @type {Number} */
this.prevScrollX = 0;
/** @type {Number} */
this.prevScrollY = 0;
/** @type {Number} */
this.scrollWidth = 0;
/** @type {Number} */
this.scrollHeight = 0;
/** @type {Number} */
this.velocity = 0;
/** @type {Boolean} */
this.backwardX = false;
/** @type {Boolean} */
this.backwardY = false;
/** @type {Timer} */
this.scrollTicker = new Timer({
autoplay: false,
onBegin: () => this.dataTimer.resume(),
onUpdate: () => {
const backwards = this.backwardX || this.backwardY;
forEachChildren(this, (/** @type {ScrollObserver} */child) => child.handleScroll(), backwards);
},
onComplete: () => this.dataTimer.pause()
}).init();
/** @type {Timer} */
this.dataTimer = new Timer({
autoplay: false,
frameRate: 30,
onUpdate: self => {
const dt = self.deltaTime;
const px = this.prevScrollX;
const py = this.prevScrollY;
const nx = this.scrollX;
const ny = this.scrollY;
const dx = px - nx;
const dy = py - ny;
this.prevScrollX = nx;
this.prevScrollY = ny;
if (dx) this.backwardX = px > nx;
if (dy) this.backwardY = py > ny;
this.velocity = round(dt > 0 ? Math.sqrt(dx * dx + dy * dy) / dt : 0, 5);
}
}).init();
/** @type {Timer} */
this.resizeTicker = new Timer({
autoplay: false,
duration: 250 * globals.timeScale,
onComplete: () => {
this.updateWindowBounds();
this.refreshScrollObservers();
this.handleScroll();
}
}).init();
/** @type {Timer} */
this.wakeTicker = new Timer({
autoplay: false,
duration: 500 * globals.timeScale,
onBegin: () => {
this.scrollTicker.resume();
},
onComplete: () => {
this.scrollTicker.pause();
}
}).init();
/** @type {ScrollObserver} */
this._head = null;
/** @type {ScrollObserver} */
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 ? win : this.element).addEventListener('scroll', this, false);
}
updateScrollCoords() {
const useWin = this.useWin;
const $el = this.element;
this.scrollX = round(useWin ? win.scrollX : $el.scrollLeft, 0);
this.scrollY = round(useWin ? win.scrollY : $el.scrollTop, 0);
}
updateWindowBounds() {
this.winWidth = win.innerWidth;
this.winHeight = getMaxViewHeight();
}
updateBounds() {
const style = getComputedStyle(this.element);
const $el = this.element;
this.scrollWidth = $el.scrollWidth + parseFloat(style.marginLeft) + parseFloat(style.marginRight);
this.scrollHeight = $el.scrollHeight + parseFloat(style.marginTop) + parseFloat(style.marginBottom);
this.updateWindowBounds();
let width, height;
if (this.useWin) {
width = this.winWidth;
height = this.winHeight;
} else {
const elRect = $el.getBoundingClientRect();
width = elRect.width;
height = elRect.height;
this.top = elRect.top;
this.left = elRect.left;
}
this.width = width;
this.height = height;
}
refreshScrollObservers() {
forEachChildren(this, (/** @type {ScrollObserver} */child) => {
if (child._debug) {
child.removeDebug();
}
});
this.updateBounds();
forEachChildren(this, (/** @type {ScrollObserver} */child) => {
child.refresh();
if (child._debug) {
child.debug();
}
});
}
refresh() {
this.updateWindowBounds();
this.updateBounds();
this.refreshScrollObservers();
this.handleScroll();
}
handleScroll() {
this.updateScrollCoords();
this.wakeTicker.restart();
}
/**
* @param {Event} e
*/
handleEvent(e) {
switch (e.type) {
case 'scroll':
this.handleScroll();
break;
}
}
revert() {
this.scrollTicker.cancel();
this.dataTimer.cancel();
this.resizeTicker.cancel();
this.wakeTicker.cancel();
this.resizeObserver.disconnect();
(this.useWin ? win : this.element).removeEventListener('scroll', this);
scrollContainers.delete(this.element);
}
}
/**
* @param {TargetsParam} target
* @return {ScrollContainer}
*/
const registerAndGetScrollContainer = target => {
const $el = /** @type {HTMLElement} */(target ? parseTargets(target)[0] || doc.body : doc.body);
let scrollContainer = scrollContainers.get($el);
if (!scrollContainer) {
scrollContainer = new ScrollContainer($el);
scrollContainers.set($el, scrollContainer);
}
return scrollContainer;
};
/**
* @param {HTMLElement} $el
* @param {Number|string} v
* @param {Number} size
* @param {Number} [under]
* @param {Number} [over]
* @return {Number}
*/
const convertValueToPx = ($el, v, size, under, over) => {
const clampMin = v === 'min';
const clampMax = v === 'max';
const value = v === 'top' || v === 'left' || v === 'start' || clampMin ? 0 :
v === 'bottom' || v === 'right' || v === 'end' || clampMax ? '100%' :
v === 'center' ? '50%' :
v;
const { n, u } = decomposeRawValue(value, decomposedOriginalValue);
let px = n;
if (u === '%') {
px = (n / 100) * size;
} else if (u) {
px = convertValueUnit($el, decomposedOriginalValue, 'px', true).n;
}
if (clampMax && under < 0) px += under;
if (clampMin && over > 0) px += over;
return px;
};
/**
* @param {HTMLElement} $el
* @param {ScrollThresholdValue} v
* @param {Number} size
* @param {Number} [under]
* @param {Number} [over]
* @return {Number}
*/
const parseBoundValue = ($el, v, size, under, over) => {
/** @type {Number} */
let value;
if (isStr(v)) {
const matchedOperator = relativeValuesExecRgx.exec(/** @type {String} */(v));
if (matchedOperator) {
const splitter = matchedOperator[0];
const operator = splitter[0];
const splitted = /** @type {String} */(v).split(splitter);
const clampMin = splitted[0] === 'min';
const clampMax = splitted[0] === 'max';
const valueAPx = convertValueToPx($el, splitted[0], size, under, over);
const valueBPx = convertValueToPx($el, splitted[1], size, under, over);
if (clampMin) {
const min = getRelativeValue(convertValueToPx($el, 'min', size), valueBPx, operator);
value = min < valueAPx ? valueAPx : min;
} else if (clampMax) {
const max = getRelativeValue(convertValueToPx($el, 'max', size), valueBPx, operator);
value = max > valueAPx ? valueAPx : max;
} else {
value = getRelativeValue(valueAPx, valueBPx, operator);
}
} else {
value = convertValueToPx($el, v, size, under, over);
}
} else {
value = /** @type {Number} */(v);
}
return round(value, 0);
};
/**
* @param {JSAnimation} linked
* @return {HTMLElement}
*/
const getAnimationDomTarget = linked => {
let $linkedTarget;
const linkedTargets = linked.targets;
for (let i = 0, l = linkedTargets.length; i < l; i++) {
const target = linkedTargets[i];
if (target[isDomSymbol]) {
$linkedTarget = /** @type {HTMLElement} */(target);
break;
}
}
return $linkedTarget;
};
let scrollerIndex = 0;
const debugColors$1 = ['#FF4B4B','#FF971B','#FFC730','#F9F640','#7AFF5A','#18FF74','#17E09B','#3CFFEC','#05DBE9','#33B3F1','#638CF9','#C563FE','#FF4FCF','#F93F8A'];
/**
* @typedef {Object} ScrollThresholdParam
* @property {ScrollThresholdValue} [target]
* @property {ScrollThresholdValue} [container]
*/
/**
* @callback ScrollObserverAxisCallback
* @param {ScrollObserver} self
* @return {'x'|'y'}
*/
/**
* @callback ScrollThresholdCallback
* @param {ScrollObserver} self
* @return {ScrollThresholdValue|ScrollThresholdParam}
*/
/**
* @typedef {Object} ScrollObserverParams
* @property {Number|String} [id]
* @property {Boolean|Number|String|EasingParam} [sync]
* @property {TargetsParam} [container]
* @property {TargetsParam} [target]
* @property {'x'|'y'|ScrollObserverAxisCallback|((observer: ScrollObserver) => 'x'|'y'|ScrollObserverAxisCallback)} [axis]
* @property {ScrollThresholdValue|ScrollThresholdParam|ScrollThresholdCallback|((observer: ScrollObserver) => ScrollThresholdValue|ScrollThresholdParam|ScrollThresholdCallback)} [enter]
* @property {ScrollThresholdValue|ScrollThresholdParam|ScrollThresholdCallback|((observer: ScrollObserver) => ScrollThresholdValue|ScrollThresholdParam|ScrollThresholdCallback)} [leave]
* @property {Boolean|((observer: ScrollObserver) => Boolean)} [repeat]
* @property {Boolean} [debug]
* @property {Callback<ScrollObserver>} [onEnter]
* @property {Callback<ScrollObserver>} [onLeave]
* @property {Callback<ScrollObserver>} [onEnterForward]
* @property {Callback<ScrollObserver>} [onLeaveForward]
* @property {Callback<ScrollObserver>} [onEnterBackward]
* @property {Callback<ScrollObserver>} [onLeaveBackward]
* @property {Callback<ScrollObserver>} [onUpdate]
* @property {Callback<ScrollObserver>} [onSyncComplete]
*/
class ScrollObserver {
/**
* @param {ScrollObserverParams} parameters
*/
constructor(parameters = {}) {
if (scope.current) scope.current.register(this);
const syncMode = setValue(parameters.sync, 'play pause');
const ease = syncMode ? parseEasings(/** @type {EasingParam} */(syncMode)) : null;
const isLinear = syncMode && (syncMode === 'linear' || syncMode === none);
const isEase = syncMode && !(ease === none && !isLinear);
const isSmooth = syncMode && (isNum(syncMode) || syncMode === true || isLinear);
const isMethods = syncMode && (isStr(syncMode) && !isEase && !isSmooth);
const syncMethods = isMethods ? /** @type {String} */(syncMode).split(' ').map(
(/** @type {String} */m) => () => {
const linked = this.linked;
return linked && linked[m] ? linked[m]() : null;
}
) : null;
const biDirSync = isMethods && syncMethods.length > 2;
/** @type {Number} */
this.index = scrollerIndex++;
/** @type {String|Number} */
this.id = !isUnd(parameters.id) ? parameters.id : this.index;
/** @type {ScrollContainer} */
this.container = registerAndGetScrollContainer(parameters.container);
/** @type {HTMLElement} */
this.target = null;
/** @type {Tickable|WAAPIAnimation} */
this.linked = null;
/** @type {Boolean} */
this.repeat = null;
/** @type {Boolean} */
this.horizontal = null;
/** @type {ScrollThresholdParam|ScrollThresholdValue|ScrollThresholdCallback} */
this.enter = null;
/** @type {ScrollThresholdParam|ScrollThresholdValue|ScrollThresholdCallback} */
this.leave = null;
/** @type {Boolean} */
this.sync = isEase || isSmooth || !!syncMethods;
/** @type {EasingFunction} */
this.syncEase = isEase ? ease : null;
/** @type {Number} */
this.syncSmooth = isSmooth ? syncMode === true || isLinear ? 1 : /** @type {Number} */(syncMode) : null;
/** @type {Callback<ScrollObserver>} */
this.onSyncEnter = syncMethods && !biDirSync && syncMethods[0] ? syncMethods[0] : noop;
/** @type {Callback<ScrollObserver>} */
this.onSyncLeave = syncMethods && !biDirSync && syncMethods[1] ? syncMethods[1] : noop;
/** @type {Callback<ScrollObserver>} */
this.onSyncEnterForward = syncMethods && biDirSync && syncMethods[0] ? syncMethods[0] : noop;
/** @type {Callback<ScrollObserver>} */
this.onSyncLeaveForward = syncMethods && biDirSync && syncMethods[1] ? syncMethods[1] : noop;
/** @type {Callback<ScrollObserver>} */
this.onSyncEnterBackward = syncMethods && biDirSync && syncMethods[2] ? syncMethods[2] : noop;
/** @type {Callback<ScrollObserver>} */
this.onSyncLeaveBackward = syncMethods && biDirSync && syncMethods[3] ? syncMethods[3] : noop;
/** @type {Callback<ScrollObserver>} */
this.onEnter = parameters.onEnter || noop;
/** @type {Callback<ScrollObserver>} */
this.onLeave = parameters.onLeave || noop;
/** @type {Callback<ScrollObserver>} */
this.onEnterForward = parameters.onEnterForward || noop;
/** @type {Callback<ScrollObserver>} */
this.onLeaveForward = parameters.onLeaveForward || noop;
/** @type {Callback<ScrollObserver>} */
this.onEnterBackward = parameters.onEnterBackward || noop;
/** @type {Callback<ScrollObserver>} */
this.onLeaveBackward = parameters.onLeaveBackward || noop;
/** @type {Callback<ScrollObserver>} */
this.onUpdate = parameters.onUpdate || noop;
/** @type {Callback<ScrollObserver>} */
this.onSyncComplete = parameters.onSyncComplete || noop;
/** @type {Boolean} */
this.reverted = false;
/** @type {Boolean} */
this.completed = false;
/** @type {Boolean} */
this.began = false;
/** @type {Boolean} */
this.isInView = false;
/** @type {Boolean} */
this.forceEnter = false;
/** @type {Boolean} */
this.hasEntered = false;
// /** @type {Array.<Number>} */
// this.offsets = [];
/** @type {Number} */
this.offset = 0;
/** @type {Number} */
this.offsetStart = 0;
/** @type {Number} */
this.offsetEnd = 0;
/** @type {Number} */
this.distance = 0;
/** @type {Number} */
this.prevProgress = 0;
/** @type {Array} */
this.thresholds = ['start', 'end', 'end', 'start'];
/** @type {[Number, Number, Number, Number]} */
this.coords = [0, 0, 0, 0];
/** @type {JSAnimation} */
this.debugStyles = null;
/** @type {HTMLElement} */
this.$debug = null;
/** @type {ScrollObserverParams} */
this._params = parameters;
/** @type {Boolean} */
this._debug = setValue(parameters.debug, false);
/** @type {ScrollObserver} */
this._next = null;
/** @type {ScrollObserver} */
this._prev = null;
addChild(this.container, this);
// Wait for the next frame to add to the container in order to handle calls to link()
sync(() => {
if (this.reverted) return;
if (!this.target) {
const target = /** @type {HTMLElement} */(parseTargets(parameters.target)[0]);
this.target = target || doc.body;
this.refresh();
}
if (this._debug) this.debug();
});
}
/**
* @param {Tickable|WAAPIAnimation} linked
*/
link(linked) {
if (linked) {
// Make sure to pause the linked object in case it's added later
linked.pause();
this.linked = linked;
// Try to use a target of the linked object if no target parameters specified
if (!this._params.target) {
/** @type {HTMLElement} */
let $linkedTarget;
if (!isUnd(/** @type {JSAnimation} */(linked).targets)) {
$linkedTarget = getAnimationDomTarget(/** @type {JSAnimation} */(linked));
} else {
forEachChildren(/** @type {Timeline} */(linked), (/** @type {JSAnimation} */child) => {
if (child.targets && !$linkedTarget) {
$linkedTarget = getAnimationDomTarget(/** @type {JSAnimation} */(child));
}
});
}
// Fallback to body if no target found
this.target = $linkedTarget || doc.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 p = (this.scroll - this.offsetStart) / this.distance;
return p === Infinity || isNaN(p) ? 0 : round(clamp(p, 0, 1), 6);
}
refresh() {
this.reverted = false;
const params = this._params;
this.repeat = setValue(parseScrollObserverFunctionParameter(params.repeat, this), true);
this.horizontal = setValue(parseScrollObserverFunctionParameter(params.axis, this), 'y') === 'x';
this.enter = setValue(parseScrollObserverFunctionParameter(params.enter, this), 'end start');
this.leave = setValue(parseScrollObserverFunctionParameter(params.leave, this), 'start end');
this.updateBounds();
this.handleScroll();
return this;
}
removeDebug() {
if (this.$debug) {
this.$debug.parentNode.removeChild(this.$debug);
this.$debug = null;
}
if (this.debugStyles) {
this.debugStyles.revert();
this.$debug = null;
}
return this;
}
debug() {
this.removeDebug();
const container = this.container;
const isHori = this.horizontal;
const $existingDebug = container.element.querySelector(':scope > .animejs-onscroll-debug');
const $debug = doc.createElement('div');
const $thresholds = doc.createElement('div');
const $triggers = doc.createElement('div');
const color = debugColors$1[this.index % debugColors$1.length];
const useWin = container.useWin;
const containerWidth = useWin ? container.winWidth : container.width;
const containerHeight = useWin ? container.winHeight : container.height;
const scrollWidth = container.scrollWidth;
const scrollHeight = container.scrollHeight;
const size = this.container.width > 360 ? 320 : 260;
const offLeft = isHori ? 0 : 10;
const offTop = isHori ? 10 : 0;
const half = isHori ? 24 : size / 2;
const labelHeight = isHori ? half : 15;
const labelWidth = isHori ? 60 : half;
const labelSize = isHori ? labelWidth : labelHeight;
const repeat = isHori ? 'repeat-x' : 'repeat-y';
/**
* @param {Number} v
* @return {String}
*/
const gradientOffset = v => isHori ? '0px '+(v)+'px' : (v)+'px'+' 2px';
/**
* @param {String} c
* @return {String}
*/
const lineCSS = (c) => `linear-gradient(${isHori ? 90 : 0}deg, ${c} 2px, transparent 1px)`;
/**
* @param {String} p
* @param {Number} l
* @param {Number} t
* @param {Number} w
* @param {Number} h
* @return {String}
*/
const baseCSS = (p, l, t, w, h) => `position:${p};left:${l}px;top:${t}px;width:${w}px;height:${h}px;`;
$debug.style.cssText = `${baseCSS('absolute', offLeft, offTop, isHori ? scrollWidth : size, isHori ? size : scrollHeight)}
pointer-events: none;
z-index: ${this.container.zIndex++};
display: flex;
flex-direction: ${isHori ? 'column' : 'row'};
filter: drop-shadow(0px 1px 0px rgba(0,0,0,.75));
`;
$thresholds.style.cssText = `${baseCSS('sticky', 0, 0, isHori ? containerWidth : half, isHori ? half : containerHeight)}`;
if (!$existingDebug) {
$thresholds.style.cssText += `background:
${lineCSS('#FFFF')}${gradientOffset(half-10)} / ${isHori ? '100px 100px' : '100px 100px'} ${repeat},
${lineCSS('#FFF8')}${gradientOffset(half-10)} / ${isHori ? '10px 10px' : '10px 10px'} ${repeat};
`;
}
$triggers.style.cssText = `${baseCSS('relative', 0, 0, isHori ? scrollWidth : half, isHori ? half : scrollHeight)}`;
if (!$existingDebug) {
$triggers.style.cssText += `background:
${lineCSS('#FFFF')}${gradientOffset(0)} / ${isHori ? '100px 10px' : '10px 100px'} ${repeat},
${lineCSS('#FFF8')}${gradientOffset(0)} / ${isHori ? '10px 0px' : '0px 10px'} ${repeat};
`;
}
const labels = [' enter: ', ' leave: '];
this.coords.forEach((v, i) => {
const isView = i > 1;
const value = (isView ? 0 : this.offset) + v;
const isTail = i % 2;
const isFirst = value < labelSize;
const isOver = value > (isView ? isHori ? containerWidth : containerHeight : isHori ? scrollWidth : scrollHeight) - labelSize;
const isFlip = (isView ? isTail && !isFirst : !isTail && !isFirst) || isOver;
const $label = doc.createElement('div');
const $text = doc.createElement('div');
const dirProp = isHori ? isFlip ? 'right' : 'left' : isFlip ? 'bottom' : 'top';
const flipOffset = isFlip ? (isHori ? labelWidth : labelHeight) + (!isView ? isHori ? -1 : -2 : isHori ? -1 : isOver ? 0 : -2) : !isView ? isHori ? 1 : 0 : isHori ? 1 : 0;
// $text.innerHTML = `${!isView ? '' : labels[isTail] + ' '}${this.id}: ${this.thresholds[i]} ${isView ? '' : labels[isTail]}`;
$text.innerHTML = `${this.id}${labels[isTail]}${this.thresholds[i]}`;
$label.style.cssText = `${baseCSS('absolute', 0, 0, labelWidth, labelHeight)}
display: flex;
flex-direction: ${isHori ? 'column' : 'row'};
justify-content: flex-${isView ? 'start' : 'end'};
align-items: flex-${isFlip ? 'end' : 'start'};
border-${dirProp}: 2px ${isTail ? 'solid' : 'solid'} ${color};
`;
$text.style.cssText = `
overflow: hidden;
max-width: ${(size / 2) - 10}px;
height: ${labelHeight};
margin-${isHori ? isFlip ? 'right' : 'left' : isFlip ? 'bottom' : 'top'}: -2px;
padding: 1px;
font-family: ui-monospace, monospace;
font-size: 10px;
letter-spacing: -.025em;
line-height: 9px;
font-weight: 600;
text-align: ${isHori && isFlip || !isHori && !isView ? 'right' : 'left'};
white-space: pre;
text-overflow: ellipsis;
color: ${isTail ? color : 'rgba(0,0,0,.75)'};
background-color: ${isTail ? 'rgba(0,0,0,.65)' : color};
border: 2px solid ${isTail ? color : 'transparent'};
border-${isHori ? isFlip ? 'top-left' : 'top-right' : isFlip ? 'top-left' : 'bottom-left'}-radius: 5px;
border-${isHori ? isFlip ? 'bottom-left' : 'bottom-right' : isFlip ? 'top-right' : 'bottom-right'}-radius: 5px;
`;
$label.appendChild($text);
let position = value - flipOffset + (isHori ? 1 : 0);
$label.style[isHori ? 'left' : 'top'] = `${position}px`;
// $label.style[isHori ? 'left' : 'top'] = value - flipOffset + (!isFlip && isFirst && !isView ? 1 : isFlip ? 0 : -2) + 'px';
(isView ? $thresholds : $triggers).appendChild($label);
});
$debug.appendChild($thresholds);
$debug.appendChild($triggers);
container.element.appendChild($debug);
if (!$existingDebug) $debug.classList.add('animejs-onscroll-debug');
this.$debug = $debug;
const containerPosition = getTargetValue(container.element, 'position');
if (containerPosition === 'static') {
this.debugStyles = setTargetValues(container.element, { position: 'relative '});
}
}
updateBounds() {
if (this._debug) {
this.removeDebug();
}
let stickys;
const $target = this.target;
const container = this.container;
const isHori = this.horizontal;
const linked = this.linked;
let linkedTime;
let $el = $target;
// 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
// const isContainerStatic = getTargetValue(container.element, 'position') === 'static' ? setTargetValues(container.element, { position: 'relative '}) : false;
// while ($el && $el !== container.element && $el !== doc.body) {
// const isSticky = getTargetValue($el, 'position') === 'sticky' ?
// setTargetValues($el, { position: 'static' }) :
// false;
// if ($el === $offsetParent) {
// offsetX += $el.offsetLeft || 0;
// offsetY += $el.offsetTop || 0;
// $offsetParent = $el.offsetParent;
// }
// $el = /** @type {HTMLElement} */($el.parentElement);
// if (isSticky) {
// if (!stickys) stickys = [];
// stickys.push(isSticky);
// }
// }
// if (isContainerStatic) isContainerStatic.revert();
// const offset = isHori ? offsetX : offsetY;
// const targetSize = isHori ? $target.offsetWidth : $target.offsetHeight;
while ($el && $el !== container.element && $el !== doc.body) {
const isSticky = getTargetValue($el, 'position') === 'sticky' ? setTargetValues($el, { position: 'static' }) : false;
$el = $el.parentElement;
if (isSticky) {
if (!stickys) stickys = [];
stickys.push(isSticky);
}
}
const rect = $target.getBoundingClientRect();
const offset = isHori ? rect.left + container.scrollX - container.left : rect.top + container.scrollY - container.top;
const targetSize = isHori ? rect.width : rect.height;
const containerSize = isHori ? container.width : container.height;
const scrollSize = isHori ? container.scrollWidth : container.scrollHeight;
const maxScroll = scrollSize - containerSize;
const enter = this.enter;
const leave = this.leave;
/** @type {ScrollThresholdValue} */
let enterTarget = 'start';
/** @type {ScrollThresholdValue} */
let leaveTarget = 'end';
/** @type {ScrollThresholdValue} */
let enterContainer = 'end';
/** @type {ScrollThresholdValue} */
let leaveContainer = 'start';
if (isStr(enter)) {
const splitted = /** @type {String} */(enter).split(' ');
enterContainer = splitted[0];
enterTarget = splitted.length > 1 ? splitted[1] : enterTarget;
} else if (isObj(enter)) {
const e = /** @type {ScrollThresholdParam} */(enter);
if (!isUnd(e.container)) enterContainer = e.container;
if (!isUnd(e.target)) enterTarget = e.target;
} else if (isNum(enter)) {
enterContainer = /** @type {Number} */(enter);
}
if (isStr(leave)) {
const splitted = /** @type {String} */(leave).split(' ');
leaveContainer = splitted[0];
leaveTarget = splitted.length > 1 ? splitted[1] : leaveTarget;
} else if (isObj(leave)) {
const t = /** @type {ScrollThresholdParam} */(leave);
if (!isUnd(t.container)) leaveContainer = t.container;
if (!isUnd(t.target)) leaveTarget = t.target;
} else if (isNum(leave)) {
leaveContainer = /** @type {Number} */(leave);
}
const parsedEnterTarget = parseBoundValue($target, enterTarget, targetSize);
const parsedLeaveTarget = parseBoundValue($target, leaveTarget, targetSize);
const under = (parsedEnterTarget + offset) - containerSize;
const over = (parsedLeaveTarget + offset) - maxScroll;
const parsedEnterContainer = parseBoundValue($target, enterContainer, containerSize, under, over);
const parsedLeaveContainer = parseBoundValue($target, leaveContainer, containerSize, under, over);
const offsetStart = parsedEnterTarget + offset - parsedEnterContainer;
const offsetEnd = parsedLeaveTarget + offset - parsedLeaveContainer;
const scrollDelta = offsetEnd - offsetStart;
// this.offsets[0] = offsetX;
// this.offsets[1] = offsetY;
this.offset = offset;
this.offsetStart = offsetStart;
this.offsetEnd = offsetEnd;
this.distance = scrollDelta <= 0 ? 0 : scrollDelta;
this.thresholds = [enterTarget, leaveTarget, enterContainer, leaveContainer];
this.coords = [parsedEnterTarget, parsedLeaveTarget, parsedEnterContainer, parsedLeaveContainer];
if (stickys) {
stickys.forEach(sticky => sticky.revert());
}
if (linked) {
linked.seek(linkedTime, true);
}
if (this._debug) {
this.debug();
}
}
handleScroll() {
const linked = this.linked;
const sync = this.sync;
const syncEase = this.syncEase;
const syncSmooth = this.syncSmooth;
const shouldSeek = linked && (syncEase || syncSmooth);
const isHori = this.horizontal;
const container = this.container;
const scroll = this.scroll;
const isBefore = scroll <= this.offsetStart;
const isAfter = scroll >= this.offsetEnd;
const isInView = !isBefore && !isAfter;
const isOnTheEdge = scroll === this.offsetStart || scroll === this.offsetEnd;
const forceEnter = !this.hasEntered && isOnTheEdge;
const $debug = this._debug && this.$debug;
let hasUpdated = false;
let syncCompleted = false;
let p = this.progress;
if (isBefore && this.began) {
this.began = false;
}
if (p > 0 && !this.began) {
this.began = true;
}
if (shouldSeek) {
const lp = linked.progress;
if (syncSmooth && isNum(syncSmooth)) {
if (/** @type {Number} */(syncSmooth) < 1) {
const step = 0.0001;
const snap = lp < p && p === 1 ? step : lp > p && !p ? -1e-4 : 0;
p = round(lerp(lp, p, interpolate(.01, .2, /** @type {Number} */(syncSmooth)), false) + snap, 6);
}
} else if (syncEase) {
p = syncEase(p);
}
hasUpdated = p !== this.prevProgress;
syncCompleted = lp === 1;
if (hasUpdated && !syncCompleted && (syncSmooth && lp)) {
container.wakeTicker.restart();
}
}
if ($debug) {
const sticky = isHori ? container.scrollY : container.scrollX;
$debug.style[isHori ? 'top' : 'left'] = sticky + 10 + 'px';
}
// Trigger enter callbacks if already in view or when entering the view
if ((isInView && !this.isInView) || (forceEnter && !this.forceEnter && !this.hasEntered)) {
if (isInView) this.isInView = true;
if (!this.forceEnter || !this.hasEntered) {
if ($debug && isInView) $debug.style.zIndex = `${this.container.zIndex++}`;
this.onSyncEnter(this);
this.onEnter(this);
if (this.backward) {
this.onSyncEnterBackward(this);
this.onEnterBackward(this);
} else {
this.onSyncEnterForward(this);
this.onEnterForward(this);
}
this.hasEntered = true;
if (forceEnter) this.forceEnter = true;
} else if (isInView) {
this.forceEnter = false;
}
}
if (isInView || !isInView && this.isInView) {
hasUpdated = true;
}
if (hasUpdated) {
if (shouldSeek) linked.seek(linked.duration * p);
this.onUpdate(this);
}
if (!isInView && this.isInView) {
this.isInView = false;
this.onSyncLeave(this);
this.onLeave(this);
if (this.backward) {
this.onSyncLeaveBackward(this);
this.onLeaveBackward(this);
} else {
this.onSyncLeaveForward(this);
this.onLeaveForward(this);
}
if (sync && !syncSmooth) {
syncCompleted = true;
}
}
if (p >= 1 && this.began && !this.completed && (sync && syncCompleted || !sync)) {
if (sync) {
this.onSyncComplete(this);
}
this.completed = true;
if ((!this.repeat && !linked) || (!this.repeat && linked && linked.completed)) {
this.revert();
}
}
if (p < 1 && this.completed) {
this.completed = false;
}
this.prevProgress = p;
}
revert() {
if (this.reverted) return;
const container = this.container;
removeChild(container, this);
if (!container._head) {
container.revert();
}
if (this._debug) {
this.removeDebug();
}
this.reverted = true;
return this;
}
}
/**
* @param {ScrollObserverParams} [parameters={}]
* @return {ScrollObserver}
*/
const onScroll = (parameters = {}) => new ScrollObserver(parameters);
const segmenter = !isUnd(Intl) && 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>} 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<HTMLElement>}
*/
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<HTMLElement>} bin
* @returns {Set<HTMLElement>}
*/
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}
*/
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 += `<span${overflow ? ` style="overflow:${overflow};"` : ''}>`;
template += `<span${classString}${cloneType ? ` style="position:relative;"` : ''} data-${type}="{i}">`;
if (cloneType) {
const left = cloneType === 'left' ? '-100%' : cloneType === 'right' ? '100%' : '0';
const top = cloneType === 'top' ? '-100%' : cloneType === 'bottom' ? '100%' : '0';
template += `<span>{value}</span>`;
template += `<span inert style="position:absolute;top:${top};left:${left};white-space:nowrap;">{value}</span>`;
} else {
template += `{value}`;
}
template += `</span>`;
if (wrapType) template += `</span>`;
return template;
};
/**
* @param {String|SplitFunctionValue} htmlTemplate
* @param {Array<HTMLElement>} 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, `<i class="${className}"></i>`)
.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<HTMLElement>} */($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 {HTMLElement|NodeList|String|Array<HTMLElement>} 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)} effect
* @return this
*/
addEffect(effect) {
if (!isFnc(effect)) return console.warn('Effect must return a function.');
const refreshableEffect = createRefreshable(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 (!lastWord.includes(' ') && !segment.includes(' ')) {
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<Node>} */([.../** @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 (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);
}
parents.forEach(filterEmptyElements);
for (let cloneIndex = 0, clonesLength = clones.length; cloneIndex < clonesLength; cloneIndex++) {
processHTMLTemplate(lineTemplate, this.lines, clones[cloneIndex], linesFragment, lineType, this.debug, cloneIndex);
}
$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);
}
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;
}
refresh() {
this.split(true);
}
}
/**
* @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
* @param {TextSplitterParams} [parameters]
* @return {TextSplitter}
*/
const split = (target, parameters) => new TextSplitter(target, parameters);
const text = {
split,
};
/**
* @param {Number|String|[Number|String,Number|String]} val
* @param {StaggerParams} params
* @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 ? parseEasings(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(values[staggerIndex], 2)) || 0);
if (params.modifier) output = params.modifier(output);
if (unitMatch) output = `${output}${unitMatch[2]}`;
return output;
}
};
export { Animatable, Draggable, JSAnimation, Scope, ScrollObserver, Spring, TextSplitter, Timeline, Timer, WAAPIAnimation, animate, createAnimatable, createDraggable, createScope, createSpring, createTimeline, createTimer, eases, engine, onScroll, scrollContainers, stagger, svg, text, utils, waapi };