| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- 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 };
|