motion 10.1.3 → 10.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -0
  3. package/dist/main.cjs.js +4 -0
  4. package/dist/main.es.js +2 -0
  5. package/dist/motion.min.js +1 -1
  6. package/dist/motion.umd.js +524 -189
  7. package/dist/size-animate-dom.js +1 -1
  8. package/dist/size-animate-style.js +1 -1
  9. package/dist/size-react.js +1 -1
  10. package/dist/size-spring.js +1 -1
  11. package/dist/size-timeline-dom.js +1 -1
  12. package/dist/size-webpack-animate.js +1 -1
  13. package/dist/targets/dom/animate-style.cjs.js +136 -111
  14. package/dist/targets/dom/animate-style.es.js +139 -114
  15. package/dist/targets/dom/animate.cjs.js +16 -3
  16. package/dist/targets/dom/animate.es.js +17 -4
  17. package/dist/targets/dom/data.cjs.js +4 -2
  18. package/dist/targets/dom/data.es.js +4 -2
  19. package/dist/targets/dom/style.cjs.js +1 -1
  20. package/dist/targets/dom/style.es.js +2 -2
  21. package/dist/targets/dom/timeline/index.cjs.js +17 -7
  22. package/dist/targets/dom/timeline/index.es.js +18 -8
  23. package/dist/targets/dom/timeline/utils/calc-time.cjs.js +3 -1
  24. package/dist/targets/dom/timeline/utils/calc-time.es.js +3 -1
  25. package/dist/targets/dom/utils/apply.cjs.js +4 -8
  26. package/dist/targets/dom/utils/apply.es.js +3 -7
  27. package/dist/targets/dom/utils/controls.cjs.js +6 -2
  28. package/dist/targets/dom/utils/controls.es.js +6 -2
  29. package/dist/targets/dom/utils/css-var.cjs.js +2 -2
  30. package/dist/targets/dom/utils/css-var.es.js +3 -3
  31. package/dist/targets/dom/utils/easing.cjs.js +7 -2
  32. package/dist/targets/dom/utils/easing.es.js +7 -3
  33. package/dist/targets/dom/utils/feature-detection.cjs.js +4 -4
  34. package/dist/targets/dom/utils/feature-detection.es.js +4 -4
  35. package/dist/targets/dom/utils/get-style-name.cjs.js +13 -0
  36. package/dist/targets/dom/utils/get-style-name.es.js +9 -0
  37. package/dist/targets/dom/utils/keyframes.cjs.js +2 -4
  38. package/dist/targets/dom/utils/keyframes.es.js +2 -4
  39. package/dist/targets/dom/utils/options.cjs.js +1 -1
  40. package/dist/targets/dom/utils/options.es.js +1 -1
  41. package/dist/targets/dom/utils/stop-animation.cjs.js +2 -0
  42. package/dist/targets/dom/utils/stop-animation.es.js +2 -0
  43. package/dist/targets/dom/utils/transforms.cjs.js +10 -7
  44. package/dist/targets/dom/utils/transforms.es.js +10 -7
  45. package/dist/targets/js/{animate-number.cjs.js → NumberAnimation.cjs.js} +42 -29
  46. package/dist/targets/js/{animate-number.es.js → NumberAnimation.es.js} +43 -29
  47. package/dist/targets/js/easing/glide/generator.cjs.js +99 -0
  48. package/dist/targets/js/easing/glide/generator.es.js +95 -0
  49. package/dist/targets/js/easing/glide/index.cjs.js +10 -0
  50. package/dist/targets/js/easing/glide/index.es.js +6 -0
  51. package/dist/targets/js/easing/spring/generator.cjs.js +64 -0
  52. package/dist/targets/js/easing/spring/generator.es.js +57 -0
  53. package/dist/targets/js/easing/spring/index.cjs.js +10 -0
  54. package/dist/targets/js/easing/spring/index.es.js +6 -0
  55. package/dist/targets/js/easing/utils/create-generator-easing.cjs.js +71 -0
  56. package/dist/targets/js/easing/utils/create-generator-easing.es.js +67 -0
  57. package/dist/targets/js/easing/{get-function.cjs.js → utils/get-function.cjs.js} +3 -3
  58. package/dist/targets/js/easing/{get-function.es.js → utils/get-function.es.js} +3 -3
  59. package/dist/targets/js/easing/utils/has-reached-target.cjs.js +10 -0
  60. package/dist/targets/js/easing/utils/has-reached-target.es.js +6 -0
  61. package/dist/targets/js/easing/utils/pregenerate-keyframes.cjs.js +34 -0
  62. package/dist/targets/js/easing/utils/pregenerate-keyframes.es.js +30 -0
  63. package/dist/targets/react/hooks/use-animation.cjs.js +5 -2
  64. package/dist/targets/react/hooks/use-animation.es.js +5 -2
  65. package/dist/targets/react/utils/keyframes.cjs.js +5 -3
  66. package/dist/targets/react/utils/keyframes.es.js +6 -4
  67. package/dist/utils/is-number.cjs.js +7 -0
  68. package/dist/utils/is-number.es.js +3 -0
  69. package/dist/utils/stagger.cjs.js +3 -2
  70. package/dist/utils/stagger.es.js +3 -2
  71. package/dist/utils/velocity-per-second.cjs.js +15 -0
  72. package/dist/utils/velocity-per-second.es.js +11 -0
  73. package/package.json +8 -14
  74. package/types/index.d.ts +2 -0
  75. package/types/targets/dom/animate-style.d.ts +2 -2
  76. package/types/targets/dom/data.d.ts +1 -7
  77. package/types/targets/dom/style.d.ts +1 -1
  78. package/types/targets/dom/types.d.ts +24 -18
  79. package/types/targets/dom/utils/apply.d.ts +3 -2
  80. package/types/targets/dom/utils/controls.d.ts +3 -3
  81. package/types/targets/dom/utils/easing.d.ts +2 -1
  82. package/types/targets/dom/utils/get-style-name.d.ts +1 -0
  83. package/types/targets/dom/utils/keyframes.d.ts +1 -1
  84. package/types/targets/dom/utils/stop-animation.d.ts +1 -1
  85. package/types/targets/dom/utils/transforms.d.ts +2 -2
  86. package/types/targets/js/{animate-number.d.ts → NumberAnimation.d.ts} +2 -3
  87. package/types/targets/js/easing/glide/generator.d.ts +5 -0
  88. package/types/targets/js/easing/glide/index.d.ts +2 -0
  89. package/types/targets/js/easing/glide/types.d.ts +14 -0
  90. package/types/targets/js/easing/spring/generator.d.ts +6 -0
  91. package/types/targets/js/easing/spring/index.d.ts +2 -0
  92. package/types/targets/js/easing/spring/types.d.ts +10 -0
  93. package/types/targets/js/easing/utils/create-generator-easing.d.ts +3 -0
  94. package/types/targets/js/easing/{get-function.d.ts → utils/get-function.d.ts} +2 -2
  95. package/types/targets/js/easing/utils/has-reached-target.d.ts +1 -0
  96. package/types/targets/js/easing/utils/pregenerate-keyframes.d.ts +7 -0
  97. package/types/targets/js/types.d.ts +11 -0
  98. package/types/utils/is-number.d.ts +1 -0
  99. package/types/generators/index.d.ts +0 -2
  100. package/types/generators/spring/create.d.ts +0 -13
  101. package/types/generators/spring/index.d.ts +0 -2
  102. package/types/generators/types.d.ts +0 -7
