inertia.mjs 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. import { spring } from './spring/index.mjs';
  2. import { calcGeneratorVelocity } from './utils/velocity.mjs';
  3. function inertia({ keyframes, velocity = 0.0, power = 0.8, timeConstant = 325, bounceDamping = 10, bounceStiffness = 500, modifyTarget, min, max, restDelta = 0.5, restSpeed, }) {
  4. const origin = keyframes[0];
  5. const state = {
  6. done: false,
  7. value: origin,
  8. };
  9. const isOutOfBounds = (v) => (min !== undefined && v < min) || (max !== undefined && v > max);
  10. const nearestBoundary = (v) => {
  11. if (min === undefined)
  12. return max;
  13. if (max === undefined)
  14. return min;
  15. return Math.abs(min - v) < Math.abs(max - v) ? min : max;
  16. };
  17. let amplitude = power * velocity;
  18. const ideal = origin + amplitude;
  19. const target = modifyTarget === undefined ? ideal : modifyTarget(ideal);
  20. /**
  21. * If the target has changed we need to re-calculate the amplitude, otherwise
  22. * the animation will start from the wrong position.
  23. */
  24. if (target !== ideal)
  25. amplitude = target - origin;
  26. const calcDelta = (t) => -amplitude * Math.exp(-t / timeConstant);
  27. const calcLatest = (t) => target + calcDelta(t);
  28. const applyFriction = (t) => {
  29. const delta = calcDelta(t);
  30. const latest = calcLatest(t);
  31. state.done = Math.abs(delta) <= restDelta;
  32. state.value = state.done ? target : latest;
  33. };
  34. /**
  35. * Ideally this would resolve for t in a stateless way, we could
  36. * do that by always precalculating the animation but as we know
  37. * this will be done anyway we can assume that spring will
  38. * be discovered during that.
  39. */
  40. let timeReachedBoundary;
  41. let spring$1;
  42. const checkCatchBoundary = (t) => {
  43. if (!isOutOfBounds(state.value))
  44. return;
  45. timeReachedBoundary = t;
  46. spring$1 = spring({
  47. keyframes: [state.value, nearestBoundary(state.value)],
  48. velocity: calcGeneratorVelocity(calcLatest, t, state.value), // TODO: This should be passing * 1000
  49. damping: bounceDamping,
  50. stiffness: bounceStiffness,
  51. restDelta,
  52. restSpeed,
  53. });
  54. };
  55. checkCatchBoundary(0);
  56. return {
  57. calculatedDuration: null,
  58. next: (t) => {
  59. /**
  60. * We need to resolve the friction to figure out if we need a
  61. * spring but we don't want to do this twice per frame. So here
  62. * we flag if we updated for this frame and later if we did
  63. * we can skip doing it again.
  64. */
  65. let hasUpdatedFrame = false;
  66. if (!spring$1 && timeReachedBoundary === undefined) {
  67. hasUpdatedFrame = true;
  68. applyFriction(t);
  69. checkCatchBoundary(t);
  70. }
  71. /**
  72. * If we have a spring and the provided t is beyond the moment the friction
  73. * animation crossed the min/max boundary, use the spring.
  74. */
  75. if (timeReachedBoundary !== undefined && t >= timeReachedBoundary) {
  76. return spring$1.next(t - timeReachedBoundary);
  77. }
  78. else {
  79. !hasUpdatedFrame && applyFriction(t);
  80. return state;
  81. }
  82. },
  83. };
  84. }
  85. export { inertia };