motion 10.2.1 → 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 (90) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -0
  3. package/dist/main.cjs.js +2 -0
  4. package/dist/main.es.js +1 -0
  5. package/dist/motion.min.js +1 -1
  6. package/dist/motion.umd.js +426 -280
  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 +135 -134
  14. package/dist/targets/dom/animate-style.es.js +137 -136
  15. package/dist/targets/dom/animate.cjs.js +15 -4
  16. package/dist/targets/dom/animate.es.js +16 -5
  17. package/dist/targets/dom/data.cjs.js +4 -3
  18. package/dist/targets/dom/data.es.js +4 -3
  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 +7 -6
  22. package/dist/targets/dom/timeline/index.es.js +8 -7
  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 +4 -2
  32. package/dist/targets/dom/utils/easing.es.js +4 -2
  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} +40 -29
  46. package/dist/targets/js/{animate-number.es.js → NumberAnimation.es.js} +40 -28
  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 +9 -4
  52. package/dist/targets/js/easing/spring/generator.es.js +9 -5
  53. package/dist/targets/js/easing/spring/index.cjs.js +2 -62
  54. package/dist/targets/js/easing/spring/index.es.js +2 -62
  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/utils/has-reached-target.cjs.js +10 -0
  58. package/dist/targets/js/easing/utils/has-reached-target.es.js +6 -0
  59. package/dist/targets/js/easing/utils/pregenerate-keyframes.cjs.js +13 -10
  60. package/dist/targets/js/easing/utils/pregenerate-keyframes.es.js +13 -10
  61. package/dist/targets/react/hooks/use-animation.cjs.js +5 -2
  62. package/dist/targets/react/hooks/use-animation.es.js +5 -2
  63. package/dist/targets/react/utils/keyframes.cjs.js +5 -3
  64. package/dist/targets/react/utils/keyframes.es.js +6 -4
  65. package/dist/utils/is-number.cjs.js +7 -0
  66. package/dist/utils/is-number.es.js +3 -0
  67. package/dist/utils/stagger.cjs.js +2 -1
  68. package/dist/utils/stagger.es.js +2 -1
  69. package/package.json +4 -10
  70. package/types/index.d.ts +1 -0
  71. package/types/targets/dom/animate-style.d.ts +2 -2
  72. package/types/targets/dom/style.d.ts +1 -1
  73. package/types/targets/dom/types.d.ts +8 -4
  74. package/types/targets/dom/utils/apply.d.ts +3 -2
  75. package/types/targets/dom/utils/controls.d.ts +3 -3
  76. package/types/targets/dom/utils/get-style-name.d.ts +1 -0
  77. package/types/targets/dom/utils/keyframes.d.ts +1 -1
  78. package/types/targets/dom/utils/stop-animation.d.ts +1 -1
  79. package/types/targets/dom/utils/transforms.d.ts +2 -2
  80. package/types/targets/js/{animate-number.d.ts → NumberAnimation.d.ts} +2 -3
  81. package/types/targets/js/easing/glide/generator.d.ts +5 -0
  82. package/types/targets/js/easing/glide/index.d.ts +2 -0
  83. package/types/targets/js/easing/glide/types.d.ts +14 -0
  84. package/types/targets/js/easing/spring/generator.d.ts +1 -0
  85. package/types/targets/js/easing/spring/index.d.ts +1 -2
  86. package/types/targets/js/easing/utils/create-generator-easing.d.ts +3 -0
  87. package/types/targets/js/easing/utils/has-reached-target.d.ts +1 -0
  88. package/types/targets/js/easing/utils/pregenerate-keyframes.d.ts +1 -1
  89. package/types/targets/js/types.d.ts +3 -0
  90. package/types/utils/is-number.d.ts +1 -0
@@ -8,9 +8,10 @@
8
8
  function getAnimationData(element) {
9
9
  if (!data.has(element)) {
10
10
  data.set(element, {
11
- activeTransforms: [],
12
- activeAnimations: {},
13
- activeGenerators: {},
11
+ transforms: [],
12
+ animations: {},
13
+ generators: {},
14
+ prevGeneratorState: {},
14
15
  });
15
16
  }
16
17
  return data.get(element);
@@ -61,7 +62,7 @@
61
62
  },
