349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
(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.MotionUtils = {}));
|
|
})(this, (function (exports) { 'use strict';
|
|
|
|
function addUniqueItem(arr, item) {
|
|
if (arr.indexOf(item) === -1)
|
|
arr.push(item);
|
|
}
|
|
function removeItem(arr, item) {
|
|
const index = arr.indexOf(item);
|
|
if (index > -1)
|
|
arr.splice(index, 1);
|
|
}
|
|
// Adapted from array-move
|
|
function moveItem([...arr], fromIndex, toIndex) {
|
|
const startIndex = fromIndex < 0 ? arr.length + fromIndex : fromIndex;
|
|
if (startIndex >= 0 && startIndex < arr.length) {
|
|
const endIndex = toIndex < 0 ? arr.length + toIndex : toIndex;
|
|
const [item] = arr.splice(fromIndex, 1);
|
|
arr.splice(endIndex, 0, item);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
const clamp = (min, max, v) => {
|
|
if (v > max)
|
|
return max;
|
|
if (v < min)
|
|
return min;
|
|
return v;
|
|
};
|
|
|
|
function formatErrorMessage(message, errorCode) {
|
|
return errorCode
|
|
? `${message}. For more information and steps for solving, visit https://motion.dev/troubleshooting/${errorCode}`
|
|
: message;
|
|
}
|
|
|
|
exports.warning = () => { };
|
|
exports.invariant = () => { };
|
|
{
|
|
exports.warning = (check, message, errorCode) => {
|
|
if (!check && typeof console !== "undefined") {
|
|
console.warn(formatErrorMessage(message, errorCode));
|
|
}
|
|
};
|
|
exports.invariant = (check, message, errorCode) => {
|
|
if (!check) {
|
|
throw new Error(formatErrorMessage(message, errorCode));
|
|
}
|
|
};
|
|
}
|
|
|
|
const MotionGlobalConfig = {};
|
|
|
|
/**
|
|
* Check if value is a numerical string, ie a string that is purely a number eg "100" or "-100.1"
|
|
*/
|
|
const isNumericalString = (v) => /^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(v);
|
|
|
|
function isObject(value) {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
/**
|
|
* Check if the value is a zero value string like "0px" or "0%"
|
|
*/
|
|
const isZeroValueString = (v) => /^0[^.\s]+$/u.test(v);
|
|
|
|
/*#__NO_SIDE_EFFECTS__*/
|
|
function memo(callback) {
|
|
let result;
|
|
return () => {
|
|
if (result === undefined)
|
|
result = callback();
|
|
return result;
|
|
};
|
|
}
|
|
|
|
/*#__NO_SIDE_EFFECTS__*/
|
|
const noop = (any) => any;
|
|
|
|
/**
|
|
* Pipe
|
|
* Compose other transformers to run linearily
|
|
* pipe(min(20), max(40))
|
|
* @param {...functions} transformers
|
|
* @return {function}
|
|
*/
|
|
const combineFunctions = (a, b) => (v) => b(a(v));
|
|
const pipe = (...transformers) => transformers.reduce(combineFunctions);
|
|
|
|
/*
|
|
Progress within given range
|
|
|
|
Given a lower limit and an upper limit, we return the progress
|
|
(expressed as a number 0-1) represented by the given value, and
|
|
limit that progress to within 0-1.
|
|
|
|
@param [number]: Lower limit
|
|
@param [number]: Upper limit
|
|
@param [number]: Value to find progress within given range
|
|
@return [number]: Progress of value within range as expressed 0-1
|
|
*/
|
|
/*#__NO_SIDE_EFFECTS__*/
|
|
const progress = (from, to, value) => {
|
|
const toFromDifference = to - from;
|
|
return toFromDifference === 0 ? 1 : (value - from) / toFromDifference;
|
|
};
|
|
|
|
class SubscriptionManager {
|
|
constructor() {
|
|
this.subscriptions = [];
|
|
}
|
|
add(handler) {
|
|
addUniqueItem(this.subscriptions, handler);
|
|
return () => removeItem(this.subscriptions, handler);
|
|
}
|
|
notify(a, b, c) {
|
|
const numSubscriptions = this.subscriptions.length;
|
|
if (!numSubscriptions)
|
|
return;
|
|
if (numSubscriptions === 1) {
|
|
/**
|
|
* If there's only a single handler we can just call it without invoking a loop.
|
|
*/
|
|
this.subscriptions[0](a, b, c);
|
|
}
|
|
else {
|
|
for (let i = 0; i < numSubscriptions; i++) {
|
|
/**
|
|
* Check whether the handler exists before firing as it's possible
|
|
* the subscriptions were modified during this loop running.
|
|
*/
|
|
const handler = this.subscriptions[i];
|
|
handler && handler(a, b, c);
|
|
}
|
|
}
|
|
}
|
|
getSize() {
|
|
return this.subscriptions.length;
|
|
}
|
|
clear() {
|
|
this.subscriptions.length = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts seconds to milliseconds
|
|
*
|
|
* @param seconds - Time in seconds.
|
|
* @return milliseconds - Converted time in milliseconds.
|
|
*/
|
|
/*#__NO_SIDE_EFFECTS__*/
|
|
const secondsToMilliseconds = (seconds) => seconds * 1000;
|
|
/*#__NO_SIDE_EFFECTS__*/
|
|
const millisecondsToSeconds = (milliseconds) => milliseconds / 1000;
|
|
|
|
/*
|
|
Convert velocity into velocity per second
|
|
|
|
@param [number]: Unit per frame
|
|
@param [number]: Frame duration in ms
|
|
*/
|
|
function velocityPerSecond(velocity, frameDuration) {
|
|
return frameDuration ? velocity * (1000 / frameDuration) : 0;
|
|
}
|
|
|
|
const warned = new Set();
|
|
function hasWarned(message) {
|
|
return warned.has(message);
|
|
}
|
|
function warnOnce(condition, message, errorCode) {
|
|
if (condition || warned.has(message))
|
|
return;
|
|
console.warn(formatErrorMessage(message, errorCode));
|
|
warned.add(message);
|
|
}
|
|
|
|
const wrap = (min, max, v) => {
|
|
const rangeSize = max - min;
|
|
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
|
|
};
|
|
|
|
/*
|
|
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 noticeably 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 noop;
|
|
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);
|
|
}
|
|
|
|
// Accepts an easing function and returns a new one that outputs mirrored values for
|
|
// the second half of the animation. Turns easeIn into easeInOut.
|
|
const mirrorEasing = (easing) => (p) => p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2;
|
|
|
|
// Accepts an easing function and returns a new one that outputs reversed values.
|
|
// Turns easeIn into easeOut.
|
|
const reverseEasing = (easing) => (p) => 1 - easing(1 - p);
|
|
|
|
const backOut = /*@__PURE__*/ cubicBezier(0.33, 1.53, 0.69, 0.99);
|
|
const backIn = /*@__PURE__*/ reverseEasing(backOut);
|
|
const backInOut = /*@__PURE__*/ mirrorEasing(backIn);
|
|
|
|
const anticipate = (p) => (p *= 2) < 1 ? 0.5 * backIn(p) : 0.5 * (2 - Math.pow(2, -10 * (p - 1)));
|
|
|
|
const circIn = (p) => 1 - Math.sin(Math.acos(p));
|
|
const circOut = reverseEasing(circIn);
|
|
const circInOut = mirrorEasing(circIn);
|
|
|
|
const easeIn = /*@__PURE__*/ cubicBezier(0.42, 0, 1, 1);
|
|
const easeOut = /*@__PURE__*/ cubicBezier(0, 0, 0.58, 1);
|
|
const easeInOut = /*@__PURE__*/ cubicBezier(0.42, 0, 0.58, 1);
|
|
|
|
function steps(numSteps, direction = "end") {
|
|
return (progress) => {
|
|
progress =
|
|
direction === "end"
|
|
? Math.min(progress, 0.999)
|
|
: Math.max(progress, 0.001);
|
|
const expanded = progress * numSteps;
|
|
const rounded = direction === "end" ? Math.floor(expanded) : Math.ceil(expanded);
|
|
return clamp(0, 1, rounded / numSteps);
|
|
};
|
|
}
|
|
|
|
const isEasingArray = (ease) => {
|
|
return Array.isArray(ease) && typeof ease[0] !== "number";
|
|
};
|
|
|
|
function getEasingForSegment(easing, i) {
|
|
return isEasingArray(easing) ? easing[wrap(0, easing.length, i)] : easing;
|
|
}
|
|
|
|
const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
|
|
|
|
const easingLookup = {
|
|
linear: noop,
|
|
easeIn,
|
|
easeInOut,
|
|
easeOut,
|
|
circIn,
|
|
circInOut,
|
|
circOut,
|
|
backIn,
|
|
backInOut,
|
|
backOut,
|
|
anticipate,
|
|
};
|
|
const isValidEasing = (easing) => {
|
|
return typeof easing === "string";
|
|
};
|
|
const easingDefinitionToFunction = (definition) => {
|
|
if (isBezierDefinition(definition)) {
|
|
// If cubic bezier definition, create bezier curve
|
|
exports.invariant(definition.length === 4, `Cubic bezier arrays must contain four numerical values.`, "cubic-bezier-length");
|
|
const [x1, y1, x2, y2] = definition;
|
|
return cubicBezier(x1, y1, x2, y2);
|
|
}
|
|
else if (isValidEasing(definition)) {
|
|
// Else lookup from table
|
|
exports.invariant(easingLookup[definition] !== undefined, `Invalid easing type '${definition}'`, "invalid-easing-type");
|
|
return easingLookup[definition];
|
|
}
|
|
return definition;
|
|
};
|
|
|
|
exports.MotionGlobalConfig = MotionGlobalConfig;
|
|
exports.SubscriptionManager = SubscriptionManager;
|
|
exports.addUniqueItem = addUniqueItem;
|
|
exports.anticipate = anticipate;
|
|
exports.backIn = backIn;
|
|
exports.backInOut = backInOut;
|
|
exports.backOut = backOut;
|
|
exports.circIn = circIn;
|
|
exports.circInOut = circInOut;
|
|
exports.circOut = circOut;
|
|
exports.clamp = clamp;
|
|
exports.cubicBezier = cubicBezier;
|
|
exports.easeIn = easeIn;
|
|
exports.easeInOut = easeInOut;
|
|
exports.easeOut = easeOut;
|
|
exports.easingDefinitionToFunction = easingDefinitionToFunction;
|
|
exports.getEasingForSegment = getEasingForSegment;
|
|
exports.hasWarned = hasWarned;
|
|
exports.isBezierDefinition = isBezierDefinition;
|
|
exports.isEasingArray = isEasingArray;
|
|
exports.isNumericalString = isNumericalString;
|
|
exports.isObject = isObject;
|
|
exports.isZeroValueString = isZeroValueString;
|
|
exports.memo = memo;
|
|
exports.millisecondsToSeconds = millisecondsToSeconds;
|
|
exports.mirrorEasing = mirrorEasing;
|
|
exports.moveItem = moveItem;
|
|
exports.noop = noop;
|
|
exports.pipe = pipe;
|
|
exports.progress = progress;
|
|
exports.removeItem = removeItem;
|
|
exports.reverseEasing = reverseEasing;
|
|
exports.secondsToMilliseconds = secondsToMilliseconds;
|
|
exports.steps = steps;
|
|
exports.velocityPerSecond = velocityPerSecond;
|
|
exports.warnOnce = warnOnce;
|
|
exports.wrap = wrap;
|
|
|
|
}));
|