start.mjs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import { secondsToMilliseconds } from 'motion-utils';
  2. import { GroupAnimation } from '../animation/GroupAnimation.mjs';
  3. import { NativeAnimation } from '../animation/NativeAnimation.mjs';
  4. import { getValueTransition } from '../animation/utils/get-value-transition.mjs';
  5. import { mapEasingToNativeEasing } from '../animation/waapi/easing/map-easing.mjs';
  6. import { applyGeneratorOptions } from '../animation/waapi/utils/apply-generator.mjs';
  7. import { chooseLayerType } from './utils/choose-layer-type.mjs';
  8. import { css } from './utils/css.mjs';
  9. import { getLayerName } from './utils/get-layer-name.mjs';
  10. import { getViewAnimations } from './utils/get-view-animations.mjs';
  11. import { hasTarget } from './utils/has-target.mjs';
  12. const definitionNames = ["layout", "enter", "exit", "new", "old"];
  13. function startViewAnimation(builder) {
  14. const { update, targets, options: defaultOptions } = builder;
  15. if (!document.startViewTransition) {
  16. return new Promise(async (resolve) => {
  17. await update();
  18. resolve(new GroupAnimation([]));
  19. });
  20. }
  21. // TODO: Go over existing targets and ensure they all have ids
  22. /**
  23. * If we don't have any animations defined for the root target,
  24. * remove it from being captured.
  25. */
  26. if (!hasTarget("root", targets)) {
  27. css.set(":root", {
  28. "view-transition-name": "none",
  29. });
  30. }
  31. /**
  32. * Set the timing curve to linear for all view transition layers.
  33. * This gets baked into the keyframes, which can't be changed
  34. * without breaking the generated animation.
  35. *
  36. * This allows us to set easing via updateTiming - which can be changed.
  37. */
  38. css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
  39. css.commit(); // Write
  40. const transition = document.startViewTransition(async () => {
  41. await update();
  42. // TODO: Go over new targets and ensure they all have ids
  43. });
  44. transition.finished.finally(() => {
  45. css.remove(); // Write
  46. });
  47. return new Promise((resolve) => {
  48. transition.ready.then(() => {
  49. const generatedViewAnimations = getViewAnimations();
  50. const animations = [];
  51. /**
  52. * Create animations for each of our explicitly-defined subjects.
  53. */
  54. targets.forEach((definition, target) => {
  55. // TODO: If target is not "root", resolve elements
  56. // and iterate over each
  57. for (const key of definitionNames) {
  58. if (!definition[key])
  59. continue;
  60. const { keyframes, options } = definition[key];
  61. for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
  62. if (!valueKeyframes)
  63. continue;
  64. const valueOptions = {
  65. ...getValueTransition(defaultOptions, valueName),
  66. ...getValueTransition(options, valueName),
  67. };
  68. const type = chooseLayerType(key);
  69. /**
  70. * If this is an opacity animation, and keyframes are not an array,
  71. * we need to convert them into an array and set an initial value.
  72. */
  73. if (valueName === "opacity" &&
  74. !Array.isArray(valueKeyframes)) {
  75. const initialValue = type === "new" ? 0 : 1;
  76. valueKeyframes = [initialValue, valueKeyframes];
  77. }
  78. /**
  79. * Resolve stagger function if provided.
  80. */
  81. if (typeof valueOptions.delay === "function") {
  82. valueOptions.delay = valueOptions.delay(0, 1);
  83. }
  84. valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
  85. valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
  86. const animation = new NativeAnimation({
  87. element: document.documentElement,
  88. name: valueName,
  89. pseudoElement: `::view-transition-${type}(${target})`,
  90. keyframes: valueKeyframes,
  91. transition: valueOptions,
  92. });
  93. animations.push(animation);
  94. }
  95. }
  96. });
  97. /**
  98. * Handle browser generated animations
  99. */
  100. for (const animation of generatedViewAnimations) {
  101. if (animation.playState === "finished")
  102. continue;
  103. const { effect } = animation;
  104. if (!effect || !(effect instanceof KeyframeEffect))
  105. continue;
  106. const { pseudoElement } = effect;
  107. if (!pseudoElement)
  108. continue;
  109. const name = getLayerName(pseudoElement);
  110. if (!name)
  111. continue;
  112. const targetDefinition = targets.get(name.layer);
  113. if (!targetDefinition) {
  114. /**
  115. * If transition name is group then update the timing of the animation
  116. * whereas if it's old or new then we could possibly replace it using
  117. * the above method.
  118. */
  119. const transitionName = name.type === "group" ? "layout" : "";
  120. let animationTransition = {
  121. ...getValueTransition(defaultOptions, transitionName),
  122. };
  123. animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
  124. animationTransition =
  125. applyGeneratorOptions(animationTransition);
  126. const easing = mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
  127. effect.updateTiming({
  128. delay: secondsToMilliseconds(animationTransition.delay ?? 0),
  129. duration: animationTransition.duration,
  130. easing,
  131. });
  132. animations.push(new NativeAnimation({ animation }));
  133. }
  134. else if (hasOpacity(targetDefinition, "enter") &&
  135. hasOpacity(targetDefinition, "exit") &&
  136. effect
  137. .getKeyframes()
  138. .some((keyframe) => keyframe.mixBlendMode)) {
  139. animations.push(new NativeAnimation({ animation }));
  140. }
  141. else {
  142. animation.cancel();
  143. }
  144. }
  145. resolve(new GroupAnimation(animations));
  146. });
  147. });
  148. }
  149. function hasOpacity(target, key) {
  150. return target?.[key]?.keyframes.opacity;
  151. }
  152. export { startViewAnimation };