62
63
  skew: rotation,
63
64
  };
64
- const transformPropertyDefinitions = new Map();
65
+ const transformDefinitions = new Map();
65
66
  const asTransformCssVar = (name) => `--motion-${name}`;
66
67
  /**
67
68
  * Generate a list of every possible transform key
@@ -70,7 +71,7 @@
70
71
  order.forEach((name) => {
71
72
  axes.forEach((axis) => {
72
73
  transforms.push(name + axis);
73
- transformPropertyDefinitions.set(asTransformCssVar(name + axis), baseTransformProperties[name]);
74
+ transformDefinitions.set(asTransformCssVar(name + axis), baseTransformProperties[name]);
74
75
  });
75
76
  });
76
77
  /**
@@ -83,11 +84,14 @@
83
84
  const transformLookup = new Set(transforms);
84
85
  const isTransform = (name) => transformLookup.has(name);
85
86
  const addTransformToElement = (element, name) => {
86
- const { activeTransforms } = getAnimationData(element);
87
- addUniqueItem(activeTransforms, name);
88
- 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);
89
93
  };
90
- const buildTransformTemplate = (activeTransforms) => activeTransforms
94
+ const buildTransformTemplate = (transforms) => transforms
91
95
  .sort(compareTransformOrder)
92
96
  .reduce(transformListToString, "")
93
97
  .trim();
@@ -100,8 +104,8 @@
100
104
  return;
101
105
  registeredProperties.add(name);
102
106
  try {
103
- const { syntax, initialValue } = transformPropertyDefinitions.has(name)
104
- ? transformPropertyDefinitions.get(name)
107
+ const { syntax, initialValue } = transformDefinitions.has(name)
108
+ ? transformDefinitions.get(name)
105
109
  : {};
106
110
  CSS.registerProperty({
107
111
  name,
@@ -115,20 +119,10 @@
115
119
 
116
120
  const ms = (seconds) => seconds * 1000;
117
121
 
118
- function stopAnimation(animation) {
119
- // Suppress error thrown by WAAPI
120
- try {
121
- /**
122
- * commitStyles has overhead so we only want to commit and cancel
123
- */
124
- animation.playState !== "finished" && animation.commitStyles();
125
- animation.cancel();
126
- }
127
- catch (e) { }
128
- }
122
+ const isNumber = (value) => typeof value === "number";
129
123
 
130
- const isCubicBezier = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
131
- 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]);
132
126
  const isCustomEasing = (easing) => typeof easing === "object" &&
133
127
  Boolean(easing.createAnimation);
134
128
  const convertEasing = (easing) => isCubicBezier(easing) ? cubicBezierAsString(easing) : easing;
@@ -151,21 +145,17 @@
151
145
  finished: () => Boolean(testAnimation({ opacity: [0, 1] }).finished),
152
146
  };
153
147
  const results = {};
154
- const supports = Object.keys(featureTests).reduce((acc, key) => {
155
- acc[key] = () => {
148
+ const supports = {};
149
+ for (const key in featureTests) {
150
+ supports[key] = () => {
156
151
  if (results[key] === undefined)
157
152
  results[key] = featureTests[key]();
158
153
  return results[key];
159
154
  };
160
- return acc;
161
- }, {});
155
+ }
162
156
 
163
- const createCssVariableRenderer = (element, name) => {
164
- return (latest) => element.style.setProperty(name, latest);
165
- };
166
- const createStyleRenderer = (element, name) => {
167
- return (latest) => (element.style[name] = latest);
168
- };
157
+ const cssVariableRenderer = (element, name) => (latest) => element.style.setProperty(name, latest);
158
+ const styleRenderer = (element, name) => (latest) => (element.style[name] = latest);
169
159
 
170
160
  const defaults = {
171
161
  duration: 0.3,
@@ -349,8 +339,8 @@
349
339
  };
350
340
  }
351
341
 
