BaseAnimation.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { time } from 'motion-dom';
  2. import { flushKeyframeResolvers } from '../../render/utils/KeyframesResolver.mjs';
  3. import { instantAnimationState } from '../../utils/use-instant-transition-state.mjs';
  4. import { canAnimate } from './utils/can-animate.mjs';
  5. import { getFinalKeyframe } from './waapi/utils/get-final-keyframe.mjs';
  6. /**
  7. * Maximum time allowed between an animation being created and it being
  8. * resolved for us to use the latter as the start time.
  9. *
  10. * This is to ensure that while we prefer to "start" an animation as soon
  11. * as it's triggered, we also want to avoid a visual jump if there's a big delay
  12. * between these two moments.
  13. */
  14. const MAX_RESOLVE_DELAY = 40;
  15. class BaseAnimation {
  16. constructor({ autoplay = true, delay = 0, type = "keyframes", repeat = 0, repeatDelay = 0, repeatType = "loop", ...options }) {
  17. // Track whether the animation has been stopped. Stopped animations won't restart.
  18. this.isStopped = false;
  19. this.hasAttemptedResolve = false;
  20. this.createdAt = time.now();
  21. this.options = {
  22. autoplay,
  23. delay,
  24. type,
  25. repeat,
  26. repeatDelay,
  27. repeatType,
  28. ...options,
  29. };
  30. this.updateFinishedPromise();
  31. }
  32. /**
  33. * This method uses the createdAt and resolvedAt to calculate the
  34. * animation startTime. *Ideally*, we would use the createdAt time as t=0
  35. * as the following frame would then be the first frame of the animation in
  36. * progress, which would feel snappier.
  37. *
  38. * However, if there's a delay (main thread work) between the creation of
  39. * the animation and the first commited frame, we prefer to use resolvedAt
  40. * to avoid a sudden jump into the animation.
  41. */
  42. calcStartTime() {
  43. if (!this.resolvedAt)
  44. return this.createdAt;
  45. return this.resolvedAt - this.createdAt > MAX_RESOLVE_DELAY
  46. ? this.resolvedAt
  47. : this.createdAt;
  48. }
  49. /**
  50. * A getter for resolved data. If keyframes are not yet resolved, accessing
  51. * this.resolved will synchronously flush all pending keyframe resolvers.
  52. * This is a deoptimisation, but at its worst still batches read/writes.
  53. */
  54. get resolved() {
  55. if (!this._resolved && !this.hasAttemptedResolve) {
  56. flushKeyframeResolvers();
  57. }
  58. return this._resolved;
  59. }
  60. /**
  61. * A method to be called when the keyframes resolver completes. This method
  62. * will check if its possible to run the animation and, if not, skip it.
  63. * Otherwise, it will call initPlayback on the implementing class.
  64. */
  65. onKeyframesResolved(keyframes, finalKeyframe) {
  66. this.resolvedAt = time.now();
  67. this.hasAttemptedResolve = true;
  68. const { name, type, velocity, delay, onComplete, onUpdate, isGenerator, } = this.options;
  69. /**
  70. * If we can't animate this value with the resolved keyframes
  71. * then we should complete it immediately.
  72. */
  73. if (!isGenerator && !canAnimate(keyframes, name, type, velocity)) {
  74. // Finish immediately
  75. if (instantAnimationState.current || !delay) {
  76. onUpdate &&
  77. onUpdate(getFinalKeyframe(keyframes, this.options, finalKeyframe));
  78. onComplete && onComplete();
  79. this.resolveFinishedPromise();
  80. return;
  81. }
  82. // Finish after a delay
  83. else {
  84. this.options.duration = 0;
  85. }
  86. }
  87. const resolvedAnimation = this.initPlayback(keyframes, finalKeyframe);
  88. if (resolvedAnimation === false)
  89. return;
  90. this._resolved = {
  91. keyframes,
  92. finalKeyframe,
  93. ...resolvedAnimation,
  94. };
  95. this.onPostResolved();
  96. }
  97. onPostResolved() { }
  98. /**
  99. * Allows the returned animation to be awaited or promise-chained. Currently
  100. * resolves when the animation finishes at all but in a future update could/should
  101. * reject if its cancels.
  102. */
  103. then(resolve, reject) {
  104. return this.currentFinishedPromise.then(resolve, reject);
  105. }
  106. flatten() {
  107. if (!this.options.allowFlatten)
  108. return;
  109. this.options.type = "keyframes";
  110. this.options.ease = "linear";
  111. }
  112. updateFinishedPromise() {
  113. this.currentFinishedPromise = new Promise((resolve) => {
  114. this.resolveFinishedPromise = resolve;
  115. });
  116. }
  117. }
  118. export { BaseAnimation };