animejs
Version:
JavaScript animation engine
1,507 lines (1,299 loc) • 209 kB
JavaScript
/**
* anime.js GUI - ESM
* @version v4.1.0
* @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|'visible'|'clip'} [wrap]
* @property {Boolean|'top'|'right'|'bottom'|'left'} [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]
* @property {Callback<TextSplitter>} [onLineCalc]
*/
// 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 {Object|Null} */
const win = isBrowser ? window : null;
/** @type {Document} */
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 = new Map();
shortTransforms.set('x', 'translateX');
shortTransforms.set('y', 'translateY');
shortTransforms.set('z', 'translateZ');
const validTransforms = [
'translateX',
'translateY',
'translateZ',
'rotate',
'rotateX',
'rotateY',
'rotateZ',
'scale',
'scaleX',
'scaleY',
'scaleZ',
'skew',
'skewX',
'skewY',
'perspective',
'matrix',
'matrix3d',
];
const transformsFragmentStrings = 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 globals = {
/** @type {DefaultsParams} */
defaults,
/** @type {Document|DOMTarget} */
root: doc,
/** @type {Scope} */
scope: null,
/** @type {Number} */
precision: 4,
/** @type {Number} */
timeScale: 1,
/** @type {Number} */
tickThreshold: 200,
};
const globalVersions = { version: '4.1.0', 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 ceil = Math.ceil;
const floor = Math.floor;
const asin = Math.asin;
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;
};
/*
* 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 && 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 = isBrowser ? requestAnimationFrame : setImmediate;
const engineCancelMethod = 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 parseInlineTran