352
- class Animation {
353
- constructor(output, keyframes, { easing = defaults.easing, duration = defaults.duration, delay = defaults.delay, endDelay = defaults.endDelay, repeat = defaults.repeat, offset, 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", } = {}) {
354
344
  this.startTime = 0;
355
345
  this.rate = 1;
356
346
  this.t = 0;
@@ -361,54 +351,69 @@
361
351
  this.reject = reject;
362
352
  });
363
353
  const totalDuration = duration * (repeat + 1);
364
- // TODO: This isn't currently supported but keeps TypeScript happy
365
- if (isCustomEasing(easing)) {
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))
366
360
  easing = "ease";
367
- }
368
361
  const interpolate = slowInterpolateNumbers(keyframes, offset, isEasingList(easing)
369
362
  ? easing.map(getEasingFunction)
370
363
  : getEasingFunction(easing));
371
364
  this.tick = (timestamp) => {
372
- if (this.playState === "finished") {
373
- const latest = interpolate(1);
374
- output(latest);
375
- this.resolve(latest);
376
- return;
377
- }
378
- if (this.pauseTime) {
365
+ if (this.pauseTime)
379
366
  timestamp = this.pauseTime;
380
- }
381
367
  let t = (timestamp - this.startTime) * this.rate;
382
368
  this.t = t;
383
369
  // Convert to seconds
384
370
  t /= 1000;
385
371
  // Rebase on delay
386
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
+ */
387
384
  const progress = t / duration;
388
385
  // TODO progress += iterationStart
386
+ /**
387
+ * Get the current iteration (0 indexed). For instance the floor of
388
+ * 2.5 is 2.
389
+ */
389
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
+ */
390
395
  let iterationProgress = progress % 1.0;
391
396
  if (!iterationProgress && progress >= 1) {
392
397
  iterationProgress = 1;
393
398
  }
394
- if (iterationProgress === 1) {
395
- currentIteration--;
396
- }
397
- // 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
+ */
398
407
  const iterationIsOdd = currentIteration % 2;
399
408
  if (direction === "reverse" ||
400
409
  (direction === "alternate" && iterationIsOdd) ||
401
410
  (direction === "alternate-reverse" && !iterationIsOdd)) {
402
411
  iterationProgress = 1 - iterationProgress;
403
412
  }
404
- const interpolationIsFinished = t >= totalDuration;
405
- const interpolationProgress = interpolationIsFinished
406
- ? 1
407
- : Math.min(iterationProgress, 1);
408
- const latest = interpolate(interpolationProgress);
413
+ const latest = interpolate(t >= totalDuration ? 1 : Math.min(iterationProgress, 1));
409
414
  output(latest);
410
- const isFinished = t >= totalDuration + endDelay;
411
- if (isFinished) {
415
+ const isAnimationFinished = this.playState === "finished" || t >= totalDuration + endDelay;
416
+ if (isAnimationFinished) {
412
417
  this.playState = "finished";
413
418
  this.resolve(latest);
414
419
  }
@@ -467,9 +472,16 @@
467
472
  this.rate = rate;
468
473
  }
469
474
  }
470
- function animateNumber(output, keyframes = [0, 1], options = {}) {
471
- 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;
472
483
  }
484
+ const keyframesList = (keyframes) => Array.isArray(keyframes) ? keyframes : [keyframes];
473
485
 
474
486
  const style = {
475
487
  get: (element, name) => {
@@ -477,7 +489,7 @@
477
489
  ? element.style.getPropertyValue(name)
478
490
  : getComputedStyle(element)[name];
479
491
  if (!value && value !== 0) {
480
- const definition = transformPropertyDefinitions.get(name);
492
+ const definition = transformDefinitions.get(name);
481
493
  if (definition)
482
494
  value = definition.initialValue;
483
495
  }
@@ -485,179 +497,188 @@
485
497
  },
486
498
  };
487
499
 
488
- function hydrateKeyframes(keyframes, element, name) {
489
- for (let i = 0; i < keyframes.length; i++) {
490
- if (keyframes[i] === null) {
491
- keyframes[i] = i ? keyframes[i - 1] : style.get(element, name);
492
- }
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();
493
516
  }
494
- return keyframes;
517
+ catch (e) { }
495
518
  }
496
- const keyframesList = (keyframes) => Array.isArray(keyframes) ? keyframes : [keyframes];
497
519
 
