UNPKG

animejs

Version:

JavaScript animation engine

506 lines (453 loc) 18.2 kB
/** * Anime.js - waapi - ESM * @version v4.3.6 * @license MIT * @copyright 2026 - Julian Garnier */ import { isNil, isUnd, stringStartsWith, isKey, isObj, isArr, isStr, toLowerCase, round, isFnc, isNum } from '../core/helpers.js'; import { scope, globals } from '../core/globals.js'; import { registerTargets } from '../core/targets.js'; import { setValue, getFunctionValue } from '../core/values.js'; import { isBrowser, validTransforms, noop, transformsSymbol, shortTransforms, transformsFragmentStrings, emptyString, K } from '../core/consts.js'; import { none } from '../easings/none.js'; import { parseEaseString } from '../easings/eases/parser.js'; import { addWAAPIAnimation } from './composition.js'; /** * @import { * Callback, * EasingFunction, * EasingParam, * DOMTarget, * DOMTargetsParam, * DOMTargetsArray, * WAAPIAnimationParams, * WAAPITweenOptions, * WAAPIKeyframeValue, * WAAPITweenValue * } from '../types/index.js' */ /** * @import { * Spring, * } from '../easings/spring/index.js' */ /** * @import { * ScrollObserver, * } from '../events/scroll.js' */ /** * Converts an easing function into a valid CSS linear() timing function string * @param {EasingFunction} fn * @param {number} [samples=100] * @returns {string} CSS linear() timing function */ const easingToLinear = (fn, samples = 100) => { const points = []; for (let i = 0; i <= samples; i++) points.push(round(fn(i / samples), 4)); return `linear(${points.join(', ')})`; }; const WAAPIEasesLookups = {}; /** * @param {EasingParam} ease * @return {String} */ const parseWAAPIEasing = (ease) => { let parsedEase = WAAPIEasesLookups[ease]; if (parsedEase) return parsedEase; parsedEase = 'linear'; if (isStr(ease)) { if ( stringStartsWith(ease, 'linear') || stringStartsWith(ease, 'cubic-') || stringStartsWith(ease, 'steps') || stringStartsWith(ease, 'ease') ) { parsedEase = ease; } else if (stringStartsWith(ease, 'cubicB')) { parsedEase = toLowerCase(ease); } else { const parsed = parseEaseString(ease); if (isFnc(parsed)) parsedEase = parsed === none ? 'linear' : easingToLinear(parsed); } // Only cache string based easing name, otherwise function arguments get lost WAAPIEasesLookups[ease] = parsedEase; } else if (isFnc(ease)) { const easing = easingToLinear(ease); if (easing) parsedEase = easing; } else if (/** @type {Spring} */(ease).ease) { parsedEase = easingToLinear(/** @type {Spring} */(ease).ease); } return parsedEase; }; 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; /** * @param {String} propName * @param {WAAPIKeyframeValue} value * @param {DOMTarget} $el * @param {Number} i * @param {Number} targetsLength * @return {String} */ const normalizeTweenValue = (propName, value, $el, i, targetsLength) => { // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, targetsLength); if (!isNum(v)) return v; if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`; if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`; return `${v}`; }; /** * @param {DOMTarget} $el * @param {String} propName * @param {WAAPIKeyframeValue} from * @param {WAAPIKeyframeValue} to * @param {Number} i * @param {Number} targetsLength * @return {WAAPITweenValue} */ const parseIndividualTweenValue = ($el, propName, from, to, i, targetsLength) => { /** @type {WAAPITweenValue} */ let tweenValue = '0'; const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, targetsLength) : getComputedStyle($el)[propName]; if (!isUnd(from)) { const computedFrom = normalizeTweenValue(propName, from, $el, i, targetsLength); tweenValue = [computedFrom, computedTo]; } else { tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, targetsLength)) : computedTo; } return tweenValue; }; 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 autoplay = setValue(params.autoplay, globals.defaults.autoplay); const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false; const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true; const reversed = params.reversed && /** @type {Boolean} */(params.reversed) === true; const loop = setValue(params.loop, globals.defaults.loop); const iterations = /** @type {Number} */((loop === true || loop === Infinity) ? Infinity : isNum(loop) ? loop + 1 : 1); /** @type {PlaybackDirection} */ const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal'; /** @type {FillMode} */ const fill = 'both'; // We use 'both' here because the animation can be reversed during playback const timeScale = (globals.timeScale === 1 ? 1 : K); /** @type {DOMTargetsArray}] */ this.targets = parsedTargets; /** @type {Array<globalThis.Animation>}] */ this.animations = []; /** @type {globalThis.Animation}] */ this.controlAnimation = null; /** @type {Callback<this>} */ this.onComplete = params.onComplete || /** @type {Callback<WAAPIAnimation>} */(/** @type {unknown} */(globals.defaults.onComplete)); /** @type {Number} */ this.duration = 0; /** @type {Boolean} */ this.muteCallbacks = false; /** @type {Boolean} */ this.completed = false; /** @type {Boolean} */ this.paused = !autoplay || scroll !== false; /** @type {Boolean} */ this.reversed = reversed; /** @type {Boolean} */ this.persist = setValue(params.persist, globals.defaults.persist); /** @type {Boolean|ScrollObserver} */ this.autoplay = autoplay; /** @type {Number} */ this._speed = setValue(params.playbackRate, globals.defaults.playbackRate); /** @type {Function} */ this._resolve = noop; // Used by .then() /** @type {Number} */ this._completed = 0; /** @type {Array.<Object>} */ this._inlineStyles = []; parsedTargets.forEach(($el, i) => { const cachedTransforms = $el[transformsSymbol]; const hasIndividualTransforms = validIndividualTransforms.some(t => params.hasOwnProperty(t)); const elStyle = $el.style; const inlineStyles = this._inlineStyles[i] = {}; const easeToParse = setValue(params.ease, globals.defaults.ease); const easeFunctionResult = getFunctionValue(easeToParse, $el, i, targetsLength); const keyEasing = isFnc(easeFunctionResult) || isStr(easeFunctionResult) ? easeFunctionResult : easeToParse; const spring = /** @type {Spring} */(easeToParse).ease && easeToParse; /** @type {String} */ const easing = parseWAAPIEasing(keyEasing); /** @type {Number} */ const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, targetsLength)) * timeScale; /** @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; const styleName = individualTransformProperty ? 'transform' : name; if (!inlineStyles[styleName]) { inlineStyles[styleName] = elStyle[styleName]; } let parsedPropertyValue; if (isObj(propertyValue)) { const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue); const tweenOptionsEase = setValue(tweenOptions.ease, easing); const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase; const to = /** @type {WAAPITweenOptions} */(tweenOptions).to; const from = /** @type {WAAPITweenOptions} */(tweenOptions).from; /** @type {Number} */ tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, 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) { elStyle[name] = keyframes[name][0]; } else { const key = `--${individualTransformProperty}`; elStyle.setProperty(key, keyframes[key][0]); } } } else { parsedPropertyValue = isArr(propertyValue) ? propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, 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})) `; } elStyle.transform = transforms; } }); if (scroll) { /** @type {ScrollObserver} */(this.autoplay).link(this); } } /** * @callback forEachCallback * @param {globalThis.Animation} animation */ /** * @param {forEachCallback|String} callback * @return {this} */ forEach(callback) { try { const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback; this.animations.forEach(cb); } catch {} return this; } get speed() { return this._speed; } set speed(speed) { this._speed = +speed; this.forEach(anim => anim.playbackRate = speed); } get currentTime() { const controlAnimation = this.controlAnimation; const timeScale = globals.timeScale; return this.completed ? this.duration : controlAnimation ? +controlAnimation.currentTime * (timeScale === 1 ? 1 : timeScale) : 0; } set currentTime(time) { const t = time * (globals.timeScale === 1 ? 1 : K); this.forEach(anim => { // Make sure the animation playState is not 'paused' in order to properly trigger an onfinish callback. // The "paused" play state supersedes the "finished" play state; if the animation is both paused and finished, the "paused" state is the one that will be reported. // https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish_event // This is not needed for persisting animations since they never finish. if (!this.persist && t >= this.duration) anim.play(); anim.currentTime = t; }); } get progress() { return this.currentTime / this.duration; } set progress(progress) { this.forEach(anim => anim.currentTime = progress * this.duration || 0); } resume() { if (!this.paused) return this; this.paused = false; // TODO: Store the current time, and seek back to the last position return this.forEach('play'); } pause() { if (this.paused) return this; this.paused = true; return this.forEach('pause'); } alternate() { this.reversed = !this.reversed; this.forEach('reverse'); if (this.paused) this.forEach('pause'); return this; } play() { if (this.reversed) this.alternate(); return this.resume(); } reverse() { if (!this.reversed) this.alternate(); return this.resume(); } /** * @param {Number} time * @param {Boolean} muteCallbacks */ seek(time, muteCallbacks = false) { if (muteCallbacks) this.muteCallbacks = true; if (time < this.duration) this.completed = false; this.currentTime = time; this.muteCallbacks = false; if (this.paused) this.pause(); return this; } restart() { this.completed = false; return this.seek(0, true).resume(); } commitStyles() { return this.forEach('commitStyles'); } complete() { return this.seek(this.duration); } cancel() { this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise this.commitStyles().forEach('cancel'); this.animations.length = 0; // Needed to release all animations from memory requestAnimationFrame(() => { this.targets.forEach(($el) => { // Needed to avoid unecessary inline transorms if ($el.style.transform === 'none') $el.style.removeProperty('transform'); }); }); return this; } revert() { // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted, // This means if you have multiple animations animating different transforms on the same target, // reverting one of them will also override the transform property of the other animations. // A better approach would be to store the original custom property values if they exist instead of the entire transform value, // and update the CSS variables with the orignal value this.cancel().targets.forEach(($el, i) => { const targetStyle = $el.style; const targetInlineStyles = this._inlineStyles[i]; for (let name in targetInlineStyles) { const originalInlinedValue = targetInlineStyles[name]; if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) { targetStyle.removeProperty(toLowerCase(name)); } else { $el.style[name] = originalInlinedValue; } } // Remove style attribute if empty if ($el.getAttribute('style') === emptyString) $el.removeAttribute('style'); }); return this; } /** * @typedef {this & {then: null}} ResolvedWAAPIAnimation */ /** * @param {Callback<ResolvedWAAPIAnimation>} [callback] * @return Promise<this> */ then(callback = noop) { const then = this.then; const onResolve = () => { this.then = null; callback(/** @type {ResolvedWAAPIAnimation} */(this)); this.then = then; this._resolve = noop; }; return new Promise(r => { this._resolve = () => r(onResolve()); if (this.completed) this._resolve(); return this; }); } } const waapi = { /** * @param {DOMTargetsParam} targets * @param {WAAPIAnimationParams} params * @return {WAAPIAnimation} */ animate: (targets, params) => new WAAPIAnimation(targets, params), convertEase: easingToLinear }; export { WAAPIAnimation, waapi };