create.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import { isGenerator, createGeneratorEasing } from 'motion-dom';
  2. import { progress, secondsToMilliseconds, invariant } from 'motion-utils';
  3. import { getEasingForSegment } from '../../easing/utils/get-easing-for-segment.mjs';
  4. import { defaultOffset } from '../../utils/offsets/default.mjs';
  5. import { fillOffset } from '../../utils/offsets/fill.mjs';
  6. import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
  7. import { resolveSubjects } from '../animate/resolve-subjects.mjs';
  8. import { calculateRepeatDuration } from './utils/calc-repeat-duration.mjs';
  9. import { calcNextTime } from './utils/calc-time.mjs';
  10. import { addKeyframes } from './utils/edit.mjs';
  11. import { normalizeTimes } from './utils/normalize-times.mjs';
  12. import { compareByTime } from './utils/sort.mjs';
  13. const defaultSegmentEasing = "easeInOut";
  14. const MAX_REPEAT = 20;
  15. function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
  16. const defaultDuration = defaultTransition.duration || 0.3;
  17. const animationDefinitions = new Map();
  18. const sequences = new Map();
  19. const elementCache = {};
  20. const timeLabels = new Map();
  21. let prevTime = 0;
  22. let currentTime = 0;
  23. let totalDuration = 0;
  24. /**
  25. * Build the timeline by mapping over the sequence array and converting
  26. * the definitions into keyframes and offsets with absolute time values.
  27. * These will later get converted into relative offsets in a second pass.
  28. */
  29. for (let i = 0; i < sequence.length; i++) {
  30. const segment = sequence[i];
  31. /**
  32. * If this is a timeline label, mark it and skip the rest of this iteration.
  33. */
  34. if (typeof segment === "string") {
  35. timeLabels.set(segment, currentTime);
  36. continue;
  37. }
  38. else if (!Array.isArray(segment)) {
  39. timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels));
  40. continue;
  41. }
  42. let [subject, keyframes, transition = {}] = segment;
  43. /**
  44. * If a relative or absolute time value has been specified we need to resolve
  45. * it in relation to the currentTime.
  46. */
  47. if (transition.at !== undefined) {
  48. currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels);
  49. }
  50. /**
  51. * Keep track of the maximum duration in this definition. This will be
  52. * applied to currentTime once the definition has been parsed.
  53. */
  54. let maxDuration = 0;
  55. const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
  56. const valueKeyframesAsList = keyframesAsList(valueKeyframes);
  57. const { delay = 0, times = defaultOffset(valueKeyframesAsList), type = "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
  58. let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
  59. /**
  60. * Resolve stagger() if defined.
  61. */
  62. const calculatedDelay = typeof delay === "function"
  63. ? delay(elementIndex, numSubjects)
  64. : delay;
  65. /**
  66. * If this animation should and can use a spring, generate a spring easing function.
  67. */
  68. const numKeyframes = valueKeyframesAsList.length;
  69. const createGenerator = isGenerator(type)
  70. ? type
  71. : generators?.[type];
  72. if (numKeyframes <= 2 && createGenerator) {
  73. /**
  74. * As we're creating an easing function from a spring,
  75. * ideally we want to generate it using the real distance
  76. * between the two keyframes. However this isn't always
  77. * possible - in these situations we use 0-100.
  78. */
  79. let absoluteDelta = 100;
  80. if (numKeyframes === 2 &&
  81. isNumberKeyframesArray(valueKeyframesAsList)) {
  82. const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
  83. absoluteDelta = Math.abs(delta);
  84. }
  85. const springTransition = { ...remainingTransition };
  86. if (duration !== undefined) {
  87. springTransition.duration = secondsToMilliseconds(duration);
  88. }
  89. const springEasing = createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
  90. ease = springEasing.ease;
  91. duration = springEasing.duration;
  92. }
  93. duration ?? (duration = defaultDuration);
  94. const startTime = currentTime + calculatedDelay;
  95. /**
  96. * If there's only one time offset of 0, fill in a second with length 1
  97. */
  98. if (times.length === 1 && times[0] === 0) {
  99. times[1] = 1;
  100. }
  101. /**
  102. * Fill out if offset if fewer offsets than keyframes
  103. */
  104. const remainder = times.length - valueKeyframesAsList.length;
  105. remainder > 0 && fillOffset(times, remainder);
  106. /**
  107. * If only one value has been set, ie [1], push a null to the start of
  108. * the keyframe array. This will let us mark a keyframe at this point
  109. * that will later be hydrated with the previous value.
  110. */
  111. valueKeyframesAsList.length === 1 &&
  112. valueKeyframesAsList.unshift(null);
  113. /**
  114. * Handle repeat options
  115. */
  116. if (repeat) {
  117. invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20");
  118. duration = calculateRepeatDuration(duration, repeat);
  119. const originalKeyframes = [...valueKeyframesAsList];
  120. const originalTimes = [...times];
  121. ease = Array.isArray(ease) ? [...ease] : [ease];
  122. const originalEase = [...ease];
  123. for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
  124. valueKeyframesAsList.push(...originalKeyframes);
  125. for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) {
  126. times.push(originalTimes[keyframeIndex] + (repeatIndex + 1));
  127. ease.push(keyframeIndex === 0
  128. ? "linear"
  129. : getEasingForSegment(originalEase, keyframeIndex - 1));
  130. }
  131. }
  132. normalizeTimes(times, repeat);
  133. }
  134. const targetTime = startTime + duration;
  135. /**
  136. * Add keyframes, mapping offsets to absolute time.
  137. */
  138. addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
  139. maxDuration = Math.max(calculatedDelay + duration, maxDuration);
  140. totalDuration = Math.max(targetTime, totalDuration);
  141. };
  142. if (isMotionValue(subject)) {
  143. const subjectSequence = getSubjectSequence(subject, sequences);
  144. resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
  145. }
  146. else {
  147. const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
  148. const numSubjects = subjects.length;
  149. /**
  150. * For every element in this segment, process the defined values.
  151. */
  152. for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
  153. /**
  154. * Cast necessary, but we know these are of this type
  155. */
  156. keyframes = keyframes;
  157. transition = transition;
  158. const thisSubject = subjects[subjectIndex];
  159. const subjectSequence = getSubjectSequence(thisSubject, sequences);
  160. for (const key in keyframes) {
  161. resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
  162. }
  163. }
  164. }
  165. prevTime = currentTime;
  166. currentTime += maxDuration;
  167. }
  168. /**
  169. * For every element and value combination create a new animation.
  170. */
  171. sequences.forEach((valueSequences, element) => {
  172. for (const key in valueSequences) {
  173. const valueSequence = valueSequences[key];
  174. /**
  175. * Arrange all the keyframes in ascending time order.
  176. */
  177. valueSequence.sort(compareByTime);
  178. const keyframes = [];
  179. const valueOffset = [];
  180. const valueEasing = [];
  181. /**
  182. * For each keyframe, translate absolute times into
  183. * relative offsets based on the total duration of the timeline.
  184. */
  185. for (let i = 0; i < valueSequence.length; i++) {
  186. const { at, value, easing } = valueSequence[i];
  187. keyframes.push(value);
  188. valueOffset.push(progress(0, totalDuration, at));
  189. valueEasing.push(easing || "easeOut");
  190. }
  191. /**
  192. * If the first keyframe doesn't land on offset: 0
  193. * provide one by duplicating the initial keyframe. This ensures
  194. * it snaps to the first keyframe when the animation starts.
  195. */
  196. if (valueOffset[0] !== 0) {
  197. valueOffset.unshift(0);
  198. keyframes.unshift(keyframes[0]);
  199. valueEasing.unshift(defaultSegmentEasing);
  200. }
  201. /**
  202. * If the last keyframe doesn't land on offset: 1
  203. * provide one with a null wildcard value. This will ensure it
  204. * stays static until the end of the animation.
  205. */
  206. if (valueOffset[valueOffset.length - 1] !== 1) {
  207. valueOffset.push(1);
  208. keyframes.push(null);
  209. }
  210. if (!animationDefinitions.has(element)) {
  211. animationDefinitions.set(element, {
  212. keyframes: {},
  213. transition: {},
  214. });
  215. }
  216. const definition = animationDefinitions.get(element);
  217. definition.keyframes[key] = keyframes;
  218. definition.transition[key] = {
  219. ...defaultTransition,
  220. duration: totalDuration,
  221. ease: valueEasing,
  222. times: valueOffset,
  223. ...sequenceTransition,
  224. };
  225. }
  226. });
  227. return animationDefinitions;
  228. }
  229. function getSubjectSequence(subject, sequences) {
  230. !sequences.has(subject) && sequences.set(subject, {});
  231. return sequences.get(subject);
  232. }
  233. function getValueSequence(name, sequences) {
  234. if (!sequences[name])
  235. sequences[name] = [];
  236. return sequences[name];
  237. }
  238. function keyframesAsList(keyframes) {
  239. return Array.isArray(keyframes) ? keyframes : [keyframes];
  240. }
  241. function getValueTransition(transition, key) {
  242. return transition && transition[key]
  243. ? {
  244. ...transition,
  245. ...transition[key],
  246. }
  247. : { ...transition };
  248. }
  249. const isNumber = (keyframe) => typeof keyframe === "number";
  250. const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
  251. export { createAnimationsFromSequence, getValueTransition };