498
- function animateStyle(element, name, keyframesDefinition, options = {}) {
520
+ function animateStyle(element, key, keyframesDefinition, options = {}) {
499
521
  let animation;
500
522
  let { duration = defaults.duration, delay = defaults.delay, endDelay = defaults.endDelay, repeat = defaults.repeat, easing = defaults.easing, direction, offset, allowWebkitAcceleration = false, } = options;
501
523
  const data = getAnimationData(element);
502
524
  let canAnimateNatively = supports.waapi();
503
525
  let render = noop;
504
- const valueIsTransform = isTransform(name);
526
+ const valueIsTransform = isTransform(key);
505
527
  /**
506
528
  * If this is an individual transform, we need to map its
507
529
  * key to a CSS variable and update the element's transform style
508
530
  */
509
- if (valueIsTransform) {
510
- if (transformAlias[name])
511
- name = transformAlias[name];
512
- addTransformToElement(element, name);
513
- name = asTransformCssVar(name);
514
- }
531
+ valueIsTransform && addTransformToElement(element, key);
532
+ const name = getStyleName(key);
515
533
  /**
516
534
  * Get definition of value, this will be used to convert numerical
517
535
  * keyframes into the default value type.
518
536
  */
519
- const definition = transformPropertyDefinitions.get(name);
520
- /**
521
- * Replace null values with the previous keyframe value, or read
522
- * it from the DOM if it's the first keyframe.
523
- *
524
- * TODO: This needs to come after the valueIsTransform
525
- * check so it can correctly read the underlying value.
526
- * Should make a test for this.
527
- */
528
- let keyframes = hydrateKeyframes(keyframesList(keyframesDefinition), element, name);
529
- if (isCustomEasing(easing)) {
530
- const custom = easing.createAnimation(keyframes, () => style.get(element, name), valueIsTransform, name, data);
531
- easing = custom.easing;
532
- if (custom.keyframes)
533
- keyframes = custom.keyframes;
534
- if (custom.duration)
535
- duration = custom.duration;
536
- }
537
- /**
538
- * If this is a CSS variable we need to register it with the browser
539
- * before it can be animated natively. We also set it with setProperty
540
- * rather than directly onto the element.style object.
541
- */
542
- if (isCssVar(name)) {
543
- render = createCssVariableRenderer(element, name);
544
- if (supports.cssRegisterProperty()) {
545
- registerCssVariable(name);
546
- }
547
- else {
548
- canAnimateNatively = false;
549
- }
550
- }
551
- else {
552
- render = createStyleRenderer(element, name);
553
- }
537
+ const definition = transformDefinitions.get(name);
554
538
  /**
555
- * Stop the current animation, if any, after we have the
556
- * opportunity to use its data to generate velocity for
557
- * the subsequent animation.
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,
558
543
  */
559
- stopCurrentAnimation(data, name);
544
+ stopAnimation(data.animations[name]);
560
545
  /**
561
- * If we can animate this value with WAAPI, do so. Currently this only
562
- * feature detects CSS.registerProperty but could check WAAPI too.
546
+ * Batchable factory function containing all DOM reads.
563
547
  */
564
- 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; };
565
550
  /**
566
- * Convert numbers to default value types. Currently this only supports
567
- * 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.
568
553
  */
569
- if (definition) {
570
- keyframes = keyframes.map((value) => typeof value === "number" ? definition.toDefaultUnit(value) : value);
571
- }
572
- if (!supports.partialKeyframes() && keyframes.length === 1) {
573
- 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;
574
562
  }
575
- const animationOptions = {
576
- delay: ms(delay),
577
- duration: ms(duration),
578
- endDelay: ms(endDelay),
579
- easing: !isEasingList(easing) ? convertEasing(easing) : undefined,
580
- direction,
581
- iterations: repeat + 1,
582
- fill: "both",
583
- };
584
- animation = element.animate({
585
- [name]: keyframes,
586
- offset,
587
- easing: isEasingList(easing) ? easing.map(convertEasing) : undefined,
588
- }, animationOptions);
589
563
  /**
590
- * 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.
591
567
  */
592
- if (!animation.finished) {
593
- animation.finished = new Promise((resolve, reject) => {
594
- animation.onfinish = resolve;
595
- animation.oncancel = reject;
596
- });
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);
597
579
  }