@@ -8,8 +8,10 @@
8
8
  function getAnimationData(element) {
9
9
  if (!data.has(element)) {
10
10
  data.set(element, {
11
- activeTransforms: [],
12
- activeAnimations: {},
11
+ transforms: [],
12
+ animations: {},
13
+ generators: {},
14
+ prevGeneratorState: {},
13
15
  });
14
16
  }
15
17
  return data.get(element);
@@ -60,7 +62,7 @@
60
62
  },
61
63
  skew: rotation,
62
64
  };
63
- const transformPropertyDefinitions = new Map();
65
+ const transformDefinitions = new Map();
64
66
  const asTransformCssVar = (name) => `--motion-${name}`;
65
67
  /**
66
68
  * Generate a list of every possible transform key
@@ -69,7 +71,7 @@
69
71
  order.forEach((name) => {
70
72
  axes.forEach((axis) => {
71
73
  transforms.push(name + axis);
72
- transformPropertyDefinitions.set(asTransformCssVar(name + axis), baseTransformProperties[name]);
74
+ transformDefinitions.set(asTransformCssVar(name + axis), baseTransformProperties[name]);
73
75
  });
74
76
  });
75
77
  /**
@@ -82,11 +84,14 @@
82
84
  const transformLookup = new Set(transforms);
83
85
  const isTransform = (name) => transformLookup.has(name);
84
86
  const addTransformToElement = (element, name) => {
85
- const { activeTransforms } = getAnimationData(element);
86
- addUniqueItem(activeTransforms, name);
87
- element.style.transform = buildTransformTemplate(activeTransforms);
87
+ // Map x to translateX etc
88
+ if (transformAlias[name])
89
+ name = transformAlias[name];
90
+ const { transforms } = getAnimationData(element);
91
+ addUniqueItem(transforms, name);
92
+ element.style.transform = buildTransformTemplate(transforms);
88
93
  };
89
- const buildTransformTemplate = (activeTransforms) => activeTransforms
94
+ const buildTransformTemplate = (transforms) => transforms
90
95
  .sort(compareTransformOrder)
91
96
  .reduce(transformListToString, "")
92
97
  .trim();
@@ -99,8 +104,8 @@
99
104
  return;
100
105
  registeredProperties.add(name);
101
106
  try {
102
- const { syntax, initialValue } = transformPropertyDefinitions.has(name)
103
- ? transformPropertyDefinitions.get(name)
107
+ const { syntax, initialValue } = transformDefinitions.has(name)
108
+ ? transformDefinitions.get(name)
104
109
  : {};
105
110
  CSS.registerProperty({
106
111
  name,
@@ -114,20 +119,12 @@
114
119
 
115
120
  const ms = (seconds) => seconds * 1000;
116
121
 
117
- function stopAnimation(animation) {
118
- // Suppress error thrown by WAAPI
119
- try {
120
- /**
121
- * commitStyles has overhead so we only want to commit and cancel
122
- */
123
- animation.playState !== "finished" && animation.commitStyles();
124
- animation.cancel();
125
- }
126
- catch (e) { }
127
- }
122
+ const isNumber = (value) => typeof value === "number";
128
123
 
129
- const isCubicBezier = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
130
- const isEasingList = (easing) => Array.isArray(easing) && typeof easing[0] !== "number";
124
+ const isCubicBezier = (easing) => Array.isArray(easing) && isNumber(easing[0]);
125
+ const isEasingList = (easing) => Array.isArray(easing) && !isNumber(easing[0]);
126
+ const isCustomEasing = (easing) => typeof easing === "object" &&
127
+ Boolean(easing.createAnimation);
131
128
  const convertEasing = (easing) => isCubicBezier(easing) ? cubicBezierAsString(easing) : easing;
132
129
  const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
133
130
 
@@ -148,21 +145,17 @@
148
145
  finished: () => Boolean(testAnimation({ opacity: [0, 1] }).finished),
149
146
  };
150
147
  const results = {};
151
- const supports = Object.keys(featureTests).reduce((acc, key) => {
152
- acc[key] = () => {
148
+ const supports = {};
149
+ for (const key in featureTests) {
150
+ supports[key] = () => {
153
151
  if (results[key] === undefined)
154
152
  results[key] = featureTests[key]();
155
153
  return results[key];
156
154
  };
157
- return acc;
158
- }, {});
155
+ }
159
156
 
160
- const createCssVariableRenderer = (element, name) => {
161
- return (latest) => element.style.setProperty(name, latest);
162
- };
163
- const createStyleRenderer = (element, name) => {
164
- return (latest) => (element.style[name] = latest);
165
- };
157
+ const cssVariableRenderer = (element, name) => (latest) => element.style.setProperty(name, latest);
158
+ const styleRenderer = (element, name) => (latest) => (element.style[name] = latest);
166
159
 
167
160
  const defaults = {
168
161
  duration: 0.3,
@@ -346,10 +339,8 @@
346
339
  };
347
340
  }
