Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
167 lines
6.9 KiB
Plaintext
167 lines
6.9 KiB
Plaintext
import { 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;
|
|
},
|
|
};
|
|
return generator;
|
|
}
|
|
|
|
export { spring };
|