598
- const target = keyframes[keyframes.length - 1];
599
- animation.finished
600
- .then(() => {
601
- // Apply styles to target
602
- render(target);
603
- // Ensure fill modes don't persist
604
- animation.cancel();
605
- })
606
- .catch(noop);
607
580
  /**
608
- * This forces Webkit to run animations on the main thread by exploiting
609
- * this condition:
610
- * https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp?rev=281238#L1099
611
- *
612
- * This fixes Webkit's timing bugs, like accelerated animations falling
613
- * out of sync with main thread animations and massive delays in starting
614
- * 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.
615
583
  */
616
- if (!allowWebkitAcceleration)
617
- animation.playbackRate = 1.000001;
618
- }
619
- else if (valueIsTransform && keyframes.every(isNumber)) {
620
- if (keyframes.length === 1) {
621
- 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
+ */
622
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;
623
669
  /**
624
- * Transform styles are currently only accepted as numbers of
625
- * their default value type, so here we loop through and map
626
- * them to numbers.
670
+ * When an animation finishes, delete the reference to the previous animation.
627
671
  */
628
- keyframes = keyframes.map((value) => typeof value === "string" ? parseFloat(value) : value);
629
- if (definition) {
630
- const applyStyle = render;
631
- render = (v) => applyStyle(definition.toDefaultUnit(v));
632
- }
633
- animation = animateNumber(render, keyframes, Object.assign(Object.assign({}, options), { duration,
634
- easing }));
635
- }
636
- else {
637
- const target = keyframes[keyframes.length - 1];
638
- render(definition && typeof target === "number"
639
- ? definition.toDefaultUnit(target)
640
- : target);
641
- }
642
- data.activeAnimations[name] = animation;
643
- /**
644
- * When an animation finishes, delete the reference to the previous animation.
645
- */
646
- animation === null || animation === void 0 ? void 0 : animation.finished.then(() => clearData(data, name)).catch(noop);
647
- return animation;
648
- }
649
- function stopCurrentAnimation(data, name) {
650
- if (data.activeAnimations[name]) {
651
- stopAnimation(data.activeAnimations[name]);
652
- clearData(data, name);
653
- }
654
- }
655
- function clearData(data, name) {
656
- data.activeGenerators[name] = data.activeAnimations[name] = undefined;
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
+ };
657
679
  }
658
- const isNumber = (value) => typeof value === "number";
659
680
 
660
- 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;
661
682
 
662
683
  function resolveElements(elements, selectorCache) {
663
684
  var _a;
@@ -676,7 +697,11 @@
676
697
  return Array.from(elements);
677
698
  }
678
699
 
679
- 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);
680
705
  /**
681
706
  * TODO:
682
707
  * Currently this returns the first animation, ideally it would return
@@ -723,7 +748,7 @@
723
748
 
724
749
  function stagger(duration = 0.1, { start = 0, from = 0, easing } = {}) {
725
750
  return (i, total) => {
726
- const fromIndex = typeof from === "number" ? from : getFromIndex(from, total);
751
+ const fromIndex = isNumber(from) ? from : getFromIndex(from, total);
727
752
  const distance = Math.abs(fromIndex - i);
728
753
  let delay = duration * distance;
729
754
  if (easing) {
@@ -752,19 +777,30 @@
752
777
  function animate(elements, keyframes, options = {}) {
753
778
  var _a;
754
779
  elements = resolveElements(elements);
755
- const animations = [];
756
780
  const numElements = elements.length;
781
+ /**
782
+ * Create and start new animations
783
+ */
784
+ const animationFactories = [];
757
785
  for (let i = 0; i < numElements; i++) {
758
786
  const element = elements[i];
759
787
  for (const key in keyframes) {
760
788
  const valueOptions = getOptions(options, key);
761
789
  valueOptions.delay = resolveOption(valueOptions.delay, i, numElements);
762
790
  const animation = animateStyle(element, key, keyframes[key], valueOptions);
763
- animation && animations.push(animation);
791
+ animationFactories.push(animation);
764
792
  }
765
793
  }
766
- return createAnimationControls(animations,
767
- // TODO: Remove this in case duration is dynamically generated
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
+ */
768
804
  (_a = options.duration) !== null && _a !== void 0 ? _a : defaults.duration);
