index.mjs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { createGeneratorEasing, supportsLinearEasing, calcGeneratorDuration, maxGeneratorDuration, generateLinearEasing } from 'motion-dom';
  2. import { millisecondsToSeconds, secondsToMilliseconds } from 'motion-utils';
  3. import { clamp } from '../../../utils/clamp.mjs';
  4. import { calcGeneratorVelocity } from '../utils/velocity.mjs';
  5. import { springDefaults } from './defaults.mjs';
  6. import { findSpring, calcAngularFreq } from './find.mjs';
  7. const durationKeys = ["duration", "bounce"];
  8. const physicsKeys = ["stiffness", "damping", "mass"];
  9. function isSpringType(options, keys) {
  10. return keys.some((key) => options[key] !== undefined);
  11. }
  12. function getSpringOptions(options) {
  13. let springOptions = {
  14. velocity: springDefaults.velocity,
  15. stiffness: springDefaults.stiffness,
  16. damping: springDefaults.damping,
  17. mass: springDefaults.mass,
  18. isResolvedFromDuration: false,
  19. ...options,
  20. };
  21. // stiffness/damping/mass overrides duration/bounce
  22. if (!isSpringType(options, physicsKeys) &&
  23. isSpringType(options, durationKeys)) {
  24. if (options.visualDuration) {
  25. const visualDuration = options.visualDuration;
  26. const root = (2 * Math.PI) / (visualDuration * 1.2);
  27. const stiffness = root * root;
  28. const damping = 2 *
  29. clamp(0.05, 1, 1 - (options.bounce || 0)) *
  30. Math.sqrt(stiffness);
  31. springOptions = {
  32. ...springOptions,
  33. mass: springDefaults.mass,
  34. stiffness,
  35. damping,
  36. };
  37. }
  38. else {
  39. const derived = findSpring(options);
  40. springOptions = {
  41. ...springOptions,
  42. ...derived,
  43. mass: springDefaults.mass,
  44. };
  45. springOptions.isResolvedFromDuration = true;
  46. }
  47. }
  48. return springOptions;
  49. }
  50. function spring(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) {
  51. const options = typeof optionsOrVisualDuration !== "object"
  52. ? {
  53. visualDuration: optionsOrVisualDuration,
  54. keyframes: [0, 1],
  55. bounce,
  56. }
  57. : optionsOrVisualDuration;
  58. let { restSpeed, restDelta } = options;
  59. const origin = options.keyframes[0];
  60. const target = options.keyframes[options.keyframes.length - 1];
  61. /**
  62. * This is the Iterator-spec return value. We ensure it's mutable rather than using a generator
  63. * to reduce GC during animation.
  64. */
  65. const state = { done: false, value: origin };
  66. const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, } = getSpringOptions({
  67. ...options,
  68. velocity: -millisecondsToSeconds(options.velocity || 0),
  69. });
  70. const initialVelocity = velocity || 0.0;
  71. const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass));
  72. const initialDelta = target - origin;
  73. const undampedAngularFreq = millisecondsToSeconds(Math.sqrt(stiffness / mass));
  74. /**
  75. * If we're working on a granular scale, use smaller defaults for determining
  76. * when the spring is finished.
  77. *
  78. * These defaults have been selected emprically based on what strikes a good
  79. * ratio between feeling good and finishing as soon as changes are imperceptible.
  80. */
  81. const isGranularScale = Math.abs(initialDelta) < 5;
  82. restSpeed || (restSpeed = isGranularScale
  83. ? springDefaults.restSpeed.granular
  84. : springDefaults.restSpeed.default);
  85. restDelta || (restDelta = isGranularScale
  86. ? springDefaults.restDelta.granular
  87. : springDefaults.restDelta.default);
  88. let resolveSpring;
  89. if (dampingRatio < 1) {
  90. const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
  91. // Underdamped spring
  92. resolveSpring = (t) => {
  93. const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
  94. return (target -
  95. envelope *
  96. (((initialVelocity +
  97. dampingRatio * undampedAngularFreq * initialDelta) /
  98. angularFreq) *
  99. Math.sin(angularFreq * t) +
  100. initialDelta * Math.cos(angularFreq * t)));
  101. };
  102. }
  103. else if (dampingRatio === 1) {
  104. // Critically damped spring
  105. resolveSpring = (t) => target -
  106. Math.exp(-undampedAngularFreq * t) *
  107. (initialDelta +
  108. (initialVelocity + undampedAngularFreq * initialDelta) * t);
  109. }
  110. else {
  111. // Overdamped spring
  112. const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1);
  113. resolveSpring = (t) => {
  114. const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
  115. // When performing sinh or cosh values can hit Infinity so we cap them here
  116. const freqForT = Math.min(dampedAngularFreq * t, 300);
  117. return (target -
  118. (envelope *
  119. ((initialVelocity +
  120. dampingRatio * undampedAngularFreq * initialDelta) *
  121. Math.sinh(freqForT) +
  122. dampedAngularFreq *
  123. initialDelta *
  124. Math.cosh(freqForT))) /
  125. dampedAngularFreq);
  126. };
  127. }
  128. const generator = {
  129. calculatedDuration: isResolvedFromDuration ? duration || null : null,
  130. next: (t) => {
  131. const current = resolveSpring(t);
  132. if (!isResolvedFromDuration) {
  133. let currentVelocity = 0.0;
  134. /**
  135. * We only need to calculate velocity for under-damped springs
  136. * as over- and critically-damped springs can't overshoot, so
  137. * checking only for displacement is enough.
  138. */
  139. if (dampingRatio < 1) {
  140. currentVelocity =
  141. t === 0
  142. ? secondsToMilliseconds(initialVelocity)
  143. : calcGeneratorVelocity(resolveSpring, t, current);
  144. }
  145. const isBelowVelocityThreshold = Math.abs(currentVelocity) <= restSpeed;
  146. const isBelowDisplacementThreshold = Math.abs(target - current) <= restDelta;
  147. state.done =
  148. isBelowVelocityThreshold && isBelowDisplacementThreshold;
  149. }
  150. else {
  151. state.done = t >= duration;
  152. }
  153. state.value = state.done ? target : current;
  154. return state;
  155. },
  156. toString: () => {
  157. const calculatedDuration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
  158. const easing = generateLinearEasing((progress) => generator.next(calculatedDuration * progress).value, calculatedDuration, 30);
  159. return calculatedDuration + "ms " + easing;
  160. },
  161. toTransition: () => { },
  162. };
  163. return generator;
  164. }
  165. spring.applyToOptions = (options) => {
  166. const generatorOptions = createGeneratorEasing(options, 100, spring);
  167. options.ease = supportsLinearEasing() ? generatorOptions.ease : "easeOut";
  168. options.duration = secondsToMilliseconds(generatorOptions.duration);
  169. options.type = "keyframes";
  170. return options;
  171. };
  172. export { spring };