348
341
 
349
- class Animation {
350
- constructor(output, keyframes,
351
- // TODO Merge in defaults
352
- { easing = defaults.easing, duration = defaults.duration, delay = defaults.delay, endDelay = defaults.endDelay, offset, repeat = defaults.repeat, direction = "normal", }) {
342
+ class NumberAnimation {
343
+ constructor(output, keyframes = [0, 1], { easing = defaults.easing, duration = defaults.duration, delay = defaults.delay, endDelay = defaults.endDelay, repeat = defaults.repeat, offset, direction = "normal", } = {}) {
353
344
  this.startTime = 0;
354
345
  this.rate = 1;
355
346
  this.t = 0;
@@ -360,50 +351,69 @@
360
351
  this.reject = reject;
361
352
  });
362
353
  const totalDuration = duration * (repeat + 1);
354
+ /**
355
+ * We don't currently support custom easing (spring, glide etc) in NumberAnimation
356
+ * (although this is completely possible), so this will have been hydrated by
357
+ * animateStyle.
358
+ */
359
+ if (isCustomEasing(easing))
360
+ easing = "ease";
363
361
  const interpolate = slowInterpolateNumbers(keyframes, offset, isEasingList(easing)
364
362
  ? easing.map(getEasingFunction)
365
363
  : getEasingFunction(easing));
366
364
  this.tick = (timestamp) => {
367
- if (this.playState === "finished") {
368
- const latest = interpolate(1);
369
- output(latest);
370
- this.resolve(latest);
371
- return;
372
- }
373
- if (this.pauseTime) {
365
+ if (this.pauseTime)
374
366
  timestamp = this.pauseTime;
375
- }
376
367
  let t = (timestamp - this.startTime) * this.rate;
377
368
  this.t = t;
378
369
  // Convert to seconds
379
370
  t /= 1000;
380
371
  // Rebase on delay
381
372
  t = Math.max(t - delay, 0);
373
+ /**
374
+ * If this animation has finished, set the current time
375
+ * to the total duration.
376
+ */
377
+ if (this.playState === "finished")
378
+ t = totalDuration;
379
+ /**
380
+ * Get the current progress (0-1) of the animation. If t is >
381
+ * than duration we'll get values like 2.5 (midway through the
382
+ * third iteration)
383
+ */
382
384
  const progress = t / duration;
383
385
  // TODO progress += iterationStart
386
+ /**
387
+ * Get the current iteration (0 indexed). For instance the floor of
388
+ * 2.5 is 2.
389
+ */
384
390
  let currentIteration = Math.floor(progress);
391
+ /**
392
+ * Get the current progress of the iteration by taking the remainder
393
+ * so 2.5 is 0.5 through iteration 2
394
+ */
385
395
  let iterationProgress = progress % 1.0;
386
396
  if (!iterationProgress && progress >= 1) {
387
397
  iterationProgress = 1;
388
398
  }
389
- if (iterationProgress === 1) {
390
- currentIteration--;
391
- }
392
- // Reverse progress
399
+ /**
400
+ * If iteration progress is 1 we count that as the end
401
+ * of the previous iteration.
402
+ */
403
+ iterationProgress === 1 && currentIteration--;
404
+ /**
405
+ * Reverse progress if we're not running in "normal" direction
406
+ */
393
407
  const iterationIsOdd = currentIteration % 2;
394
408
  if (direction === "reverse" ||
395
409
  (direction === "alternate" && iterationIsOdd) ||
396
410
  (direction === "alternate-reverse" && !iterationIsOdd)) {
397
411
  iterationProgress = 1 - iterationProgress;
398
412
  }
399
- const interpolationIsFinished = t >= totalDuration;
400
- const interpolationProgress = interpolationIsFinished
401
- ? 1
402
- : Math.min(iterationProgress, 1);
403
- const latest = interpolate(interpolationProgress);
413
+ const latest = interpolate(t >= totalDuration ? 1 : Math.min(iterationProgress, 1));
404
414
  output(latest);
405
- const isFinished = t >= totalDuration + endDelay;
406
- if (isFinished) {
415
+ const isAnimationFinished = this.playState === "finished" || t >= totalDuration + endDelay;
416
+ if (isAnimationFinished) {
407
417
  this.playState = "finished";
408
418
  this.resolve(latest);
409
419
  }
@@ -462,9 +472,16 @@
462
472
  this.rate = rate;
463
473
  }
464
474
  }
465
- function animateNumber(output, keyframes = [0, 1], options = {}) {
466
- return new Animation(output, keyframes, options);
475
+
476
+ function hydrateKeyframes(keyframes, readInitialValue) {
477
+ for (let i = 0; i < keyframes.length; i++) {
478
+ if (keyframes[i] === null) {
479
+ keyframes[i] = i ? keyframes[i - 1] : readInitialValue();
480
+ }
481
+ }
482
+ return keyframes;
467
483
  }
484
+ const keyframesList = (keyframes) => Array.isArray(keyframes) ? keyframes : [keyframes];
468
485
 
469
486
  const style = {
470
487
  get: (element, name) => {
@@ -472,7 +489,7 @@
472
489
  ? element.style.getPropertyValue(name)
473
490
  : getComputedStyle(element)[name];
474
491
  if (!value && value !== 0) {
475
- const definition = transformPropertyDefinitions.get(name);
492
+ const definition = transformDefinitions.get(name);
476
493
  if (definition)
477
494
  value = definition.initialValue;
478
495
  }
@@ -480,155 +497,188 @@
480
497
  },
481
498
  };
482
499
 
483
- function hydrateKeyframes(keyframes, element, name) {
484
- for (let i = 0; i < keyframes.length; i++) {
485
- if (keyframes[i] === null) {
486
- keyframes[i] = i ? keyframes[i - 1] : style.get(element, name);
487
- }
500
+ function getStyleName(key) {
501
+ if (transformAlias[key])
502
+ key = transformAlias[key];
503
+ return isTransform(key) ? asTransformCssVar(key) : key;
504
+ }
505
+
506
+ function stopAnimation(animation) {
507
+ if (!animation)
508
+ return;
509
+ // Suppress error thrown by WAAPI
510
+ try {
511
+ /**
512
+ * commitStyles has overhead so we only want to commit and cancel
513
+ */
514
+ animation.playState !== "finished" && animation.commitStyles();
515
+ animation.cancel();
488
516
  }
489
- return keyframes;
517
+ catch (e) { }
490
518
  }
491
- const keyframesList = (keyframes) => Array.isArray(keyframes) ? keyframes : [keyframes];
492
519
 
493
- function animateStyle(element, name, keyframesDefinition, options = {}) {
520
+ function animateStyle(element, key, keyframesDefinition, options = {}) {
521
+ let animation;
494
522
  let { duration = defaults.duration, delay = defaults.delay, endDelay = defaults.endDelay, repeat = defaults.repeat, easing = defaults.easing, direction, offset, allowWebkitAcceleration = false, } = options;
495
523
  const data = getAnimationData(element);
496
524
  let canAnimateNatively = supports.waapi();
497
525
  let render = noop;
498
- const valueIsTransform = isTransform(name);
526
+ const valueIsTransform = isTransform(key);
499
527
  /**
500
528
  * If this is an individual transform, we need to map its
501
529
  * key to a CSS variable and update the element's transform style
502
530
  */
503
- if (valueIsTransform) {
504
- if (transformAlias[name])
505
- name = transformAlias[name];
506
- addTransformToElement(element, name);
507
- name = asTransformCssVar(name);
508
- }
531
+ valueIsTransform && addTransformToElement(element, key);
532
+ const name = getStyleName(key);
509
533
  /**
510
534
  * Get definition of value, this will be used to convert numerical
511
535
  * keyframes into the default value type.
512
536
  */
513
- const definition = transformPropertyDefinitions.get(name);
537
+ const definition = transformDefinitions.get(name);
514
538
  /**
515
- * Replace null values with the previous keyframe value, or read
516
- * it from the DOM if it's the first keyframe.
517
- *
518
- * TODO: This needs to come after the valueIsTransform
519
- * check so it can correctly read the underlying value.
520
- * Should make a test for this.
539
+ * Stop the current animation, if any. Because this will trigger
540
+ * commitStyles (DOM writes) and we might later trigger DOM reads,
541
+ * this is fired now and we return a factory function to create
542
+ * the actual animation that can get called in batch,
521
543
  */
522
- let keyframes = hydrateKeyframes(keyframesList(keyframesDefinition), element, name);
523
- stopCurrentAnimation(data, name);
524
- /**
525
- * If this is a CSS variable we need to register it with the browser
526
- * before it can be animated natively. We also set it with setProperty
527
- * rather than directly onto the element.style object.
528
- */
529
- if (isCssVar(name)) {
530
- render = createCssVariableRenderer(element, name);
531
- if (supports.cssRegisterProperty()) {
532
- registerCssVariable(name);
533
- }
534
- else {
535
- canAnimateNatively = false;
536
- }
537
- }
538
- else {
539
- render = createStyleRenderer(element, name);
540
- }
541
- let animation;
544
+ stopAnimation(data.animations[name]);
542
545
  /**
543
- * If we can animate this value with WAAPI, do so. Currently this only
544
- * feature detects CSS.registerProperty but could check WAAPI too.
546
+ * Batchable factory function containing all DOM reads.
545
547
  */
546
- if (canAnimateNatively) {
548
+ return () => {
549
+ const readInitialValue = () => { var _a, _b; return (_b = (_a = style.get(element, name)) !== null && _a !== void 0 ? _a : definition === null || definition === void 0 ? void 0 : definition.initialValue) !== null && _b !== void 0 ? _b : 0; };
547
550
  /**
548
- * Convert numbers to default value types. Currently this only supports
549
- * transforms but it could also support other value types.
551
+ * Replace null values with the previous keyframe value, or read
552
+ * it from the DOM if it's the first keyframe.
550
553
  */
551
- if (definition) {
552
- keyframes = keyframes.map((value) => typeof value === "number" ? definition.toDefaultUnit(value) : value);
553
- }
554
- if (!supports.partialKeyframes() && keyframes.length === 1) {
555
- keyframes.unshift(style.get(element, name));
554
+ let keyframes = hydrateKeyframes(keyframesList(keyframesDefinition), readInitialValue);
555
+ if (isCustomEasing(easing)) {
556
+ const custom = easing.createAnimation(keyframes, readInitialValue, valueIsTransform, name, data);
557
+ easing = custom.easing;
558
+ if (custom.keyframes !== undefined)
559
+ keyframes = custom.keyframes;
560
+ if (custom.duration !== undefined)
561
+ duration = custom.duration;
556
562
  }
557
- const animationOptions = {
558
- delay: ms(delay),
559
- duration: ms(duration),
560
- endDelay: ms(endDelay),
561
- easing: !isEasingList(easing) ? convertEasing(easing) : undefined,
562
- direction,
563
- iterations: repeat + 1,
564
- fill: "both",
565
- };
566
- animation = element.animate({
567
- [name]: keyframes,
568
- offset,
569
- easing: isEasingList(easing) ? easing.map(convertEasing) : undefined,
570
- }, animationOptions);
571
563
  /**
572
- * Polyfill finished Promise in browsers that don't support it
564
+ * If this is a CSS variable we need to register it with the browser
565
+ * before it can be animated natively. We also set it with setProperty
566
+ * rather than directly onto the element.style object.
573
567
  */
574
- if (!animation.finished) {
575
- animation.finished = new Promise((resolve, reject) => {
576
- animation.onfinish = resolve;
577
- animation.oncancel = reject;
578
- });
568
+ if (isCssVar(name)) {
569
+ render = cssVariableRenderer(element, name);
570
+ if (supports.cssRegisterProperty()) {
571
+ registerCssVariable(name);
572
+ }
573
+ else {
574
+ canAnimateNatively = false;
575
+ }
576
+ }
577
+ else {
578
+ render = styleRenderer(element, name);
579
579
  }
580
- const target = keyframes[keyframes.length - 1];
581
- animation.finished.then(() => render(target)).catch(noop);
582
580
  /**
583
- * This forces Webkit to run animations on the main thread by exploiting
584
- * this condition:
585
- * https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp?rev=281238#L1099
586
- *
587
- * This fixes Webkit's timing bugs, like accelerated animations falling
588
- * out of sync with main thread animations and massive delays in starting
589
- * accelerated animations in WKWebView.
581
+ * If we can animate this value with WAAPI, do so. Currently this only
582
+ * feature detects CSS.registerProperty but could check WAAPI too.
590
583
  */
591
- if (!allowWebkitAcceleration)
592
- animation.playbackRate = 1.000001;
593
- }
594
- else if (valueIsTransform && keyframes.every(isNumber)) {
595
- if (keyframes.length === 1) {
596
- keyframes.unshift(style.get(element, name) || (definition === null || definition === void 0 ? void 0 : definition.initialValue) || 0);
584
+ if (canAnimateNatively) {
585
+ /**
586
+ * Convert numbers to default value types. Currently this only supports
587
+ * transforms but it could also support other value types.
588
+ */
589
+ if (definition) {
590
+ keyframes = keyframes.map((value) => isNumber(value) ? definition.toDefaultUnit(value) : value);
591
+ }
592
+ /**
593
+ * If this browser doesn't support partial/implicit keyframes we need to
594
+ * explicitly provide one.
595
+ */
596
+ if (!supports.partialKeyframes() && keyframes.length === 1) {
597
+ keyframes.unshift(readInitialValue());
598
+ }
599
+ const animationOptions = {
600
+ delay: ms(delay),
601
+ duration: ms(duration),
602
+ endDelay: ms(endDelay),
603
+ easing: !isEasingList(easing) ? convertEasing(easing) : undefined,
604
+ direction,
605
+ iterations: repeat + 1,
606
+ fill: "both",
607
+ };
608
+ animation = element.animate({
609
+ [name]: keyframes,
610
+ offset,
611
+ easing: isEasingList(easing) ? easing.map(convertEasing) : undefined,
612
+ }, animationOptions);
613
+ /**
614
+ * Polyfill finished Promise in browsers that don't support it
615
+ */
616
+ if (!animation.finished) {
617
+ animation.finished = new Promise((resolve, reject) => {
618
+ animation.onfinish = resolve;
619
+ animation.oncancel = reject;
620
+ });
621
+ }
622
+ const target = keyframes[keyframes.length - 1];
623
+ animation.finished
624
+ .then(() => {
625
+ // Apply styles to target
626
+ render(target);
627
+ // Ensure fill modes don't persist
628
+ animation.cancel();
629
+ })
630
+ .catch(noop);
631
+ /**
632
+ * This forces Webkit to run animations on the main thread by exploiting
633
+ * this condition:
634
+ * https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp?rev=281238#L1099
635
+ *
636
+ * This fixes Webkit's timing bugs, like accelerated animations falling
637
+ * out of sync with main thread animations and massive delays in starting
638
+ * accelerated animations in WKWebView.
639
+ */
640
+ if (!allowWebkitAcceleration)
641
+ animation.playbackRate = 1.000001;
642
+ /**
643
+ * If we can't animate the value natively then we can fallback to the numbers-only
644
+ * polyfill for transforms. All keyframes must be numerical.
645
+ */
597
646
  }
647
+ else if (valueIsTransform && keyframes.every(isNumber)) {
648
+ /**
649
+ * If we only have a single keyframe, we need to create an initial keyframe by reading
650
+ * the current value from the DOM.
651
+ */
652
+ if (keyframes.length === 1) {
653
+ keyframes.unshift(parseFloat(readInitialValue()));
654
+ }
655
+ if (definition) {
656
+ const applyStyle = render;
657
+ render = (v) => applyStyle(definition.toDefaultUnit(v));
658
+ }
659
+ animation = new NumberAnimation(render, keyframes, Object.assign(Object.assign({}, options), { duration,
660
+ easing }));
661
+ }
662
+ else {
663
+ const target = keyframes[keyframes.length - 1];
664
+ render(definition && isNumber(target)
665
+ ? definition.toDefaultUnit(target)
666
+ : target);
667
+ }
668
+ data.animations[name] = animation;
598
669
  /**
599
- * Transform styles are currently only accepted as numbers of
600
- * their default value type, so here we loop through and map
601
- * them to numbers.
670
+ * When an animation finishes, delete the reference to the previous animation.
602
671
  */
603
- keyframes = keyframes.map((value) => typeof value === "string" ? parseFloat(value) : value);
604
- if (definition) {
605
- const applyStyle = render;
606
- render = (v) => applyStyle(definition.toDefaultUnit(v));
607
- }
608
- animation = animateNumber(render, keyframes, options);
609
- }
610
- else {
611
- const target = keyframes[keyframes.length - 1];
612
- render(definition && typeof target === "number"
613
- ? definition.toDefaultUnit(target)
614
- : target);
615
- }
616
- data.activeAnimations[name] = animation;
617
- /**
618
- * When an animation finishes, delete the reference to the previous animation.
619
- */
620
- animation === null || animation === void 0 ? void 0 : animation.finished.then(() => (data.activeAnimations[name] = undefined)).catch(noop);
621
- return animation;
622
- }
623
- function stopCurrentAnimation(data, name) {
624
- if (data.activeAnimations[name]) {
625
- stopAnimation(data.activeAnimations[name]);
626
- data.activeAnimations[name] = undefined;
627
- }
672
+ animation === null || animation === void 0 ? void 0 : animation.finished.then(() => {
673
+ data.animations[name] = undefined;
674
+ data.generators[name] = undefined;
675
+ data.prevGeneratorState[name] = undefined;
676
+ }).catch(noop);
677
+ return animation;
678
+ };
628
679
  }
629
- const isNumber = (value) => typeof value === "number";
630
680
 
631
- const getOptions = (options, key) => options[key] ? Object.assign(Object.assign({}, options), options[key]) : Object.assign({}, options);
681
+ const getOptions = (options, key) => options[key] ? Object.assign(Object.assign({}, options), options[key]) : options;
632
682
 
633
683
  function resolveElements(elements, selectorCache) {
634
684
  var _a;
@@ -647,7 +697,11 @@
647
697
  return Array.from(elements);
648
698
  }
649
699
 
650
- const createAnimationControls = (animations, duration) => new Proxy({ animations, duration }, controls);
700
+ const createAnimation = (factory) => factory();
701
+ const createAnimations = (animationFactory, duration) => new Proxy({
702
+ animations: animationFactory.map(createAnimation).filter(Boolean),
703
+ duration,
704
+ }, controls);
651
705
  /**
652
706
  * TODO:
653
707
  * Currently this returns the first animation, ideally it would return
@@ -694,7 +748,7 @@
694
748
 
695
749
  function stagger(duration = 0.1, { start = 0, from = 0, easing } = {}) {
696
750
  return (i, total) => {
697
- const fromIndex = typeof from === "number" ? from : getFromIndex(from, total);
751
+ const fromIndex = isNumber(from) ? from : getFromIndex(from, total);
698
752
  const distance = Math.abs(fromIndex - i);
699
753
  let delay = duration * distance;
700
754
  if (easing) {
@@ -723,18 +777,31 @@
723
777
  function animate(elements, keyframes, options = {}) {
724
778
  var _a;
725
779
  elements = resolveElements(elements);
726
- const animations = [];
727
780
  const numElements = elements.length;
781
+ /**
782
+ * Create and start new animations
783
+ */
784
+ const animationFactories = [];
728
785
  for (let i = 0; i < numElements; i++) {
729
786
  const element = elements[i];
730
787
  for (const key in keyframes) {
731
788
  const valueOptions = getOptions(options, key);
732
789
  valueOptions.delay = resolveOption(valueOptions.delay, i, numElements);
733
790
  const animation = animateStyle(element, key, keyframes[key], valueOptions);
734
- animation && animations.push(animation);
791
+ animationFactories.push(animation);
735
792
  }
736
793
  }
737
- return createAnimationControls(animations, (_a = options.duration) !== null && _a !== void 0 ? _a : defaults.duration);
794
+ return createAnimations(animationFactories,
795
+ /**
796
+ * TODO:
797
+ * If easing is set to spring or glide, duration will be dynamically
798
+ * generated. Ideally we would dynamically generate this from
799
+ * animation.effect.getComputedTiming().duration but this isn't
800
+ * supported in iOS13 or our number polyfill. Perhaps it's possible
801
+ * to Proxy animations returned from animateStyle that has duration
802
+ * as a getter.
803
+ */
804
+ (_a = options.duration) !== null && _a !== void 0 ? _a : defaults.duration);
738
805
  }
739
806
 
740
807
  /*! *****************************************************************************
@@ -766,7 +833,7 @@
766
833
 
767
834
  function calcNextTime(current, next, prev, labels) {
768
835
  var _a;
769
- if (typeof next === "number") {
836
+ if (isNumber(next)) {
770
837
  return next;
771
838
  }
772
839
  else if (next.startsWith("-") || next.startsWith("+")) {
@@ -817,13 +884,14 @@
817
884
 
818
885
  function timeline(definition, options = {}) {
819
886
  var _a, _b;
820
- const animations = [];
821
887
  const animationDefinitions = createAnimationsFromTimeline(definition, options);
822
- for (let i = 0; i < animationDefinitions.length; i++) {
823
- const animation = animateStyle(...animationDefinitions[i]);
824
- animation && animations.push(animation);
825
- }
826
- return createAnimationControls(animations,
888
+ /**
889
+ * Create and start animations
890
+ */
891
+ const animationFactories = animationDefinitions
892
+ .map((definition) => animateStyle(...definition))
893
+ .filter(Boolean);
894
+ return createAnimations(animationFactories,
827
895
  // Get the duration from the first animation definition
828
896
  (_b = (_a = animationDefinitions[0]) === null || _a === void 0 ? void 0 : _a[3].duration) !== null && _b !== void 0 ? _b : defaults.duration);
829
897
  }
@@ -868,10 +936,19 @@
868
936
  const valueSequence = getValueSequence(key, elementSequence);
869
937
  const valueKeyframes = keyframesList(keyframes[key]);
870
938
  const valueOptions = getOptions(options, key);
871
- const { duration = defaultOptions.duration || defaults.duration, easing = defaultOptions.easing || defaults.easing, offset = defaultOffset(valueKeyframes.length), } = valueOptions;
939
+ let { duration = defaultOptions.duration || defaults.duration, easing = defaultOptions.easing || defaults.easing, } = valueOptions;
872
940
  const delay = resolveOption(options.delay, elementIndex, numElements) || 0;
873
941
  const startTime = currentTime + delay;
874
942
  const targetTime = startTime + duration;
943
+ /**
944
+ *
945
+ */
946
+ let { offset = defaultOffset(valueKeyframes.length) } = valueOptions;
947
+ /**
948
+ * If there's only one offset of 0, fill in a second with length 1
949
+ *
950
+ * TODO: Ensure there's a test that covers this removal
951
+ */
875
952
  if (offset.length === 1 && offset[0] === 0) {
876
953
  offset[1] = 1;
877
954
  }
@@ -959,8 +1036,266 @@
959
1036
  return sequences[name];
960
1037
  }
961
1038
 
1039
+ /*
1040
+ Convert velocity into velocity per second
1041
+
1042
+ @param [number]: Unit per frame
1043
+ @param [number]: Frame duration in ms
1044
+ */
1045
+ function velocityPerSecond(velocity, frameDuration) {
1046
+ return frameDuration ? velocity * (1000 / frameDuration) : 0;
1047
+ }
1048
+
1049
+ function hasReachedTarget(origin, target, current) {
1050
+ return ((origin < target && current >= target) ||
1051
+ (origin > target && current <= target));
1052
+ }
1053
+
1054
+ const defaultStiffness = 100.0;
1055
+ const defaultDamping = 10.0;
1056
+ const defaultMass = 1.0;
1057
+ const calcDampingRatio = (stiffness = defaultStiffness, damping = defaultDamping, mass = defaultMass) => damping / (2 * Math.sqrt(stiffness * mass));
1058
+ const calcAngularFreq = (undampedFreq, dampingRatio) => undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio);
1059
+ const createSpringGenerator = ({ stiffness = defaultStiffness, damping = defaultDamping, mass = defaultMass, from = 0, to = 1, velocity = 0.0, restSpeed = 2, restDistance = 0.5, } = {}) => {
1060
+ velocity = velocity ? velocity / 1000 : 0.0;
1061
+ const state = {
1062
+ done: false,
1063
+ value: from,
1064
+ target: to,
1065
+ velocity,
1066
+ hasReachedTarget: false,
1067
+ };
1068
+ const dampingRatio = calcDampingRatio(stiffness, damping, mass);
1069
+ const initialDelta = to - from;
1070
+ const undampedAngularFreq = Math.sqrt(stiffness / mass) / 1000;
1071
+ const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
1072
+ let resolveSpring;
1073
+ if (dampingRatio < 1) {
1074
+ // Underdamped spring (bouncy)
1075
+ resolveSpring = (t) => to -
1076
+ Math.exp(-dampingRatio * undampedAngularFreq * t) *
1077
+ (((-velocity + dampingRatio * undampedAngularFreq * initialDelta) /
1078
+ angularFreq) *
1079
+ Math.sin(angularFreq * t) +
1080
+ initialDelta * Math.cos(angularFreq * t));
1081
+ }
1082
+ else {
1083
+ // Critically damped spring
1084
+ resolveSpring = (t) => to -
1085
+ Math.exp(-undampedAngularFreq * t) *
1086
+ (initialDelta + (velocity + undampedAngularFreq * initialDelta) * t);
1087
+ }
1088
+ return {
1089
+ next: (t) => {
1090
+ state.value = resolveSpring(t);
1091
+ state.velocity =
1092
+ t === 0 ? velocity : calcVelocity(resolveSpring, t, state.value);
1093
+ const isBelowVelocityThreshold = Math.abs(state.velocity) <= restSpeed;
1094
+ const isBelowDisplacementThreshold = Math.abs(to - state.value) <= restDistance;
1095
+ state.done = isBelowVelocityThreshold && isBelowDisplacementThreshold;
1096
+ state.hasReachedTarget = hasReachedTarget(from, to, state.value);
1097
+ return state;
1098
+ },
1099
+ };
1100
+ };
1101
+ const sampleT = 5; // ms
1102
+ function calcVelocity(resolveValue, t, current) {
1103
+ const prevT = Math.max(t - sampleT, 0);
1104
+ return velocityPerSecond(current - resolveValue(prevT), 5);
1105
+ }
1106
+
1107
+ const timeStep = 10;
1108
+ const maxDuration = 10000;
1109
+ function pregenerateKeyframes(generator) {
1110
+ let overshootDuration = undefined;
1111
+ let timestamp = timeStep;
1112
+ let state = generator.next(0);
1113
+ const keyframes = [state.value];
1114
+ while (!state.done && timestamp < maxDuration) {
1115
+ state = generator.next(timestamp);
1116
+ keyframes.push(state.done ? state.target : state.value);
1117
+ if (overshootDuration === undefined && state.hasReachedTarget) {
1118
+ overshootDuration = timestamp;
1119
+ }
1120
+ timestamp += timeStep;
1121
+ }
1122
+ const duration = timestamp - timeStep;
1123
+ /**
1124
+ * If generating an animation that didn't actually move,
1125
+ * generate a second keyframe so we have an origin and target.
1126
+ */
1127
+ if (keyframes.length === 1)
1128
+ keyframes.push(state.value);
1129
+ return {
1130
+ keyframes,
1131
+ duration: duration / 1000,
1132
+ overshootDuration: (overshootDuration !== null && overshootDuration !== void 0 ? overshootDuration : duration) / 1000,
1133
+ };
1134
+ }
1135
+
1136
+ function createGeneratorEasing(createGenerator) {
1137
+ const keyframesCache = new WeakMap();
1138
+ return (options = {}) => {
1139
+ const generatorCache = new Map();
1140
+ const getGenerator = (from = 0, to = 100, velocity = 0, isScale = false) => {
1141
+ const key = `${from}-${to}-${velocity}-${isScale}`;
1142
+ if (!generatorCache.has(key)) {
1143
+ generatorCache.set(key, createGenerator(Object.assign({ from,
1144
+ to,
1145
+ velocity, restSpeed: isScale ? 0.05 : 2, restDistance: isScale ? 0.01 : 0.5 }, options)));
1146
+ }
1147
+ return generatorCache.get(key);
1148
+ };
1149
+ const getKeyframes = (generator) => {
1150
+ if (!keyframesCache.has(generator)) {
1151
+ keyframesCache.set(generator, pregenerateKeyframes(generator));
1152
+ }
1153
+ return keyframesCache.get(generator);
1154
+ };
1155
+ return {
1156
+ createAnimation: (keyframes, getOrigin, canUseGenerator, name, data) => {
1157
+ let settings;
1158
+ let generator;
1159
+ const numKeyframes = keyframes.length;
1160
+ let shouldUseGenerator = canUseGenerator &&
1161
+ numKeyframes <= 2 &&
1162
+ keyframes.every(isNumberOrNull);
1163
+ if (shouldUseGenerator) {
1164
+ const prevAnimationState = name && data && data.prevGeneratorState[name];
1165
+ const velocity = prevAnimationState &&
1166
+ (numKeyframes === 1 ||
1167
+ (numKeyframes === 2 && keyframes[0] === null))
1168
+ ? prevAnimationState.velocity
1169
+ : 0;
1170
+ const target = keyframes[numKeyframes - 1];
1171
+ const unresolvedOrigin = numKeyframes === 1 ? null : keyframes[0];
1172
+ const origin = unresolvedOrigin === null
1173
+ ? prevAnimationState
1174
+ ? prevAnimationState.value
1175
+ : parseFloat(getOrigin())
1176
+ : unresolvedOrigin;
1177
+ generator = getGenerator(origin, target, velocity, name === null || name === void 0 ? void 0 : name.includes("scale"));
1178
+ const keyframesMetadata = getKeyframes(generator);
1179
+ settings = Object.assign(Object.assign({}, keyframesMetadata), { easing: "linear" });
1180
+ }
1181
+ else {
1182
+ generator = getGenerator(0, 100);
1183
+ const keyframesMetadata = getKeyframes(generator);
1184
+ settings = {
1185
+ easing: "ease",
1186
+ duration: keyframesMetadata.overshootDuration,
1187
+ };
1188
+ }
1189
+ // TODO Add test for this
1190
+ if (generator && data && name) {
1191
+ data.generators[name] = generator;
1192
+ }
1193
+ return settings;
1194
+ },
1195
+ };
1196
+ };
1197
+ }
1198
+ const isNumberOrNull = (value) => typeof value !== "string";
1199
+
1200
+ const spring = createGeneratorEasing(createSpringGenerator);
1201
+
1202
+ const createGlideGenerator = ({ from = 0, velocity = 0.0, power = 0.8, decay = 0.325, bounceDamping, bounceStiffness, changeTarget, min, max, restDistance = 0.5, restSpeed, }) => {
1203
+ decay = ms(decay);
1204
+ const state = {
1205
+ value: from,
1206
+ target: from,
1207
+ velocity,
1208
+ hasReachedTarget: false,
1209
+ done: false,
1210
+ };
1211
+ const isOutOfBounds = (v) => (min !== undefined && v < min) || (max !== undefined && v > max);
1212
+ const nearestBoundary = (v) => {
1213
+ if (min === undefined)
1214
+ return max;
1215
+ if (max === undefined)
1216
+ return min;
1217
+ return Math.abs(min - v) < Math.abs(max - v) ? min : max;
1218
+ };
1219
+ let amplitude = power * velocity;
1220
+ const ideal = from + amplitude;
1221
+ const target = changeTarget === undefined ? ideal : changeTarget(ideal);
1222
+ state.target = target;
1223
+ /**
1224
+ * If the target has changed we need to re-calculate the amplitude, otherwise
1225
+ * the animation will start from the wrong position.
1226
+ */
1227
+ if (target !== ideal)
1228
+ amplitude = target - from;
1229
+ const calcDelta = (t) => -amplitude * Math.exp(-t / decay);
1230
+ const calcLatest = (t) => target + calcDelta(t);
1231
+ const applyFriction = (t) => {
1232
+ const delta = calcDelta(t);
1233
+ const latest = calcLatest(t);
1234
+ state.done = Math.abs(delta) <= restDistance;
1235
+ state.value = state.done ? target : latest;
1236
+ state.velocity =
1237
+ t === 0 ? velocity : calcVelocity(calcLatest, t, state.value);
1238
+ };
1239
+ /**
1240
+ * Ideally this would resolve for t in a stateless way, we could
1241
+ * do that by always precalculating the animation but as we know
1242
+ * this will be done anyway we can assume that spring will
1243
+ * be discovered during that.
1244
+ */
1245
+ let timeReachedBoundary;
1246
+ let spring;
1247
+ const checkCatchBoundary = (t) => {
1248
+ if (!isOutOfBounds(state.value))
1249
+ return;
1250
+ timeReachedBoundary = t;
1251
+ spring = createSpringGenerator({
1252
+ from: state.value,
1253
+ to: nearestBoundary(state.value),
1254
+ velocity: state.velocity,
1255
+ damping: bounceDamping,
1256
+ stiffness: bounceStiffness,
1257
+ restDistance,
1258
+ restSpeed,
1259
+ });
1260
+ };
1261
+ checkCatchBoundary(0);
1262
+ return {
1263
+ next: (t) => {
1264
+ /**
1265
+ * We need to resolve the friction to figure out if we need a
1266
+ * spring but we don't want to do this twice per frame. So here
1267
+ * we flag if we updated for this frame and later if we did
1268
+ * we can skip doing it again.
1269
+ */
1270
+ let hasUpdatedFrame = false;
1271
+ if (!spring && timeReachedBoundary === undefined) {
1272
+ hasUpdatedFrame = true;
1273
+ applyFriction(t);
1274
+ checkCatchBoundary(t);
1275
+ }
1276
+ /**
1277
+ * If we have a spring and the provided t is beyond the moment the friction
1278
+ * animation crossed the min/max boundary, use the spring.
1279
+ */
1280
+ if (timeReachedBoundary !== undefined && t > timeReachedBoundary) {
1281
+ state.hasReachedTarget = true;
1282
+ return spring.next(t - timeReachedBoundary);
1283
+ }
1284
+ else {
1285
+ state.hasReachedTarget = false;
1286
+ !hasUpdatedFrame && applyFriction(t);
1287
+ return state;
1288
+ }
1289
+ },
1290
+ };
1291
+ };
1292
+
1293
+ const glide = createGeneratorEasing(createGlideGenerator);
1294
+
962
1295
  exports.animate = animate;
963
1296
  exports.animateStyle = animateStyle;
1297
+ exports.glide = glide;
1298
+ exports.spring = spring;
964
1299
  exports.stagger = stagger;
965
1300
  exports.timeline = timeline;
966
1301