769
805
  }
770
806
 
@@ -797,7 +833,7 @@
797
833
 
798
834
  function calcNextTime(current, next, prev, labels) {
799
835
  var _a;
800
- if (typeof next === "number") {
836
+ if (isNumber(next)) {
801
837
  return next;
802
838
  }
803
839
  else if (next.startsWith("-") || next.startsWith("+")) {
@@ -848,13 +884,14 @@
848
884
 
849
885
  function timeline(definition, options = {}) {
850
886
  var _a, _b;
851
- const animations = [];
852
887
  const animationDefinitions = createAnimationsFromTimeline(definition, options);
853
- for (let i = 0; i < animationDefinitions.length; i++) {
854
- const animation = animateStyle(...animationDefinitions[i]);
855
- animation && animations.push(animation);
856
- }
857
- 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,
858
895
  // Get the duration from the first animation definition
859
896
  (_b = (_a = animationDefinitions[0]) === null || _a === void 0 ? void 0 : _a[3].duration) !== null && _b !== void 0 ? _b : defaults.duration);
860
897
  }
@@ -1009,6 +1046,11 @@
1009
1046
  return frameDuration ? velocity * (1000 / frameDuration) : 0;
1010
1047
  }
1011
1048
 
1049
+ function hasReachedTarget(origin, target, current) {
1050
+ return ((origin < target && current >= target) ||
1051
+ (origin > target && current <= target));
1052
+ }
1053
+
1012
1054
  const defaultStiffness = 100.0;
1013
1055
  const defaultDamping = 10.0;
1014
1056
  const defaultMass = 1.0;
@@ -1016,15 +1058,17 @@
1016
1058
  const calcAngularFreq = (undampedFreq, dampingRatio) => undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio);
1017
1059
  const createSpringGenerator = ({ stiffness = defaultStiffness, damping = defaultDamping, mass = defaultMass, from = 0, to = 1, velocity = 0.0, restSpeed = 2, restDistance = 0.5, } = {}) => {
1018
1060
  velocity = velocity ? velocity / 1000 : 0.0;
1019
- const dampingRatio = calcDampingRatio(stiffness, damping, mass);
1020
- const initialDelta = to - from;
1021
- const undampedAngularFreq = Math.sqrt(stiffness / mass) / 1000;
1022
- const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
1023
1061
  const state = {
1024
1062
  done: false,
1025
1063
  value: from,
1064
+ target: to,
1026
1065
  velocity,
1066
+ hasReachedTarget: false,
1027
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);
1028
1072
  let resolveSpring;
1029
1073
  if (dampingRatio < 1) {
1030
1074
  // Underdamped spring (bouncy)
@@ -1049,6 +1093,7 @@
1049
1093
  const isBelowVelocityThreshold = Math.abs(state.velocity) <= restSpeed;
1050
1094
  const isBelowDisplacementThreshold = Math.abs(to - state.value) <= restDistance;
1051
1095
  state.done = isBelowVelocityThreshold && isBelowDisplacementThreshold;
1096
+ state.hasReachedTarget = hasReachedTarget(from, to, state.value);
1052
1097
  return state;
1053
1098
  },
1054
1099
  };
@@ -1060,24 +1105,27 @@
1060
1105
  }
1061
1106
 
1062
1107
  const timeStep = 10;
