framer-motion
Version:
A simple and powerful JavaScript animation library
1,453 lines (1,378 loc) ⢠189 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var motionDom = require('motion-dom');
var motionUtils = require('motion-utils');
const clamp = (min, max, v) => {
if (v > max)
return max;
if (v < min)
return min;
return v;
};
const velocitySampleDuration = 5; // ms
function calcGeneratorVelocity(resolveValue, t, current) {
const prevT = Math.max(t - velocitySampleDuration, 0);
return motionUtils.velocityPerSecond(current - resolveValue(prevT), t - prevT);
}
const springDefaults = {
// Default spring physics
stiffness: 100,
damping: 10,
mass: 1.0,
velocity: 0.0,
// Default duration/bounce-based options
duration: 800, // in ms
bounce: 0.3,
visualDuration: 0.3, // in seconds
// Rest thresholds
restSpeed: {
granular: 0.01,
default: 2,
},
restDelta: {
granular: 0.005,
default: 0.5,
},
// Limits
minDuration: 0.01, // in seconds
maxDuration: 10.0, // in seconds
minDamping: 0.05,
maxDamping: 1,
};
const safeMin = 0.001;
function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, velocity = springDefaults.velocity, mass = springDefaults.mass, }) {
let envelope;
let derivative;
motionUtils.warning(duration <= motionUtils.secondsToMilliseconds(springDefaults.maxDuration), "Spring duration must be 10 seconds or less");
let dampingRatio = 1 - bounce;
/**
* Restrict dampingRatio and duration to within acceptable ranges.
*/
dampingRatio = clamp(springDefaults.minDamping, springDefaults.maxDamping, dampingRatio);
duration = clamp(springDefaults.minDuration, springDefaults.maxDuration, motionUtils.millisecondsToSeconds(duration));
if (dampingRatio < 1) {
/**
* Underdamped spring
*/
envelope = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio;
const delta = exponentialDecay * duration;
const a = exponentialDecay - velocity;
const b = calcAngularFreq(undampedFreq, dampingRatio);
const c = Math.exp(-delta);
return safeMin - (a / b) * c;
};
derivative = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio;
const delta = exponentialDecay * duration;
const d = delta * velocity + velocity;
const e = Math.pow(dampingRatio, 2) * Math.pow(undampedFreq, 2) * duration;
const f = Math.exp(-delta);
const g = calcAngularFreq(Math.pow(undampedFreq, 2), dampingRatio);
const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1;
return (factor * ((d - e) * f)) / g;
};
}
else {
/**
* Critically-damped spring
*/
envelope = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration);
const b = (undampedFreq - velocity) * duration + 1;
return -safeMin + a * b;
};
derivative = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration);
const b = (velocity - undampedFreq) * (duration * duration);
return a * b;
};
}
const initialGuess = 5 / duration;
const undampedFreq = approximateRoot(envelope, derivative, initialGuess);
duration = motionUtils.secondsToMilliseconds(duration);
if (isNaN(undampedFreq)) {
return {
stiffness: springDefaults.stiffness,
damping: springDefaults.damping,
duration,
};
}
else {
const stiffness = Math.pow(undampedFreq, 2) * mass;
return {
stiffness,
damping: dampingRatio * 2 * Math.sqrt(mass * stiffness),
duration,
};
}
}
const rootIterations = 12;
function approximateRoot(envelope, derivative, initialGuess) {
let result = initialGuess;
for (let i = 1; i < rootIterations; i++) {
result = result - envelope(result) / derivative(result);
}
return result;
}
function calcAngularFreq(undampedFreq, dampingRatio) {
return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio);
}
const durationKeys = ["duration", "bounce"];
const physicsKeys = ["stiffness", "damping", "mass"];
function isSpringType(options, keys) {
return keys.some((key) => options[key] !== undefined);
}
function getSpringOptions(options) {
let springOptions = {
velocity: springDefaults.velocity,
stiffness: springDefaults.stiffness,
damping: springDefaults.damping,
mass: springDefaults.mass,
isResolvedFromDuration: false,
...options,
};
// stiffness/damping/mass overrides duration/bounce
if (!isSpringType(options, physicsKeys) &&
isSpringType(options, durationKeys)) {
if (options.visualDuration) {
const visualDuration = options.visualDuration;
const root = (2 * Math.PI) / (visualDuration * 1.2);
const stiffness = root * root;
const damping = 2 *
clamp(0.05, 1, 1 - (options.bounce || 0)) *
Math.sqrt(stiffness);
springOptions = {
...springOptions,
mass: springDefaults.mass,
stiffness,
damping,
};
}
else {
const derived = findSpring(options);
springOptions = {
...springOptions,
...derived,
mass: springDefaults.mass,
};
springOptions.isResolvedFromDuration = true;
}
}
return springOptions;
}
function spring(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) {
const options = typeof optionsOrVisualDuration !== "object"
? {
visualDuration: optionsOrVisualDuration,
keyframes: [0, 1],
bounce,
}
: optionsOrVisualDuration;
let { restSpeed, restDelta } = options;
const origin = options.keyframes[0];
const target = options.keyframes[options.keyframes.length - 1];
/**
* This is the Iterator-spec return value. We ensure it's mutable rather than using a generator
* to reduce GC during animation.
*/
const state = { done: false, value: origin };
const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, } = getSpringOptions({
...options,
velocity: -motionUtils.millisecondsToSeconds(options.velocity || 0),
});
const initialVelocity = velocity || 0.0;
const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass));
const initialDelta = target - origin;
const undampedAngularFreq = motionUtils.millisecondsToSeconds(Math.sqrt(stiffness / mass));
/**
* If we're working on a granular scale, use smaller defaults for determining
* when the spring is finished.
*
* These defaults have been selected emprically based on what strikes a good
* ratio between feeling good and finishing as soon as changes are imperceptible.
*/
const isGranularScale = Math.abs(initialDelta) < 5;
restSpeed || (restSpeed = isGranularScale
? springDefaults.restSpeed.granular
: springDefaults.restSpeed.default);
restDelta || (restDelta = isGranularScale
? springDefaults.restDelta.granular
: springDefaults.restDelta.default);
let resolveSpring;
if (dampingRatio < 1) {
const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
// Underdamped spring
resolveSpring = (t) => {
const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
return (target -
envelope *
(((initialVelocity +
dampingRatio * undampedAngularFreq * initialDelta) /
angularFreq) *
Math.sin(angularFreq * t) +
initialDelta * Math.cos(angularFreq * t)));
};
}
else if (dampingRatio === 1) {
// Critically damped spring
resolveSpring = (t) => target -
Math.exp(-undampedAngularFreq * t) *
(initialDelta +
(initialVelocity + undampedAngularFreq * initialDelta) * t);
}
else {
// Overdamped spring
const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1);
resolveSpring = (t) => {
const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
// When performing sinh or cosh values can hit Infinity so we cap them here
const freqForT = Math.min(dampedAngularFreq * t, 300);
return (target -
(envelope *
((initialVelocity +
dampingRatio * undampedAngularFreq * initialDelta) *
Math.sinh(freqForT) +
dampedAngularFreq *
initialDelta *
Math.cosh(freqForT))) /
dampedAngularFreq);
};
}
const generator = {
calculatedDuration: isResolvedFromDuration ? duration || null : null,
next: (t) => {
const current = resolveSpring(t);
if (!isResolvedFromDuration) {
let currentVelocity = 0.0;
/**
* We only need to calculate velocity for under-damped springs
* as over- and critically-damped springs can't overshoot, so
* checking only for displacement is enough.
*/
if (dampingRatio < 1) {
currentVelocity =
t === 0
? motionUtils.secondsToMilliseconds(initialVelocity)
: calcGeneratorVelocity(resolveSpring, t, current);
}
const isBelowVelocityThreshold = Math.abs(currentVelocity) <= restSpeed;
const isBelowDisplacementThreshold = Math.abs(target - current) <= restDelta;
state.done =
isBelowVelocityThreshold && isBelowDisplacementThreshold;
}
else {
state.done = t >= duration;
}
state.value = state.done ? target : current;
return state;
},
toString: () => {
const calculatedDuration = Math.min(motionDom.calcGeneratorDuration(generator), motionDom.maxGeneratorDuration);
const easing = motionDom.generateLinearEasing((progress) => generator.next(calculatedDuration * progress).value, calculatedDuration, 30);
return calculatedDuration + "ms " + easing;
},
toTransition: () => { },
};
return generator;
}
spring.applyToOptions = (options) => {
const generatorOptions = motionDom.createGeneratorEasing(options, 100, spring);
options.ease = motionDom.supportsLinearEasing() ? generatorOptions.ease : "easeOut";
options.duration = motionUtils.secondsToMilliseconds(generatorOptions.duration);
options.type = "keyframes";
return options;
};
const wrap = (min, max, v) => {
const rangeSize = max - min;
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
};
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;
}
/*
Value in range from progress
Given a lower limit and an upper limit, we return the value within
that range as expressed by progress (usually a number from 0 to 1)
So progress = 0.5 would change
from -------- to
to
from ---- to
E.g. from = 10, to = 20, progress = 0.5 => 15
@param [number]: Lower limit of range
@param [number]: Upper limit of range
@param [number]: The progress between lower and upper limits expressed 0-1
@return [number]: Value as calculated from progress within range (not limited within range)
*/
const mixNumber$1 = (from, to, progress) => {
return from + (to - from) * progress;
};
function fillOffset(offset, remaining) {
const min = offset[offset.length - 1];
for (let i = 1; i <= remaining; i++) {
const offsetProgress = motionUtils.progress(0, remaining, i);
offset.push(mixNumber$1(min, 1, offsetProgress));
}
}
function defaultOffset$1(arr) {
const offset = [0];
fillOffset(offset, arr.length - 1);
return offset;
}
const isMotionValue = (value) => Boolean(value && value.getVelocity);
function isDOMKeyframes(keyframes) {
return typeof keyframes === "object" && !Array.isArray(keyframes);
}
function resolveSubjects(subject, keyframes, scope, selectorCache) {
if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
return motionDom.resolveElements(subject, scope, selectorCache);
}
else if (subject instanceof NodeList) {
return Array.from(subject);
}
else if (Array.isArray(subject)) {
return subject;
}
else {
return [subject];
}
}
function calculateRepeatDuration(duration, repeat, _repeatDelay) {
return duration * (repeat + 1);
}
/**
* Given a absolute or relative time definition and current/prev time state of the sequence,
* calculate an absolute time for the next keyframes.
*/
function calcNextTime(current, next, prev, labels) {
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 labels.get(next) ?? 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) {
motionUtils.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: mixNumber$1(startTime, endTime, offset[i]),
easing: getEasingForSegment(easing, i),
});
}
}
/**
* Take an array of times that represent repeated keyframes. For instance
* if we have original times of [0, 0.5, 1] then our repeated times will
* be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
* down to a 0-1 scale.
*/
function normalizeTimes(times, repeat) {
for (let i = 0; i < times.length; i++) {
times[i] = times[i] / (repeat + 1);
}
}
function compareByTime(a, b) {
if (a.at === b.at) {
if (a.value === null)
return 1;
if (b.value === null)
return -1;
return 0;
}
else {
return a.at - b.at;
}
}
const defaultSegmentEasing = "easeInOut";
const MAX_REPEAT = 20;
function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
const defaultDuration = defaultTransition.duration || 0.3;
const animationDefinitions = new Map();
const sequences = new Map();
const elementCache = {};
const timeLabels = new Map();
let prevTime = 0;
let currentTime = 0;
let totalDuration = 0;
/**
* Build the timeline by mapping over the sequence 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 < sequence.length; i++) {
const segment = sequence[i];
/**
* If this is a timeline label, mark it and skip the rest of this iteration.
*/
if (typeof segment === "string") {
timeLabels.set(segment, currentTime);
continue;
}
else if (!Array.isArray(segment)) {
timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels));
continue;
}
let [subject, keyframes, transition = {}] = segment;
/**
* If a relative or absolute time value has been specified we need to resolve
* it in relation to the currentTime.
*/
if (transition.at !== undefined) {
currentTime = calcNextTime(currentTime, transition.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;
const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
const valueKeyframesAsList = keyframesAsList(valueKeyframes);
const { delay = 0, times = defaultOffset$1(valueKeyframesAsList), type = "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
/**
* Resolve stagger() if defined.
*/
const calculatedDelay = typeof delay === "function"
? delay(elementIndex, numSubjects)
: delay;
/**
* If this animation should and can use a spring, generate a spring easing function.
*/
const numKeyframes = valueKeyframesAsList.length;
const createGenerator = motionDom.isGenerator(type)
? type
: generators?.[type];
if (numKeyframes <= 2 && createGenerator) {
/**
* As we're creating an easing function from a spring,
* ideally we want to generate it using the real distance
* between the two keyframes. However this isn't always
* possible - in these situations we use 0-100.
*/
let absoluteDelta = 100;
if (numKeyframes === 2 &&
isNumberKeyframesArray(valueKeyframesAsList)) {
const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
absoluteDelta = Math.abs(delta);
}
const springTransition = { ...remainingTransition };
if (duration !== undefined) {
springTransition.duration = motionUtils.secondsToMilliseconds(duration);
}
const springEasing = motionDom.createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
ease = springEasing.ease;
duration = springEasing.duration;
}
duration ?? (duration = defaultDuration);
const startTime = currentTime + calculatedDelay;
/**
* If there's only one time offset of 0, fill in a second with length 1
*/
if (times.length === 1 && times[0] === 0) {
times[1] = 1;
}
/**
* Fill out if offset if fewer offsets than keyframes
*/
const remainder = times.length - valueKeyframesAsList.length;
remainder > 0 && fillOffset(times, 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.
*/
valueKeyframesAsList.length === 1 &&
valueKeyframesAsList.unshift(null);
/**
* Handle repeat options
*/
if (repeat) {
motionUtils.invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20");
duration = calculateRepeatDuration(duration, repeat);
const originalKeyframes = [...valueKeyframesAsList];
const originalTimes = [...times];
ease = Array.isArray(ease) ? [...ease] : [ease];
const originalEase = [...ease];
for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
valueKeyframesAsList.push(...originalKeyframes);
for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) {
times.push(originalTimes[keyframeIndex] + (repeatIndex + 1));
ease.push(keyframeIndex === 0
? "linear"
: getEasingForSegment(originalEase, keyframeIndex - 1));
}
}
normalizeTimes(times, repeat);
}
const targetTime = startTime + duration;
/**
* Add keyframes, mapping offsets to absolute time.
*/
addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
maxDuration = Math.max(calculatedDelay + duration, maxDuration);
totalDuration = Math.max(targetTime, totalDuration);
};
if (isMotionValue(subject)) {
const subjectSequence = getSubjectSequence(subject, sequences);
resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
}
else {
const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
const numSubjects = subjects.length;
/**
* For every element in this segment, process the defined values.
*/
for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
/**
* Cast necessary, but we know these are of this type
*/
keyframes = keyframes;
transition = transition;
const thisSubject = subjects[subjectIndex];
const subjectSequence = getSubjectSequence(thisSubject, sequences);
for (const key in keyframes) {
resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
}
}
}
prevTime = currentTime;
currentTime += maxDuration;
}
/**
* For every element and value combination create a new animation.
*/
sequences.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(motionUtils.progress(0, totalDuration, at));
valueEasing.push(easing || "easeOut");
}
/**
* If the first keyframe doesn't land on offset: 0
* provide one by duplicating the initial keyframe. This ensures
* it snaps to the first keyframe when the animation starts.
*/
if (valueOffset[0] !== 0) {
valueOffset.unshift(0);
keyframes.unshift(keyframes[0]);
valueEasing.unshift(defaultSegmentEasing);
}
/**
* If the last keyframe doesn't land on offset: 1
* 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);
}
if (!animationDefinitions.has(element)) {
animationDefinitions.set(element, {
keyframes: {},
transition: {},
});
}
const definition = animationDefinitions.get(element);
definition.keyframes[key] = keyframes;
definition.transition[key] = {
...defaultTransition,
duration: totalDuration,
ease: valueEasing,
times: valueOffset,
...sequenceTransition,
};
}
});
return animationDefinitions;
}
function getSubjectSequence(subject, sequences) {
!sequences.has(subject) && sequences.set(subject, {});
return sequences.get(subject);
}
function getValueSequence(name, sequences) {
if (!sequences[name])
sequences[name] = [];
return sequences[name];
}
function keyframesAsList(keyframes) {
return Array.isArray(keyframes) ? keyframes : [keyframes];
}
function getValueTransition(transition, key) {
return transition && transition[key]
? {
...transition,
...transition[key],
}
: { ...transition };
}
const isNumber = (keyframe) => typeof keyframe === "number";
const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
const visualElementStore = new WeakMap();
/**
* Generate a list of every possible transform key.
*/
const transformPropOrder = [
"transformPerspective",
"x",
"y",
"z",
"translateX",
"translateY",
"translateZ",
"scale",
"scaleX",
"scaleY",
"rotate",
"rotateX",
"rotateY",
"rotateZ",
"skew",
"skewX",
"skewY",
];
/**
* A quick lookup for transform props.
*/
const transformProps = new Set(transformPropOrder);
const positionalKeys = new Set([
"width",
"height",
"top",
"left",
"right",
"bottom",
...transformPropOrder,
]);
const isKeyframesTarget = (v) => {
return Array.isArray(v);
};
const resolveFinalValueInKeyframes = (v) => {
// TODO maybe throw if v.length - 1 is placeholder token?
return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v;
};
function getValueState(visualElement) {
const state = [{}, {}];
visualElement?.values.forEach((value, key) => {
state[0][key] = value.get();
state[1][key] = value.getVelocity();
});
return state;
}
function resolveVariantFromProps(props, definition, custom, visualElement) {
/**
* If the variant definition is a function, resolve.
*/
if (typeof definition === "function") {
const [current, velocity] = getValueState(visualElement);
definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
}
/**
* If the variant definition is a variant label, or
* the function returned a variant label, resolve.
*/
if (typeof definition === "string") {
definition = props.variants && props.variants[definition];
}
/**
* At this point we've resolved both functions and variant labels,
* but the resolved variant label might itself have been a function.
* If so, resolve. This can only have returned a valid target object.
*/
if (typeof definition === "function") {
const [current, velocity] = getValueState(visualElement);
definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
}
return definition;
}
function resolveVariant(visualElement, definition, custom) {
const props = visualElement.getProps();
return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
}
/**
* Set VisualElement's MotionValue, creating a new MotionValue for it if
* it doesn't exist.
*/
function setMotionValue(visualElement, key, value) {
if (visualElement.hasValue(key)) {
visualElement.getValue(key).set(value);
}
else {
visualElement.addValue(key, motionDom.motionValue(value));
}
}
function setTarget(visualElement, definition) {
const resolved = resolveVariant(visualElement, definition);
let { transitionEnd = {}, transition = {}, ...target } = resolved || {};
target = { ...target, ...transitionEnd };
for (const key in target) {
const value = resolveFinalValueInKeyframes(target[key]);
setMotionValue(visualElement, key, value);
}
}
function isWillChangeMotionValue(value) {
return Boolean(isMotionValue(value) && value.add);
}
function addValueToWillChange(visualElement, key) {
const willChange = visualElement.getValue("willChange");
/**
* It could be that a user has set willChange to a regular MotionValue,
* in which case we can't add the value to it.
*/
if (isWillChangeMotionValue(willChange)) {
return willChange.add(key);
}
else if (!willChange && motionUtils.MotionGlobalConfig.WillChange) {
const newWillChange = new motionUtils.MotionGlobalConfig.WillChange("auto");
visualElement.addValue("willChange", newWillChange);
newWillChange.add(key);
}
}
/**
* Convert camelCase to dash-case properties.
*/
const camelToDash = (str) => str.replace(/([a-z])([A-Z])/gu, "$1-$2").toLowerCase();
const optimizedAppearDataId = "framerAppearId";
const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId);
function getOptimisedAppearId(visualElement) {
return visualElement.props[optimizedAppearDataAttribute];
}
/*
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 motionUtils.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);
/**
* Check if the value is a zero value string like "0px" or "0%"
*/
const isZeroValueString = (v) => /^0[^.\s]+$/u.test(v);
function isNone(value) {
if (typeof value === "number") {
return value === 0;
}
else if (value !== null) {
return value === "none" || value === "0" || isZeroValueString(value);
}
else {
return true;
}
}
const number = {
test: (v) => typeof v === "number",
parse: parseFloat,
transform: (v) => v,
};
const alpha = {
...number,
transform: (v) => clamp(0, 1, v),
};
const scale = {
...number,
default: 1,
};
// If this number is a decimal, make it just five decimal places
// to avoid exponents
const sanitize = (v) => Math.round(v * 100000) / 100000;
const floatRegex = /-?(?:\d+(?:\.\d+)?|\.\d+)/gu;
function isNullish(v) {
return v == null;
}
const singleColorRegex = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))$/iu;
/**
* Returns true if the provided string is a color, ie rgba(0,0,0,0) or #000,
* but false if a number or multiple colors
*/
const isColorString = (type, testProp) => (v) => {
return Boolean((typeof v === "string" &&
singleColorRegex.test(v) &&
v.startsWith(type)) ||
(testProp &&
!isNullish(v) &&
Object.prototype.hasOwnProperty.call(v, testProp)));
};
const splitColor = (aName, bName, cName) => (v) => {
if (typeof v !== "string")
return v;
const [a, b, c, alpha] = v.match(floatRegex);
return {
[aName]: parseFloat(a),
[bName]: parseFloat(b),
[cName]: parseFloat(c),
alpha: alpha !== undefined ? parseFloat(alpha) : 1,
};
};
const clampRgbUnit = (v) => clamp(0, 255, v);
const rgbUnit = {
...number,
transform: (v) => Math.round(clampRgbUnit(v)),
};
const rgba = {
test: /*@__PURE__*/ isColorString("rgb", "red"),
parse: /*@__PURE__*/ splitColor("red", "green", "blue"),
transform: ({ red, green, blue, alpha: alpha$1 = 1 }) => "rgba(" +
rgbUnit.transform(red) +
", " +
rgbUnit.transform(green) +
", " +
rgbUnit.transform(blue) +
", " +
sanitize(alpha.transform(alpha$1)) +
")",
};
function parseHex(v) {
let r = "";
let g = "";
let b = "";
let a = "";
// If we have 6 characters, ie #FF0000
if (v.length > 5) {
r = v.substring(1, 3);
g = v.substring(3, 5);
b = v.substring(5, 7);
a = v.substring(7, 9);
// Or we have 3 characters, ie #F00
}
else {
r = v.substring(1, 2);
g = v.substring(2, 3);
b = v.substring(3, 4);
a = v.substring(4, 5);
r += r;
g += g;
b += b;
a += a;
}
return {
red: parseInt(r, 16),
green: parseInt(g, 16),
blue: parseInt(b, 16),
alpha: a ? parseInt(a, 16) / 255 : 1,
};
}
const hex = {
test: /*@__PURE__*/ isColorString("#"),
parse: parseHex,
transform: rgba.transform,
};
const createUnitType = (unit) => ({
test: (v) => typeof v === "string" && v.endsWith(unit) && v.split(" ").length === 1,
parse: parseFloat,
transform: (v) => `${v}${unit}`,
});
const degrees = /*@__PURE__*/ createUnitType("deg");
const percent = /*@__PURE__*/ createUnitType("%");
const px = /*@__PURE__*/ createUnitType("px");
const vh = /*@__PURE__*/ createUnitType("vh");
const vw = /*@__PURE__*/ createUnitType("vw");
const progressPercentage = {
...percent,
parse: (v) => percent.parse(v) / 100,
transform: (v) => percent.transform(v * 100),
};
const hsla = {
test: /*@__PURE__*/ isColorString("hsl", "hue"),
parse: /*@__PURE__*/ splitColor("hue", "saturation", "lightness"),
transform: ({ hue, saturation, lightness, alpha: alpha$1 = 1 }) => {
return ("hsla(" +
Math.round(hue) +
", " +
percent.transform(sanitize(saturation)) +
", " +
percent.transform(sanitize(lightness)) +
", " +
sanitize(alpha.transform(alpha$1)) +
")");
},
};
const color = {
test: (v) => rgba.test(v) || hex.test(v) || hsla.test(v),
parse: (v) => {
if (rgba.test(v)) {
return rgba.parse(v);
}
else if (hsla.test(v)) {
return hsla.parse(v);
}
else {
return hex.parse(v);
}
},
transform: (v) => {
return typeof v === "string"
? v
: v.hasOwnProperty("red")
? rgba.transform(v)
: hsla.transform(v);
},
};
const colorRegex = /(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))/giu;
function test(v) {
return (isNaN(v) &&
typeof v === "string" &&
(v.match(floatRegex)?.length || 0) +
(v.match(colorRegex)?.length || 0) >
0);
}
const NUMBER_TOKEN = "number";
const COLOR_TOKEN = "color";
const VAR_TOKEN = "var";
const VAR_FUNCTION_TOKEN = "var(";
const SPLIT_TOKEN = "${}";
// this regex consists of the `singleCssVariableRegex|rgbHSLValueRegex|digitRegex`
const complexRegex = /var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu;
function analyseComplexValue(value) {
const originalValue = value.toString();
const values = [];
const indexes = {
color: [],
number: [],
var: [],
};
const types = [];
let i = 0;
const tokenised = originalValue.replace(complexRegex, (parsedValue) => {
if (color.test(parsedValue)) {
indexes.color.push(i);
types.push(COLOR_TOKEN);
values.push(color.parse(parsedValue));
}
else if (parsedValue.startsWith(VAR_FUNCTION_TOKEN)) {
indexes.var.push(i);
types.push(VAR_TOKEN);
values.push(parsedValue);
}
else {
indexes.number.push(i);
types.push(NUMBER_TOKEN);
values.push(parseFloat(parsedValue));
}
++i;
return SPLIT_TOKEN;
});
const split = tokenised.split(SPLIT_TOKEN);
return { values, split, indexes, types };
}
function parseComplexValue(v) {
return analyseComplexValue(v).values;
}
function createTransformer(source) {
const { split, types } = analyseComplexValue(source);
const numSections = split.length;
return (v) => {
let output = "";
for (let i = 0; i < numSections; i++) {
output += split[i];
if (v[i] !== undefined) {
const type = types[i];
if (type === NUMBER_TOKEN) {
output += sanitize(v[i]);
}
else if (type === COLOR_TOKEN) {
output += color.transform(v[i]);
}
else {
output += v[i];
}
}
}
return output;
};
}
const convertNumbersToZero = (v) => typeof v === "number" ? 0 : v;
function getAnimatableNone$1(v) {
const parsed = parseComplexValue(v);
const transformer = createTransformer(v);
return transformer(parsed.map(convertNumbersToZero));
}
const complex = {
test,
parse: parseComplexValue,
createTransformer,
getAnimatableNone: getAnimatableNone$1,
};
/**
* Properties that should default to 1 or 100%
*/
const maxDefaults = new Set(["brightness", "contrast", "saturate", "opacity"]);
function applyDefaultFilter(v) {
const [name, value] = v.slice(0, -1).split("(");
if (name === "drop-shadow")
return v;
const [number] = value.match(floatRegex) || [];
if (!number)
return v;
const unit = value.replace(number, "");
let defaultValue = maxDefaults.has(name) ? 1 : 0;
if (number !== value)
defaultValue *= 100;
return name + "(" + defaultValue + unit + ")";
}
const functionRegex = /\b([a-z-]*)\(.*?\)/gu;
const filter = {
...complex,
getAnimatableNone: (v) => {
const functions = v.match(functionRegex);
return functions ? functions.map(applyDefaultFilter).join(" ") : v;
},
};
const browserNumberValueTypes = {
// Border props
borderWidth: px,
borderTopWidth: px,
borderRightWidth: px,
borderBottomWidth: px,
borderLeftWidth: px,
borderRadius: px,
radius: px,
borderTopLeftRadius: px,
borderTopRightRadius: px,
borderBottomRightRadius: px,
borderBottomLeftRadius: px,
// Positioning props
width: px,
maxWidth: px,
height: px,
maxHeight: px,
top: px,
right: px,
bottom: px,
left: px,
// Spacing props
padding: px,
paddingTop: px,
paddingRight: px,
paddingBottom: px,
paddingLeft: px,
margin: px,
marginTop: px,
marginRight: px,
marginBottom: px,
marginLeft: px,
// Misc
backgroundPositionX: px,
backgroundPositionY: px,
};
const transformValueTypes = {
rotate: degrees,
rotateX: degrees,
rotateY: degrees,
rotateZ: degrees,
scale,
scaleX: scale,
scaleY: scale,
scaleZ: scale,
skew: degrees,
skewX: degrees,
skewY: degrees,
distance: px,
translateX: px,
translateY: px,
translateZ: px,
x: px,
y: px,
z: px,
perspective: px,
transformPerspective: px,
opacity: alpha,
originX: progressPercentage,
originY: progressPercentage,
originZ: px,
};
const int = {
...number,
transform: Math.round,
};
const numberValueTypes = {
...browserNumberValueTypes,
...transformValueTypes,
zIndex: int,
size: px,
// SVG
fillOpacity: alpha,
strokeOpacity: alpha,
numOctaves: int,
};
/**
* A map of default value types for common values
*/
const defaultValueTypes = {
...numberValueTypes,
// Color props
color,
backgroundColor: color,
outlineColor: color,
fill: color,
stroke: color,
// Border props
borderColor: color,
borderTopColor: color,
borderRightColor: color,
borderBottomColor: color,
borderLeftColor: color,
filter,
WebkitFilter: filter,
};
/**
* Gets the default ValueType for the provided value key
*/
const getDefaultValueType = (key) => defaultValueTypes[key];
function getAnimatableNone(key, value) {
let defaultValueType = getDefaultValueType(key);
if (defaultValueType !== filter)
defaultValueType = complex;
// If value is not recognised as animatable, ie "none", create an animatable version origin based on the target
return defaultValueType.getAnimatableNone
? defaultValueType.getAnimatableNone(value)
: undefined;
}
/**
* If we encounter keyframes like "none" or "0" and we also have keyframes like
* "#fff" or "200px 200px" we want to find a keyframe to serve as a template for
* the "none" keyframes. In this case "#fff" or "200px 200px" - then these get turned into
* zero equivalents, i.e. "#fff0" or "0px 0px".
*/
const invalidTemplates = new Set(["auto", "none", "0"]);
function makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name) {
let i = 0;
let animatableTemplate = undefined;
while (i < unresolvedKeyframes.length && !animatableTemplate) {
const keyframe = unresolvedKeyframes[i];
if (typeof keyframe === "string" &&
!invalidTemplates.has(keyframe) &&
analyseComplexValue(keyframe).values.length) {
animatableTemplate = unresolvedKeyframes[i];
}
i++;
}
if (animatableTemplate && name) {
for (const noneIndex of noneKeyframeIndexes) {
unresolvedKeyframes[noneIndex] = getAnimatableNone(name, animatableTemplate);
}
}
}
const radToDeg = (rad) => (rad * 180) / Math.PI;
const rotate = (v) => {
const angle = radToDeg(Math.atan2(v[1], v[0]));
return rebaseAngle(angle);
};
const matrix2dParsers = {
x: 4,
y: 5,
translateX: 4,
translateY: 5,
scaleX: 0,
scaleY: 3,
scale: (v) => (Math.abs(v[0]) + Math.abs(v[3])) / 2,
rotate,
rotateZ: rotate,
skewX: (v) => radToDeg(Math.atan(v[1])),
skewY: (v) => radToDeg(Math.atan(v[2])),
skew: (v) => (Math.abs(v[1]) + Math.abs(v[2])) / 2,
};
const rebaseAngle = (angle) => {
angle = angle % 360;
if (angle < 0)
angle += 360;
return angle;
};
const rotateZ = rotate;
const scaleX = (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1]);
const scaleY = (v) => Math.sqrt(v[4] * v[4] + v[5] * v[5]);
const matrix3dParsers = {
x: 12,
y: 13,
z: 14,
translateX: 12,
translateY: 13,
translateZ: 14,
scaleX,
scaleY,
scale: (v) => (scaleX(v) + scaleY(v)) / 2,
rotateX: (v) => rebaseAngle(radToDeg(Math.atan2(v[6], v[5]))),
rotateY: (v) => rebaseAngle(radToDeg(Math.atan2(-v[2], v[0]))),
rotateZ,
rotate: rotateZ,
skewX: (v) => radToDeg(Math.atan(v[4])),
skewY: (v) => radToDeg(Math.atan(v[1])),
skew: (v) => (Math.abs(v[1]) + Math.abs(v[4])) / 2,
};
function defaultTransformValue(name) {
return name.includes("scale") ? 1 : 0;
}
function parseValueFromTransform(transform, name) {
if (!transform || transform === "none") {
return defaultTransformValue(name);
}
const matrix3dMatch = transform.match(/^matrix3d\(([-\d.e\s,]+)\)$/u);
let parsers;
let match;
if (matrix3dMatch) {
parsers = matrix3dParsers;
match = matrix3dMatch;
}
else {
const matrix2dMatch = transform.match(/^matrix\(([-\d.e\s,]+)\)$/u);
parsers = matrix2dParsers;
match = matrix2dMatch;
}
if (!match) {
return defaultTransformValue(name);
}
const valueParser = parsers[name];
const values = match[1].split(",").map(convertTransformToNumber);
return typeof valueParser === "function"
? valueParser(values)
: values[valueParser];
}
const readTransformValue = (instance, name) => {
const { transform = "none" } = getComputedStyle(instance);
return parseValueFromTransform(transform, name);
};
function convertTransformToNumber(value) {
return parseFloat(value.trim());
}
const isNumOrPxType = (v) => v === number || v === px;
const transformKeys = new Set(["x", "y", "z"]);
const nonTranslationalTransformKeys = transformPropOrder.filter((key) => !transformKeys.has(key));
function removeNonTranslationalTransform(visualElement) {
const removedTransforms = [];
nonTranslationalTransformKeys.forEach((key) => {
const value = visualElement.getValue(key);
if (value !== undefined) {
removedTransforms.push([key, value.get()]);
value.set(key.startsWith("scale") ? 1 : 0);
}
});
return removedTransforms;
}
const positionalValues = {
// Dimensions
width: ({ x }, { paddingLeft = "0", paddingRight = "0" }) => x.max - x.min - parseFloat(paddingLeft) - parseFloat(paddingRight),
height: ({ y }, { paddingTop = "0", paddingBottom = "0" }) => y.max - y.min - parseFloat(paddingTop) - parseFloat(paddingBottom),
top: (_bbox, { top }) => parseFloat(top),
left: (_bbox, { left }) => parseFloat(left),
bottom: ({ y }, { top }) => parseFloat(top) + (y.max - y.min),
right: ({ x }, { left }) => parseFloat(left) + (x.max - x.min),
// Transform
x: (_bbox, { transform }) => parseValueFromTransform(transform, "x"),
y: (_bbox, { transform }) => parseValueFromTransform(transform, "y"),
};
// Alias translate longform names
positionalValues.translateX = positionalValues.x;
positionalValues.translateY = positionalValues.y;
const toResolve = new Set();
let isScheduled = false;
let anyNeedsMeasurement = false;
function measureAllKeyframes() {
if (anyNeedsMeasurement) {
const resolversToMeasure = Array.from(toResolve).filter((resolver) => resolver.needsMeasurement);
const elementsToMeasure = new Set(resolversToMeasure.map((resolver) => resolver.element));
const transformsToRestore = new Map();
/**
* Write pass
* If we're measuring elements we want to remove bounding box-changing transforms.
*/
elementsToMeasure.forEach((element) => {
const removedTransforms = removeNonTranslationalTransform(element);
if (!removedTransforms.length)
return;
transformsToRestore.set(element, removedTransforms);
element.render();
});
// Read
resolversToMeasure.forEach((resolver) => resolver.measureInitialState());
// Write
elementsToMeasu