framer-motion
Version:
A simple and powerful JavaScript animation library
175 lines (172 loc) • 7.41 kB
JavaScript
import { createGeneratorEasing, supportsLinearEasing, calcGeneratorDuration, maxGeneratorDuration, generateLinearEasing } from 'motion-dom';
import { millisecondsToSeconds, secondsToMilliseconds } from 'motion-utils';
import { clamp } from '../../../utils/clamp.mjs';
import { calcGeneratorVelocity } from '../utils/velocity.mjs';
import { springDefaults } from './defaults.mjs';
import { findSpring, calcAngularFreq } from './find.mjs';
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: -millisecondsToSeconds(options.velocity || 0),
});
const initialVelocity = velocity || 0.0;
const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass));
const initialDelta = target - origin;
const undampedAngularFreq = 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
? 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(calcGeneratorDuration(generator), maxGeneratorDuration);
const easing = generateLinearEasing((progress) => generator.next(calculatedDuration * progress).value, calculatedDuration, 30);
return calculatedDuration + "ms " + easing;
},
toTransition: () => { },
};
return generator;
}
spring.applyToOptions = (options) => {
const generatorOptions = createGeneratorEasing(options, 100, spring);
options.ease = supportsLinearEasing() ? generatorOptions.ease : "easeOut";
options.duration = secondsToMilliseconds(generatorOptions.duration);
options.type = "keyframes";
return options;
};
export { spring };