1063
- const maxDuration = 3000;
1064
- function pregenerateKeyframes(generator, origin, target) {
1065
- const keyframes = [];
1108
+ const maxDuration = 10000;
1109
+ function pregenerateKeyframes(generator) {
1066
1110
  let overshootDuration = undefined;
1067
- let timestamp = 0;
1111
+ let timestamp = timeStep;
1068
1112
  let state = generator.next(0);
1113
+ const keyframes = [state.value];
1069
1114
  while (!state.done && timestamp < maxDuration) {
1070
1115
  state = generator.next(timestamp);
1071
- keyframes.push(state.done ? target : state.value);
1072
- if (overshootDuration === undefined) {
1073
- if ((origin < target && state.value >= target) ||
1074
- (origin > target && state.value <= target)) {
1075
- overshootDuration = timestamp;
1076
- }
1116
+ keyframes.push(state.done ? state.target : state.value);
1117
+ if (overshootDuration === undefined && state.hasReachedTarget) {
1118
+ overshootDuration = timestamp;
1077
1119
  }
1078
1120
  timestamp += timeStep;
1079
1121
  }
1080
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);
1081
1129
  return {
1082
1130
  keyframes,
1083
1131
  duration: duration / 1000,
@@ -1085,70 +1133,168 @@
1085
1133
  };
1086
1134
  }
1087
1135
 
1088
- function spring(options = {}) {
1089
- const springCache = new Map();
1136
+ function createGeneratorEasing(createGenerator) {
1090
1137
  const keyframesCache = new WeakMap();
1091
- const getSpring = (from = 0, to = 100, velocity = 0, isScale = false) => {
1092
- const key = `${from}-${to}-${velocity}-${isScale}`;
1093
- if (!springCache.has(key)) {
1094
- springCache.set(key, createSpringGenerator(Object.assign({ from,
1095
- to,
1096
- velocity, restSpeed: isScale ? 0.05 : 2, restDistance: isScale ? 0.01 : 0.5 }, options)));
1097
- }
1098
- return springCache.get(key);
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
+ };
1099
1196
  };
1100
- const getKeyframes = (generator, origin, target) => {
1101
- if (!keyframesCache.has(generator)) {
1102
- keyframesCache.set(generator, pregenerateKeyframes(generator, origin, target));
1103
- }
1104
- return keyframesCache.get(generator);
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);
1105
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);
1106
1262
  return {
1107
- createAnimation: (keyframes, getOrigin, canUseRealSpring, name, data) => {
1108
- let settings;
1109
- let spring;
1110
- const numKeyframes = keyframes.length;
1111
- let shouldUseRealSpring = canUseRealSpring && numKeyframes <= 2 && keyframes.every(isNumberOrNull);
1112
- if (shouldUseRealSpring) {
1113
- const prevAnimationState = name && data && getPreviousAnimationState(name, data);
1114
- const velocity = prevAnimationState &&
1115
- (numKeyframes === 1 || (numKeyframes === 2 && keyframes[0] === null))
1116
- ? prevAnimationState.velocity
1117
- : 0;
1118
- const target = keyframes[numKeyframes - 1];
1119
- const unresolvedOrigin = numKeyframes === 1 ? null : keyframes[0];
1120
- const origin = unresolvedOrigin === null
1121
- ? prevAnimationState
1122
- ? prevAnimationState.value
1123
- : parseFloat(getOrigin())
1124
- : unresolvedOrigin;
1125
- spring = getSpring(origin, target, velocity, name === null || name === void 0 ? void 0 : name.includes("scale"));
1126
- const keyframesMetadata = getKeyframes(spring, origin, target);
1127
- settings = Object.assign(Object.assign({}, keyframesMetadata), { easing: "linear" });
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);
1128
1283
  }
1129
1284
  else {
1130
- spring = getSpring(0, 100);
1131
- const keyframesMetadata = getKeyframes(spring, 0, 100);
1132
- settings = {
1133
- easing: "ease",
1134
- duration: keyframesMetadata.overshootDuration,
1135
- };
1285
+ state.hasReachedTarget = false;
1286
+ !hasUpdatedFrame && applyFriction(t);
1287
+ return state;
1136
1288
  }
1137
- return settings;
1138
1289
  },
1139
1290
  };
1140
- }
1141
- function getPreviousAnimationState(name, data) {
1142
- const animation = data.activeAnimations[name];
1143
- const generator = data.activeGenerators[name];
1144
- if (animation && generator) {
1145
- return generator.next(animation.currentTime);
1146
- }
1147
- }
1148
- const isNumberOrNull = (value) => typeof value !== "string";
1291
+ };
1292
+
1293
+ const glide = createGeneratorEasing(createGlideGenerator);
1149
1294
 
1150
1295
  exports.animate = animate;
1151
1296
  exports.animateStyle = animateStyle;
1297
+ exports.glide = glide;
1152
1298
  exports.spring = spring;
1153
1299
  exports.stagger = stagger;
1154
1300
  exports.timeline = timeline;