UNPKG

motion

Version:

The Motion library for the web

921 lines (875 loc) 36.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Motion = {})); }(this, (function (exports) { 'use strict'; const data = new WeakMap(); function getAnimationData(element) { if (!data.has(element)) { data.set(element, { activeTransforms: [], activeAnimations: {}, }); } return data.get(element); } function addUniqueItem(array, item) { array.indexOf(item) === -1 && array.push(item); } function removeItem(arr, item) { const index = arr.indexOf(item); index > -1 && arr.splice(index, 1); } const noop = () => { }; const noopReturn = (v) => v; /** * A list of all transformable axes. We'll use this list to generated a version * of each axes for each transform. */ const axes = ["", "X", "Y", "Z"]; /** * An ordered array of each transformable value. By default, transform values * will be sorted to this order. */ const order = ["translate", "scale", "rotate", "skew"]; const transformAlias = { x: "translateX", y: "translateY", z: "translateZ", }; const rotation = { syntax: "<angle>", initialValue: "0deg", toDefaultUnit: (v) => v + "deg", }; const baseTransformProperties = { translate: { syntax: "<length-percentage>", initialValue: "0px", toDefaultUnit: (v) => v + "px", }, rotate: rotation, scale: { syntax: "<number>", initialValue: 1, toDefaultUnit: noopReturn, }, skew: rotation, }; const transformPropertyDefinitions = new Map(); const asTransformCssVar = (name) => `--motion-${name}`; /** * Generate a list of every possible transform key */ const transforms = ["x", "y", "z"]; order.forEach((name) => { axes.forEach((axis) => { transforms.push(name + axis); transformPropertyDefinitions.set(asTransformCssVar(name + axis), baseTransformProperties[name]); }); }); /** * A function to use with Array.sort to sort transform keys by their default order. */ const compareTransformOrder = (a, b) => transforms.indexOf(a) - transforms.indexOf(b); /** * Provide a quick way to check if a string is the name of a transform */ const transformLookup = new Set(transforms); const isTransform = (name) => transformLookup.has(name); const addTransformToElement = (element, name) => { const { activeTransforms } = getAnimationData(element); addUniqueItem(activeTransforms, name); element.style.transform = buildTransformTemplate(activeTransforms); }; const buildTransformTemplate = (activeTransforms) => activeTransforms .sort(compareTransformOrder) .reduce(transformListToString, "") .trim(); const transformListToString = (template, name) => `${template} ${name}(var(${asTransformCssVar(name)}))`; const isCssVar = (name) => name.startsWith("--"); const registeredProperties = new Set(); function registerCssVariable(name) { if (registeredProperties.has(name)) return; registeredProperties.add(name); try { const { syntax, initialValue } = transformPropertyDefinitions.has(name) ? transformPropertyDefinitions.get(name) : {}; CSS.registerProperty({ name, inherits: false, syntax, initialValue, }); } catch (e) { } } const ms = (seconds) => seconds * 1000; function stopAnimation(animation) { // Suppress error thrown by WAAPI try { animation.commitStyles(); animation.cancel(); } catch (e) { } } const isCubicBezier = (easing) => Array.isArray(easing) && typeof easing[0] === "number"; const isEasingList = (easing) => Array.isArray(easing) && typeof easing[0] !== "number"; const convertEasing = (easing) => isCubicBezier(easing) ? cubicBezierAsString(easing) : easing; const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`; const testAnimation = (keyframes) => document.createElement("div").animate(keyframes, { duration: 0.001 }); const featureTests = { cssRegisterProperty: () => typeof CSS !== "undefined" && Object.hasOwnProperty.call(CSS, "registerProperty"), waapi: () => Object.hasOwnProperty.call(Element.prototype, "animate"), partialKeyframes: () => { try { testAnimation({ opacity: [1] }); } catch (e) { return false; } return true; }, finished: () => Boolean(testAnimation({ opacity: [0, 1] }).finished), }; const results = {}; const supports = Object.keys(featureTests).reduce((acc, key) => { acc[key] = () => { if (results[key] === undefined) results[key] = featureTests[key](); return results[key]; }; return acc; }, {}); const createCssVariableRenderer = (element, name) => { return (latest) => element.style.setProperty(name, latest); }; const createStyleRenderer = (element, name) => { return (latest) => (element.style[name] = latest); }; const defaults = { duration: 0.3, delay: 0, endDelay: 0, repeat: 0, easing: "ease", }; /* Bezier function generator This has been modified from Gaëtan Renaudeau's BezierEasing https://github.com/gre/bezier-easing/blob/master/src/index.js https://github.com/gre/bezier-easing/blob/master/LICENSE I've removed the newtonRaphsonIterate algo because in benchmarking it wasn't noticiably faster than binarySubdivision, indeed removing it usually improved times, depending on the curve. I also removed the lookup table, as for the added bundle size and loop we're only cutting ~4 or so subdivision iterations. I bumped the max iterations up to 12 to compensate and this still tended to be faster for no perceivable loss in accuracy. Usage const easeOut = cubicBezier(.17,.67,.83,.67); const x = easeOut(0.5); // returns 0.627... */ // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. const calcBezier = (t, a1, a2) => (((1.0 - 3.0 * a2 + 3.0 * a1) * t + (3.0 * a2 - 6.0 * a1)) * t + 3.0 * a1) * t; const subdivisionPrecision = 0.0000001; const subdivisionMaxIterations = 12; function binarySubdivide(x, lowerBound, upperBound, mX1, mX2) { let currentX; let currentT; let i = 0; do { currentT = lowerBound + (upperBound - lowerBound) / 2.0; currentX = calcBezier(currentT, mX1, mX2) - x; if (currentX > 0.0) { upperBound = currentT; } else { lowerBound = currentT; } } while (Math.abs(currentX) > subdivisionPrecision && ++i < subdivisionMaxIterations); return currentT; } function cubicBezier(mX1, mY1, mX2, mY2) { // If this is a linear gradient, return linear easing if (mX1 === mY1 && mX2 === mY2) return noopReturn; const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2); // If animation is at start/end, return t without easing return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2); } /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } var clamp = function (min, max, v) { return Math.min(Math.max(v, min), max); }; var progress = function (from, to, value) { var toFromDifference = to - from; return toFromDifference === 0 ? 1 : (value - from) / toFromDifference; }; var mix = function (from, to, progress) { return -progress * from + progress * to + from; }; var wrap = function (min, max, v) { var rangeSize = max - min; return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min; }; var steps = function (steps, direction) { if (direction === void 0) { direction = 'end'; } return function (progress) { progress = direction === 'end' ? Math.min(progress, 0.999) : Math.max(progress, 0.001); var expanded = progress * steps; var rounded = direction === 'end' ? Math.floor(expanded) : Math.ceil(expanded); return clamp(0, 1, rounded / steps); }; }; const namedEasings = { ease: cubicBezier(0.25, 0.1, 0.25, 1.0), "ease-in": cubicBezier(0.42, 0.0, 1.0, 1.0), "ease-in-out": cubicBezier(0.42, 0.0, 0.58, 1.0), "ease-out": cubicBezier(0.0, 0.0, 0.58, 1.0), }; const functionArgsRegex = /\((.*?)\)/; function getEasingFunction(definition) { // If already an easing function, return if (typeof definition === "function") return definition; // If an easing curve definition, return bezier function if (Array.isArray(definition)) return cubicBezier(...definition); // If we have a predefined easing function, return if (namedEasings[definition]) return namedEasings[definition]; // If this is a steps function, attempt to create easing curve if (definition.startsWith("steps")) { const args = functionArgsRegex.exec(definition); if (args) { const argsArray = args[1].split(","); return steps(parseFloat(argsArray[0]), argsArray[1].trim()); } } return noopReturn; } function getEasingForSegment(easing, i) { return isEasingList(easing) ? easing[wrap(0, easing.length, i)] : easing; } function fillOffset(offset, remaining) { const min = offset[offset.length - 1]; for (let i = 1; i <= remaining; i++) { const offsetProgress = progress(0, remaining, i); offset.push(mix(min, 1, offsetProgress)); } } function defaultOffset(length) { const offset = [0]; fillOffset(offset, length - 1); return offset; } const clampProgress = (p) => Math.min(1, Math.max(p, 0)); function slowInterpolateNumbers(output, input = defaultOffset(output.length), easing = noopReturn) { const length = output.length; /** * If the input length is lower than the output we * fill the input to match. This currently assumes the input * is an animation progress value so is a good candidate for * moving outside the function. */ const remainder = length - input.length; remainder > 0 && fillOffset(input, remainder); return (t) => { let i = 0; for (; i < length - 2; i++) { if (t < input[i + 1]) break; } let progressInRange = clampProgress(progress(input[i], input[i + 1], t)); const segmentEasing = getEasingForSegment(easing, i); progressInRange = segmentEasing(progressInRange); return mix(output[i], output[i + 1], progressInRange); }; } class Animation { constructor(output, keyframes, // TODO Merge in defaults { easing = defaults.easing, duration = defaults.duration, delay = defaults.delay, endDelay = defaults.endDelay, offset, repeat = defaults.repeat, direction = "normal", }) { this.startTime = 0; this.rate = 1; this.t = 0; this.cancelT = 0; this.playState = "idle"; this.finished = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); const totalDuration = duration * (repeat + 1); const interpolate = slowInterpolateNumbers(keyframes, offset, isEasingList(easing) ? easing.map(getEasingFunction) : getEasingFunction(easing)); this.tick = (timestamp) => { if (this.playState === "finished") { const latest = interpolate(1); output(latest); this.resolve(latest); return; } if (this.pauseTime) { timestamp = this.pauseTime; } let t = (timestamp - this.startTime) * this.rate; this.t = t; // Convert to seconds t /= 1000; // Rebase on delay t = Math.max(t - delay, 0); const progress = t / duration; // TODO progress += iterationStart let currentIteration = Math.floor(progress); let iterationProgress = progress % 1.0; if (!iterationProgress && progress >= 1) { iterationProgress = 1; } if (iterationProgress === 1) { currentIteration--; } // Reverse progress const iterationIsOdd = currentIteration % 2; if (direction === "reverse" || (direction === "alternate" && iterationIsOdd) || (direction === "alternate-reverse" && !iterationIsOdd)) { iterationProgress = 1 - iterationProgress; } const interpolationIsFinished = t >= totalDuration; const interpolationProgress = interpolationIsFinished ? 1 : Math.min(iterationProgress, 1); const latest = interpolate(interpolationProgress); output(latest); const isFinished = t >= totalDuration + endDelay; if (isFinished) { this.playState = "finished"; this.resolve(latest); } else if (this.playState !== "idle") { requestAnimationFrame(this.tick); } }; this.play(); } play() { const now = performance.now(); this.playState = "running"; if (this.pauseTime) { this.startTime = now - (this.pauseTime - this.startTime); } else if (!this.startTime) { this.startTime = now; } this.pauseTime = undefined; requestAnimationFrame(this.tick); } pause() { this.playState = "paused"; this.pauseTime = performance.now(); } finish() { this.playState = "finished"; this.tick(0); } cancel() { this.playState = "idle"; this.tick(this.cancelT); this.reject(false); } reverse() { this.rate *= -1; } commitStyles() { this.cancelT = this.t; } get currentTime() { return this.t; } set currentTime(t) { if (this.pauseTime || this.rate === 0) { this.pauseTime = t; } else { this.startTime = performance.now() - t / this.rate; } } get playbackRate() { return this.rate; } set playbackRate(rate) { this.rate = rate; } } function animateNumber(output, keyframes = [0, 1], options = {}) { return new Animation(output, keyframes, options); } const style = { get: (element, name) => { let value = isCssVar(name) ? element.style.getPropertyValue(name) : getComputedStyle(element)[name]; if (!value && value !== 0) { const definition = transformPropertyDefinitions.get(name); if (definition) value = definition.initialValue; } return value; }, }; function hydrateKeyframes(keyframes, element, name) { for (let i = 0; i < keyframes.length; i++) { if (keyframes[i] === null) { keyframes[i] = i ? keyframes[i - 1] : style.get(element, name); } } return keyframes; } const keyframesList = (keyframes) => Array.isArray(keyframes) ? keyframes : [keyframes]; function animateStyle(element, name, keyframesDefinition, options = {}) { let { duration = defaults.duration, delay = defaults.delay, endDelay = defaults.endDelay, repeat = defaults.repeat, easing = defaults.easing, direction, offset, allowWebkitAcceleration = false, } = options; const data = getAnimationData(element); let canAnimateNatively = supports.waapi(); let render = noop; const valueIsTransform = isTransform(name); /** * If this is an individual transform, we need to map its * key to a CSS variable and update the element's transform style */ if (valueIsTransform) { if (transformAlias[name]) name = transformAlias[name]; addTransformToElement(element, name); name = asTransformCssVar(name); } /** * Get definition of value, this will be used to convert numerical * keyframes into the default value type. */ const definition = transformPropertyDefinitions.get(name); /** * Replace null values with the previous keyframe value, or read * it from the DOM if it's the first keyframe. * * TODO: This needs to come after the valueIsTransform * check so it can correctly read the underlying value. * Should make a test for this. */ let keyframes = hydrateKeyframes(keyframesList(keyframesDefinition), element, name); stopCurrentAnimation(data, name); /** * If this is a CSS variable we need to register it with the browser * before it can be animated natively. We also set it with setProperty * rather than directly onto the element.style object. */ if (isCssVar(name)) { render = createCssVariableRenderer(element, name); if (supports.cssRegisterProperty()) { registerCssVariable(name); } else { canAnimateNatively = false; } } else { render = createStyleRenderer(element, name); } let animation; /** * If we can animate this value with WAAPI, do so. Currently this only * feature detects CSS.registerProperty but could check WAAPI too. */ if (canAnimateNatively) { /** * Convert numbers to default value types. Currently this only supports * transforms but it could also support other value types. */ if (definition) { keyframes = keyframes.map((value) => typeof value === "number" ? definition.toDefaultUnit(value) : value); } if (!supports.partialKeyframes() && keyframes.length === 1) { keyframes.unshift(style.get(element, name)); } const animationOptions = { delay: ms(delay), duration: ms(duration), endDelay: ms(endDelay), easing: !isEasingList(easing) ? convertEasing(easing) : undefined, direction, iterations: repeat + 1, }; animation = element.animate({ [name]: keyframes, offset, easing: isEasingList(easing) ? easing.map(convertEasing) : undefined, }, animationOptions); /** * Polyfill finished Promise in browsers that don't support it */ if (!animation.finished) { animation.finished = new Promise((resolve, reject) => { animation.onfinish = resolve; animation.oncancel = reject; }); } const target = keyframes[keyframes.length - 1]; animation.finished.then(() => render(target)).catch(noop); /** * This forces Webkit to run animations on the main thread by exploiting * this condition: * https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp?rev=281238#L1099 * * This fixes Webkit's timing bugs, like accelerated animations falling * out of sync with main thread animations and massive delays in starting * accelerated animations in WKWebView. */ if (!allowWebkitAcceleration) animation.playbackRate = 1.000001; } else if (valueIsTransform && keyframes.every(isNumber)) { if (keyframes.length === 1) { keyframes.unshift(style.get(element, name) || (definition === null || definition === void 0 ? void 0 : definition.initialValue) || 0); } /** * Transform styles are currently only accepted as numbers of * their default value type, so here we loop through and map * them to numbers. */ keyframes = keyframes.map((value) => typeof value === "string" ? parseFloat(value) : value); if (definition) { const applyStyle = render; render = (v) => applyStyle(definition.toDefaultUnit(v)); } animation = animateNumber(render, keyframes, options); } else { const target = keyframes[keyframes.length - 1]; render(definition && typeof target === "number" ? definition.toDefaultUnit(target) : target); } data.activeAnimations[name] = animation; return animation; } function stopCurrentAnimation(data, name) { if (data.activeAnimations[name]) { stopAnimation(data.activeAnimations[name]); data.activeAnimations[name] = undefined; } } const isNumber = (value) => typeof value === "number"; const getOptions = (options, key) => options[key] ? Object.assign(Object.assign({}, options), options[key]) : Object.assign({}, options); function resolveElements(elements, selectorCache) { var _a; if (typeof elements === "string") { if (selectorCache) { (_a = selectorCache[elements]) !== null && _a !== void 0 ? _a : (selectorCache[elements] = document.querySelectorAll(elements)); elements = selectorCache[elements]; } else { elements = document.querySelectorAll(elements); } } else if (elements instanceof Element) { elements = [elements]; } return Array.from(elements); } function createAnimationControls(animations) { // TODO Duplication with animate const state = { animations, finished: Promise.all(animations.map((animation) => animation.finished)), }; return new Proxy(state, controls); } const controls = { get: (target, key) => { var _a, _b; switch (key) { case "finished": return target.finished; case "currentTime": // TODO Find first active animation const duration = ((_a = target.animations[0]) === null || _a === void 0 ? void 0 : _a[key]) || 0; return duration ? duration / 1000 : 0; case "playbackRate": // TODO Find first active animation return (_b = target.animations[0]) === null || _b === void 0 ? void 0 : _b[key]; case "stop": return () => target.animations.forEach(stopAnimation); default: return () => target.animations.forEach((animation) => animation[key]()); } }, set: (target, key, value) => { switch (key) { case "currentTime": value = ms(value); case "currentTime": case "playbackRate": for (let i = 0; i < target.animations.length; i++) { target.animations[i][key] = value; } return true; } return false; }, }; function stagger(duration = 0.1, { start = 0, from = 0, easing } = {}) { return (i, total) => { const fromIndex = typeof from === "number" ? from : getFromIndex(from, total); const distance = Math.abs(fromIndex - i); let delay = duration * distance; if (easing) { const maxDelay = total * i; const easingFunction = getEasingFunction(easing); delay = easingFunction(delay / maxDelay) * maxDelay; } return start + delay; }; } function getFromIndex(from, total) { if (from === "first") { return 0; } else { const lastIndex = total - 1; return from === "last" ? lastIndex : lastIndex / 2; } } function resolveOption(option, i, total) { return typeof option === "function" ? option(i, total) : option; } function animate(elements, keyframes, options = {}) { elements = resolveElements(elements); const animations = []; const numElements = elements.length; for (let i = 0; i < numElements; i++) { const element = elements[i]; for (const key in keyframes) { const valueOptions = getOptions(options, key); valueOptions.delay = resolveOption(valueOptions.delay, i, numElements); const animation = animateStyle(element, key, keyframes[key], valueOptions); animation && animations.push(animation); } } return createAnimationControls(animations); } function calcNextTime(current, next, prev, labels) { var _a; if (typeof next === "number") { return next; } else if (next.startsWith("-") || next.startsWith("+")) { return Math.max(0, current + parseFloat(next)); } else if (next === "<") { return prev; } else { return (_a = labels.get(next)) !== null && _a !== void 0 ? _a : current; } } function eraseKeyframes(sequence, startTime, endTime) { for (let i = 0; i < sequence.length; i++) { const keyframe = sequence[i]; if (keyframe.at > startTime && keyframe.at < endTime) { removeItem(sequence, keyframe); // If we remove this item we have to push the pointer back one i--; } } } function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) { /** * Erase every existing value between currentTime and targetTime, * this will essentially splice this timeline into any currently * defined ones. */ eraseKeyframes(sequence, startTime, endTime); for (let i = 0; i < keyframes.length; i++) { sequence.push({ value: keyframes[i], at: mix(startTime, endTime, offset[i]), easing: getEasingForSegment(easing, i), }); } } function compareByTime(a, b) { if (a.at === b.at) { return a.value === null ? 1 : -1; } else { return a.at - b.at; } } function timeline(definition, options = {}) { const animations = []; const animationDefinitions = createAnimationsFromTimeline(definition, options); for (let i = 0; i < animationDefinitions.length; i++) { const animation = animateStyle(...animationDefinitions[i]); animation && animations.push(animation); } return createAnimationControls(animations); } function createAnimationsFromTimeline(definition, _a = {}) { var { defaultOptions = {} } = _a, timelineOptions = __rest(_a, ["defaultOptions"]); const animationDefinitions = []; const elementSequences = new Map(); const elementCache = {}; const timeLabels = new Map(); let prevTime = 0; let currentTime = 0; let totalDuration = 0; /** * Build the timeline by mapping over the definition array and converting * the definitions into keyframes and offsets with absolute time values. * These will later get converted into relative offsets in a second pass. */ for (let i = 0; i < definition.length; i++) { const [elementDefinition, keyframes, options = {}] = definition[i]; /** * If a relative or absolute time value has been specified we need to resolve * it in relation to the currentTime. */ if (options.at !== undefined) { currentTime = calcNextTime(currentTime, options.at, prevTime, timeLabels); } /** * Keep track of the maximum duration in this definition. This will be * applied to currentTime once the definition has been parsed. */ let maxDuration = 0; /** * Find all the elements specified in the definition and parse value * keyframes from their timeline definitions. */ const elements = resolveElements(elementDefinition, elementCache); const numElements = elements.length; for (let elementIndex = 0; elementIndex < numElements; elementIndex++) { const element = elements[elementIndex]; const elementSequence = getElementSequence(element, elementSequences); for (const key in keyframes) { const valueSequence = getValueSequence(key, elementSequence); const valueKeyframes = keyframesList(keyframes[key]); const valueOptions = getOptions(options, key); const { duration = defaultOptions.duration || defaults.duration, easing = defaultOptions.easing || defaults.easing, offset = defaultOffset(valueKeyframes.length), } = valueOptions; const delay = resolveOption(options.delay, elementIndex, numElements) || 0; const startTime = currentTime + delay; const targetTime = startTime + duration; if (offset.length === 1 && offset[0] === 0) { offset[1] = 1; } /** * Fill out if offset if fewer offsets than keyframes */ const remainder = length - valueKeyframes.length; remainder > 0 && fillOffset(offset, remainder); /** * If only one value has been set, ie [1], push a null to the start of * the keyframe array. This will let us mark a keyframe at this point * that will later be hydrated with the previous value. */ valueKeyframes.length === 1 && valueKeyframes.unshift(null); /** * Add keyframes, mapping offsets to absolute time. */ addKeyframes(valueSequence, valueKeyframes, easing, offset, startTime, targetTime); maxDuration = Math.max(delay + duration, maxDuration); totalDuration = Math.max(targetTime, totalDuration); } } prevTime = currentTime; currentTime += maxDuration; } /** * For every element and value combination create a new animation. */ elementSequences.forEach((valueSequences, element) => { for (const key in valueSequences) { const valueSequence = valueSequences[key]; /** * Arrange all the keyframes in ascending time order. */ valueSequence.sort(compareByTime); const keyframes = []; const valueOffset = []; const valueEasing = []; /** * For each keyframe, translate absolute times into * relative offsets based on the total duration of the timeline. */ for (let i = 0; i < valueSequence.length; i++) { const { at, value, easing } = valueSequence[i]; keyframes.push(value); valueOffset.push(progress(0, totalDuration, at)); valueEasing.push(easing || defaults.easing); } /** * If the generated animation doesn't end on the final keyframe, * provide one with a null wildcard value. This will ensure it * stays static until the end of the animation. */ if (valueOffset[valueOffset.length - 1] !== 1) { valueOffset.push(1); keyframes.push(null); } animationDefinitions.push([ element, key, keyframes, Object.assign(Object.assign(Object.assign({}, defaultOptions), { duration: totalDuration, easing: valueEasing, offset: valueOffset }), timelineOptions), ]); } }); return animationDefinitions; } function getElementSequence(element, sequences) { !sequences.has(element) && sequences.set(element, {}); return sequences.get(element); } function getValueSequence(name, sequences) { if (!sequences[name]) sequences[name] = []; return sequences[name]; } exports.animate = animate; exports.animateStyle = animateStyle; exports.stagger = stagger; exports.timeline = timeline; Object.defineProperty(exports, '__esModule', { value: true }); })));