KeyframesResolver.mjs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { frame } from 'motion-dom';
  2. import { removeNonTranslationalTransform } from '../dom/utils/unit-conversion.mjs';
  3. const toResolve = new Set();
  4. let isScheduled = false;
  5. let anyNeedsMeasurement = false;
  6. function measureAllKeyframes() {
  7. if (anyNeedsMeasurement) {
  8. const resolversToMeasure = Array.from(toResolve).filter((resolver) => resolver.needsMeasurement);
  9. const elementsToMeasure = new Set(resolversToMeasure.map((resolver) => resolver.element));
  10. const transformsToRestore = new Map();
  11. /**
  12. * Write pass
  13. * If we're measuring elements we want to remove bounding box-changing transforms.
  14. */
  15. elementsToMeasure.forEach((element) => {
  16. const removedTransforms = removeNonTranslationalTransform(element);
  17. if (!removedTransforms.length)
  18. return;
  19. transformsToRestore.set(element, removedTransforms);
  20. element.render();
  21. });
  22. // Read
  23. resolversToMeasure.forEach((resolver) => resolver.measureInitialState());
  24. // Write
  25. elementsToMeasure.forEach((element) => {
  26. element.render();
  27. const restore = transformsToRestore.get(element);
  28. if (restore) {
  29. restore.forEach(([key, value]) => {
  30. element.getValue(key)?.set(value);
  31. });
  32. }
  33. });
  34. // Read
  35. resolversToMeasure.forEach((resolver) => resolver.measureEndState());
  36. // Write
  37. resolversToMeasure.forEach((resolver) => {
  38. if (resolver.suspendedScrollY !== undefined) {
  39. window.scrollTo(0, resolver.suspendedScrollY);
  40. }
  41. });
  42. }
  43. anyNeedsMeasurement = false;
  44. isScheduled = false;
  45. toResolve.forEach((resolver) => resolver.complete());
  46. toResolve.clear();
  47. }
  48. function readAllKeyframes() {
  49. toResolve.forEach((resolver) => {
  50. resolver.readKeyframes();
  51. if (resolver.needsMeasurement) {
  52. anyNeedsMeasurement = true;
  53. }
  54. });
  55. }
  56. function flushKeyframeResolvers() {
  57. readAllKeyframes();
  58. measureAllKeyframes();
  59. }
  60. class KeyframeResolver {
  61. constructor(unresolvedKeyframes, onComplete, name, motionValue, element, isAsync = false) {
  62. /**
  63. * Track whether this resolver has completed. Once complete, it never
  64. * needs to attempt keyframe resolution again.
  65. */
  66. this.isComplete = false;
  67. /**
  68. * Track whether this resolver is async. If it is, it'll be added to the
  69. * resolver queue and flushed in the next frame. Resolvers that aren't going
  70. * to trigger read/write thrashing don't need to be async.
  71. */
  72. this.isAsync = false;
  73. /**
  74. * Track whether this resolver needs to perform a measurement
  75. * to resolve its keyframes.
  76. */
  77. this.needsMeasurement = false;
  78. /**
  79. * Track whether this resolver is currently scheduled to resolve
  80. * to allow it to be cancelled and resumed externally.
  81. */
  82. this.isScheduled = false;
  83. this.unresolvedKeyframes = [...unresolvedKeyframes];
  84. this.onComplete = onComplete;
  85. this.name = name;
  86. this.motionValue = motionValue;
  87. this.element = element;
  88. this.isAsync = isAsync;
  89. }
  90. scheduleResolve() {
  91. this.isScheduled = true;
  92. if (this.isAsync) {
  93. toResolve.add(this);
  94. if (!isScheduled) {
  95. isScheduled = true;
  96. frame.read(readAllKeyframes);
  97. frame.resolveKeyframes(measureAllKeyframes);
  98. }
  99. }
  100. else {
  101. this.readKeyframes();
  102. this.complete();
  103. }
  104. }
  105. readKeyframes() {
  106. const { unresolvedKeyframes, name, element, motionValue } = this;
  107. /**
  108. * If a keyframe is null, we hydrate it either by reading it from
  109. * the instance, or propagating from previous keyframes.
  110. */
  111. for (let i = 0; i < unresolvedKeyframes.length; i++) {
  112. if (unresolvedKeyframes[i] === null) {
  113. /**
  114. * If the first keyframe is null, we need to find its value by sampling the element
  115. */
  116. if (i === 0) {
  117. const currentValue = motionValue?.get();
  118. const finalKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1];
  119. if (currentValue !== undefined) {
  120. unresolvedKeyframes[0] = currentValue;
  121. }
  122. else if (element && name) {
  123. const valueAsRead = element.readValue(name, finalKeyframe);
  124. if (valueAsRead !== undefined && valueAsRead !== null) {
  125. unresolvedKeyframes[0] = valueAsRead;
  126. }
  127. }
  128. if (unresolvedKeyframes[0] === undefined) {
  129. unresolvedKeyframes[0] = finalKeyframe;
  130. }
  131. if (motionValue && currentValue === undefined) {
  132. motionValue.set(unresolvedKeyframes[0]);
  133. }
  134. }
  135. else {
  136. unresolvedKeyframes[i] = unresolvedKeyframes[i - 1];
  137. }
  138. }
  139. }
  140. }
  141. setFinalKeyframe() { }
  142. measureInitialState() { }
  143. renderEndStyles() { }
  144. measureEndState() { }
  145. complete() {
  146. this.isComplete = true;
  147. this.onComplete(this.unresolvedKeyframes, this.finalKeyframe);
  148. toResolve.delete(this);
  149. }
  150. cancel() {
  151. if (!this.isComplete) {
  152. this.isScheduled = false;
  153. toResolve.delete(this);
  154. }
  155. }
  156. resume() {
  157. if (!this.isComplete)
  158. this.scheduleResolve();
  159. }
  160. }
  161. export { KeyframeResolver, flushKeyframeResolvers };