animejs 4.3.0-beta.1 → 4.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 (127) hide show
  1. package/README.md +16 -17
  2. package/dist/bundles/anime.esm.js +456 -266
  3. package/dist/bundles/anime.esm.min.js +3 -3
  4. package/dist/bundles/anime.umd.js +456 -266
  5. package/dist/bundles/anime.umd.min.js +3 -3
  6. package/dist/modules/animatable/animatable.cjs +2 -2
  7. package/dist/modules/animatable/animatable.js +2 -2
  8. package/dist/modules/animatable/index.cjs +2 -2
  9. package/dist/modules/animatable/index.js +2 -2
  10. package/dist/modules/animation/additive.cjs +2 -2
  11. package/dist/modules/animation/additive.js +2 -2
  12. package/dist/modules/animation/animation.cjs +8 -5
  13. package/dist/modules/animation/animation.js +9 -6
  14. package/dist/modules/animation/composition.cjs +2 -2
  15. package/dist/modules/animation/composition.js +2 -2
  16. package/dist/modules/animation/index.cjs +2 -2
  17. package/dist/modules/animation/index.js +2 -2
  18. package/dist/modules/core/clock.cjs +2 -2
  19. package/dist/modules/core/clock.js +2 -2
  20. package/dist/modules/core/colors.cjs +2 -2
  21. package/dist/modules/core/colors.js +2 -2
  22. package/dist/modules/core/consts.cjs +2 -2
  23. package/dist/modules/core/consts.js +2 -2
  24. package/dist/modules/core/globals.cjs +3 -3
  25. package/dist/modules/core/globals.js +3 -3
  26. package/dist/modules/core/helpers.cjs +4 -4
  27. package/dist/modules/core/helpers.js +4 -4
  28. package/dist/modules/core/render.cjs +2 -2
  29. package/dist/modules/core/render.js +2 -2
  30. package/dist/modules/core/styles.cjs +2 -2
  31. package/dist/modules/core/styles.js +2 -2
  32. package/dist/modules/core/targets.cjs +2 -2
  33. package/dist/modules/core/targets.js +2 -2
  34. package/dist/modules/core/transforms.cjs +2 -2
  35. package/dist/modules/core/transforms.js +2 -2
  36. package/dist/modules/core/units.cjs +2 -2
  37. package/dist/modules/core/units.js +2 -2
  38. package/dist/modules/core/values.cjs +2 -2
  39. package/dist/modules/core/values.js +2 -2
  40. package/dist/modules/draggable/draggable.cjs +2 -2
  41. package/dist/modules/draggable/draggable.js +2 -2
  42. package/dist/modules/draggable/index.cjs +2 -2
  43. package/dist/modules/draggable/index.js +2 -2
  44. package/dist/modules/easings/cubic-bezier/index.cjs +2 -2
  45. package/dist/modules/easings/cubic-bezier/index.js +2 -2
  46. package/dist/modules/easings/eases/index.cjs +2 -2
  47. package/dist/modules/easings/eases/index.js +2 -2
  48. package/dist/modules/easings/eases/parser.cjs +2 -2
  49. package/dist/modules/easings/eases/parser.js +2 -2
  50. package/dist/modules/easings/index.cjs +2 -2
  51. package/dist/modules/easings/index.js +2 -2
  52. package/dist/modules/easings/irregular/index.cjs +2 -2
  53. package/dist/modules/easings/irregular/index.js +2 -2
  54. package/dist/modules/easings/linear/index.cjs +2 -2
  55. package/dist/modules/easings/linear/index.js +2 -2
  56. package/dist/modules/easings/none.cjs +2 -2
  57. package/dist/modules/easings/none.js +2 -2
  58. package/dist/modules/easings/spring/index.cjs +2 -2
  59. package/dist/modules/easings/spring/index.js +2 -2
  60. package/dist/modules/easings/steps/index.cjs +2 -2
  61. package/dist/modules/easings/steps/index.js +2 -2
  62. package/dist/modules/engine/engine.cjs +2 -2
  63. package/dist/modules/engine/engine.js +2 -2
  64. package/dist/modules/engine/index.cjs +2 -2
  65. package/dist/modules/engine/index.js +2 -2
  66. package/dist/modules/events/index.cjs +2 -2
  67. package/dist/modules/events/index.js +2 -2
  68. package/dist/modules/events/scroll.cjs +2 -2
  69. package/dist/modules/events/scroll.js +2 -2
  70. package/dist/modules/index.cjs +2 -2
  71. package/dist/modules/index.js +2 -2
  72. package/dist/modules/layout/index.cjs +2 -2
  73. package/dist/modules/layout/index.js +2 -2
  74. package/dist/modules/layout/layout.cjs +437 -249
  75. package/dist/modules/layout/layout.d.ts +44 -38
  76. package/dist/modules/layout/layout.js +439 -251
  77. package/dist/modules/scope/index.cjs +2 -2
  78. package/dist/modules/scope/index.js +2 -2
  79. package/dist/modules/scope/scope.cjs +2 -2
  80. package/dist/modules/scope/scope.js +2 -2
  81. package/dist/modules/svg/drawable.cjs +2 -2
  82. package/dist/modules/svg/drawable.js +2 -2
  83. package/dist/modules/svg/helpers.cjs +2 -2
  84. package/dist/modules/svg/helpers.js +2 -2
  85. package/dist/modules/svg/index.cjs +2 -2
  86. package/dist/modules/svg/index.js +2 -2
  87. package/dist/modules/svg/morphto.cjs +2 -2
  88. package/dist/modules/svg/morphto.js +2 -2
  89. package/dist/modules/svg/motionpath.cjs +2 -2
  90. package/dist/modules/svg/motionpath.js +2 -2
  91. package/dist/modules/text/index.cjs +2 -2
  92. package/dist/modules/text/index.js +2 -2
  93. package/dist/modules/text/split.cjs +2 -2
  94. package/dist/modules/text/split.js +2 -2
  95. package/dist/modules/timeline/index.cjs +2 -2
  96. package/dist/modules/timeline/index.js +2 -2
  97. package/dist/modules/timeline/position.cjs +2 -2
  98. package/dist/modules/timeline/position.js +2 -2
  99. package/dist/modules/timeline/timeline.cjs +2 -2
  100. package/dist/modules/timeline/timeline.js +2 -2
  101. package/dist/modules/timer/index.cjs +2 -2
  102. package/dist/modules/timer/index.js +2 -2
  103. package/dist/modules/timer/timer.cjs +5 -4
  104. package/dist/modules/timer/timer.d.ts +2 -1
  105. package/dist/modules/timer/timer.js +5 -4
  106. package/dist/modules/types/index.d.ts +6 -6
  107. package/dist/modules/utils/chainable.cjs +2 -2
  108. package/dist/modules/utils/chainable.js +2 -2
  109. package/dist/modules/utils/index.cjs +2 -2
  110. package/dist/modules/utils/index.js +2 -2
  111. package/dist/modules/utils/number.cjs +2 -2
  112. package/dist/modules/utils/number.js +2 -2
  113. package/dist/modules/utils/random.cjs +2 -2
  114. package/dist/modules/utils/random.js +2 -2
  115. package/dist/modules/utils/stagger.cjs +2 -2
  116. package/dist/modules/utils/stagger.js +2 -2
  117. package/dist/modules/utils/target.cjs +2 -2
  118. package/dist/modules/utils/target.js +2 -2
  119. package/dist/modules/utils/time.cjs +2 -2
  120. package/dist/modules/utils/time.js +2 -2
  121. package/dist/modules/waapi/composition.cjs +2 -2
  122. package/dist/modules/waapi/composition.js +2 -2
  123. package/dist/modules/waapi/index.cjs +2 -2
  124. package/dist/modules/waapi/index.js +2 -2
  125. package/dist/modules/waapi/waapi.cjs +12 -7
  126. package/dist/modules/waapi/waapi.js +12 -7
  127. package/package.json +1 -1
@@ -1,21 +1,25 @@
1
1
  /**
2
2
  * Anime.js - layout - ESM
3
- * @version v4.3.0-beta.1
3
+ * @version v4.3.0
4
4
  * @license MIT
5
- * @copyright 2025 - Julian Garnier
5
+ * @copyright 2026 - Julian Garnier
6
6
  */
7
7
 
8
- import { mergeObjects, isFnc, isUnd, isSvg, isStr, isArr } from '../core/helpers.js';
8
+ import { mergeObjects, isUnd, isSvg, isStr, isFnc, isArr } from '../core/helpers.js';
9
9
  import { registerTargets } from '../core/targets.js';
10
+ import { parseEase } from '../easings/eases/parser.js';
10
11
  import { setValue, getFunctionValue } from '../core/values.js';
11
- import { noop } from '../core/consts.js';
12
12
  import { createTimeline } from '../timeline/timeline.js';
13
13
  import { waapi } from '../waapi/waapi.js';
14
- import { scope } from '../core/globals.js';
14
+ import { scope, defaults } from '../core/globals.js';
15
15
 
16
16
  /**
17
17
  * @import {
18
18
  * AnimationParams,
19
+ * RenderableCallbacks,
20
+ * TickableCallbacks,
21
+ * TimelineParams,
22
+ * TimerParams,
19
23
  * } from '../types/index.js'
20
24
  */
21
25
 
@@ -31,13 +35,18 @@ import { scope } from '../core/globals.js';
31
35
  * } from '../waapi/waapi.js'
32
36
  */
33
37
 
38
+ /**
39
+ * @import {
40
+ * Spring,
41
+ } from '../easings/spring/index.js'
42
+ */
43
+
34
44
  /**
35
45
  * @import {
36
46
  * DOMTarget,
37
47
  * DOMTargetSelector,
38
48
  * FunctionValue,
39
49
  * EasingParam,
40
- * Callback,
41
50
  } from '../types/index.js'
42
51
  */
43
52
 
@@ -46,29 +55,47 @@ import { scope } from '../core/globals.js';
46
55
  */
47
56
 
48
57
  /**
49
- * @typedef {Record<String, Number|String>} LayoutStateParams
58
+ * @typedef {Object} LayoutAnimationTimingsParams
59
+ * @property {Number|FunctionValue} [delay]
60
+ * @property {Number|FunctionValue} [duration]
61
+ * @property {EasingParam|FunctionValue} [ease]
62
+ */
63
+
64
+ /**
65
+ * @typedef {Record<String, Number|String|FunctionValue>} LayoutStateAnimationProperties
50
66
  */
51
67
 
52
68
  /**
53
- * @typedef {Object} LayoutAnimationParams
69
+ * @typedef {LayoutStateAnimationProperties & LayoutAnimationTimingsParams} LayoutStateParams
70
+ */
71
+
72
+ /**
73
+ * @typedef {Object} LayoutSpecificAnimationParams
54
74
  * @property {Number|FunctionValue} [delay]
55
75
  * @property {Number|FunctionValue} [duration]
56
- * @property {EasingParam} [ease]
57
- * @property {LayoutStateParams} [frozen]
58
- * @property {LayoutStateParams} [added]
59
- * @property {LayoutStateParams} [removed]
60
- * @property {Callback<AutoLayout>} [onComplete]
76
+ * @property {EasingParam|FunctionValue} [ease]
77
+ * @property {EasingParam} [playbackEase]
78
+ * @property {LayoutStateParams} [swapAt]
79
+ * @property {LayoutStateParams} [enterFrom]
80
+ * @property {LayoutStateParams} [leaveTo]
81
+ */
82
+
83
+ /**
84
+ * @typedef {LayoutSpecificAnimationParams & TimerParams & TickableCallbacks<Timeline> & RenderableCallbacks<Timeline>} LayoutAnimationParams
61
85
  */
62
86
 
63
87
  /**
64
- * @typedef {LayoutAnimationParams & {
65
- * children?: LayoutChildrenParam,
66
- * properties?: Array<String>,
67
- * }} AutoLayoutParams
88
+ * @typedef {Object} LayoutOptions
89
+ * @property {LayoutChildrenParam} [children]
90
+ * @property {Array<String>} [properties]
68
91
  */
69
92
 
70
93
  /**
71
- * @typedef {Record<String, Number|String> & {
94
+ * @typedef {LayoutAnimationParams & LayoutOptions} AutoLayoutParams
95
+ */
96
+
97
+ /**
98
+ * @typedef {Record<String, Number|String|FunctionValue> & {
72
99
  * transform: String,
73
100
  * x: Number,
74
101
  * y: Number,
@@ -89,13 +116,15 @@ import { scope } from '../core/globals.js';
89
116
  * @property {Number} total
90
117
  * @property {Number} delay
91
118
  * @property {Number} duration
119
+ * @property {EasingParam} ease
92
120
  * @property {DOMTarget} $measure
93
121
  * @property {LayoutSnapshot} state
94
122
  * @property {AutoLayout} layout
95
123
  * @property {LayoutNode|null} parentNode
96
124
  * @property {Boolean} isTarget
125
+ * @property {Boolean} isEntering
126
+ * @property {Boolean} isLeaving
97
127
  * @property {Boolean} hasTransform
98
- * @property {Boolean} isAnimated
99
128
  * @property {Array<String>} inlineStyles
100
129
  * @property {String|null} inlineTransforms
101
130
  * @property {String|null} inlineTransition
@@ -236,7 +265,7 @@ const detachNode = node => {
236
265
  * @param {DOMTarget} $el
237
266
  * @param {LayoutNode|null} parentNode
238
267
  * @param {LayoutSnapshot} state
239
- * @param {LayoutNode} [recycledNode]
268
+ * @param {LayoutNode} recycledNode
240
269
  * @return {LayoutNode}
241
270
  */
242
271
  const createNode = ($el, parentNode, state, recycledNode) => {
@@ -250,12 +279,15 @@ const createNode = ($el, parentNode, state, recycledNode) => {
250
279
  node.total = 1;
251
280
  node.delay = 0;
252
281
  node.duration = 0;
282
+ node.ease = null;
253
283
  node.state = state;
254
284
  node.layout = state.layout;
255
285
  node.parentNode = parentNode || null;
256
286
  node.isTarget = false;
287
+ node.isEntering = false;
288
+ node.isLeaving = false;
289
+ node.isInlined = false;
257
290
  node.hasTransform = false;
258
- node.isAnimated = false;
259
291
  node.inlineStyles = [];
260
292
  node.inlineTransforms = null;
261
293
  node.inlineTransition = null;
@@ -263,7 +295,6 @@ const createNode = ($el, parentNode, state, recycledNode) => {
263
295
  node.branchRemoved = false;
264
296
  node.branchNotRendered = false;
265
297
  node.sizeChanged = false;
266
- node.isInlined = false;
267
298
  node.hasVisibilitySwap = false;
268
299
  node.hasDisplayNone = false;
269
300
  node.hasVisibilityHidden = false;
@@ -406,7 +437,7 @@ const recordNodeState = (node, $measure, computedStyle, skipMeasurements) => {
406
437
 
407
438
  /**
408
439
  * @param {LayoutNode} node
409
- * @param {LayoutStateParams} [props]
440
+ * @param {LayoutStateAnimationProperties} [props]
410
441
  */
411
442
  const updateNodeProperties = (node, props) => {
412
443
  if (!props) return;
@@ -415,6 +446,19 @@ const updateNodeProperties = (node, props) => {
415
446
  }
416
447
  };
417
448
 
449
+ /**
450
+ * @param {LayoutNode} node
451
+ * @param {LayoutAnimationTimingsParams} params
452
+ */
453
+ const updateNodeTimingParams = (node, params) => {
454
+ const easeFunctionResult = getFunctionValue(params.ease, node.$el, node.index, node.total);
455
+ const keyEasing = isFnc(easeFunctionResult) ? easeFunctionResult : params.ease;
456
+ const hasSpring = !isUnd(keyEasing) && !isUnd(/** @type {Spring} */(keyEasing).ease);
457
+ node.ease = hasSpring ? /** @type {Spring} */(keyEasing).ease : keyEasing;
458
+ node.duration = hasSpring ? /** @type {Spring} */(keyEasing).settlingDuration : getFunctionValue(params.duration, node.$el, node.index, node.total);
459
+ node.delay = getFunctionValue(params.delay, node.$el, node.index, node.total);
460
+ };
461
+
418
462
  /**
419
463
  * @param {LayoutNode} node
420
464
  */
@@ -489,9 +533,9 @@ const restoreNodeVisualState = node => {
489
533
  node.$measure.style.removeProperty('visibility');
490
534
  }
491
535
  }
492
- if (node.measuredIsRemoved) {
493
- node.layout.pendingRemoved.delete(node.$el);
494
- }
536
+ // if (node.measuredIsRemoved) {
537
+ node.layout.pendingRemoval.delete(node.$el);
538
+ // }
495
539
  };
496
540
 
497
541
  /**
@@ -541,6 +585,7 @@ class LayoutSnapshot {
541
585
  */
542
586
  revert() {
543
587
  this.forEachNode(node => {
588
+ this.layout.pendingRemoval.delete(node.$el);
544
589
  node.$el.removeAttribute('data-layout-id');
545
590
  node.$measure.removeAttribute('data-layout-id');
546
591
  });
@@ -552,34 +597,22 @@ class LayoutSnapshot {
552
597
 
553
598
  /**
554
599
  * @param {DOMTarget} $el
555
- * @return {LayoutNodeProperties|undefined}
600
+ * @return {LayoutNode}
556
601
  */
557
- get($el) {
558
- const node = this.nodes.get($el.dataset.layoutId);
559
- if (!node) {
560
- console.warn(`No node found on state`);
561
- return;
562
- }
563
- return node.properties;
602
+ getNode($el) {
603
+ if (!$el || !$el.dataset) return;
604
+ return this.nodes.get($el.dataset.layoutId);
564
605
  }
565
606
 
566
607
  /**
567
608
  * @param {DOMTarget} $el
568
609
  * @param {String} prop
569
- * @return {Number|String|undefined}
610
+ * @return {Number|String}
570
611
  */
571
- getValue($el, prop) {
572
- if (!$el || !$el.dataset) {
573
- console.warn(`No element found on state (${$el})`);
574
- return;
575
- }
576
- const node = this.nodes.get($el.dataset.layoutId);
577
- if (!node) {
578
- console.warn(`No node found on state`);
579
- return;
580
- }
581
- const value = node.properties[prop];
582
- if (!isUnd(value)) return getFunctionValue(value, $el, node.index, node.total);
612
+ getComputedValue($el, prop) {
613
+ const node = this.getNode($el);
614
+ if (!node) return;
615
+ return /** @type {Number|String} */(node.properties[prop]);
583
616
  }
584
617
 
585
618
  /**
@@ -640,10 +673,10 @@ class LayoutSnapshot {
640
673
  const $parent = /** @type {LayoutNode|null} */(stack.pop());
641
674
  /** @type {DOMTarget|null} */
642
675
  const $current = /** @type {DOMTarget|null} */(stack.pop());
676
+
643
677
  if (!$current || $current.nodeType !== 1 || isSvg($current)) continue;
644
678
 
645
679
  const skipMeasurements = $parent ? $parent.measuredIsRemoved : false;
646
-
647
680
  const computedStyle = skipMeasurements ? hiddenComputedStyle : getComputedStyle($current);
648
681
  const hasDisplayNone = skipMeasurements ? true : computedStyle.display === 'none';
649
682
  const hasVisibilityHidden = skipMeasurements ? true : computedStyle.visibility === 'hidden';
@@ -690,11 +723,10 @@ class LayoutSnapshot {
690
723
  node.branchRemoved = false;
691
724
  node.branchNotRendered = false;
692
725
  node.isTarget = false;
693
- node.isAnimated = false;
726
+ node.sizeChanged = false;
694
727
  node.hasVisibilityHidden = hasVisibilityHidden;
695
728
  node.hasDisplayNone = hasDisplayNone;
696
729
  node.hasVisibilitySwap = (hasVisibilityHidden && !node.measuredHasVisibilityHidden) || (hasDisplayNone && !node.measuredHasDisplayNone);
697
- // node.hasVisibilitySwap = (hasVisibilityHidden !== node.measuredHasVisibilityHidden) || (hasDisplayNone !== node.measuredHasDisplayNone);
698
730
 
699
731
  this.nodes.set(node.id, node);
700
732
 
@@ -713,6 +745,7 @@ class LayoutSnapshot {
713
745
  $parent._tail = node;
714
746
  }
715
747
  } else {
748
+ // Each disconnected subtree becomes its own root in the snapshot graph
716
749
  this.rootNodes.add(node);
717
750
  }
718
751
 
@@ -756,11 +789,29 @@ class LayoutSnapshot {
756
789
  * @return {this}
757
790
  */
758
791
  record() {
759
- const { children, root } = this.layout;
792
+ const layout = this.layout;
793
+ const children = layout.children;
794
+ const root = layout.root;
760
795
  const toParse = isArr(children) ? children : [children];
761
796
  const scoped = [];
762
797
  const scopeRoot = children === '*' ? root : scope.root;
763
798
 
799
+ // Mute transition and transforms of root ancestors before recording the state
800
+
801
+ /** @type {Array<DOMTarget|String|null>} */
802
+ const rootAncestorTransformStore = [];
803
+ let $ancestor = root.parentElement;
804
+ while ($ancestor && $ancestor.nodeType === 1) {
805
+ const computedStyle = getComputedStyle($ancestor);
806
+ if (computedStyle.transform && computedStyle.transform !== 'none') {
807
+ const inlineTransform = $ancestor.style.transform || '';
808
+ const inlineTransition = muteElementTransition($ancestor);
809
+ rootAncestorTransformStore.push($ancestor, inlineTransform, inlineTransition);
810
+ $ancestor.style.transform = 'none';
811
+ }
812
+ $ancestor = $ancestor.parentElement;
813
+ }
814
+
764
815
  for (let i = 0, l = toParse.length; i < l; i++) {
765
816
  const child = toParse[i];
766
817
  scoped[i] = isStr(child) ? scopeRoot.querySelectorAll(child) : child;
@@ -776,9 +827,13 @@ class LayoutSnapshot {
776
827
  rootNode.isTarget = true;
777
828
  this.rootNode = rootNode;
778
829
 
779
- // Track ids of nodes that belong to the current root to filter detached matches
780
830
  const inRootNodeIds = new Set();
831
+ // Update index and total for inital timing calculation
832
+ let index = 0, total = this.nodes.size;
781
833
  this.nodes.forEach((node, id) => {
834
+ node.index = index++;
835
+ node.total = total;
836
+ // Track ids of nodes that belong to the current root to filter detached matches
782
837
  if (node && node.measuredIsInsideRoot) {
783
838
  inRootNodeIds.add(id);
784
839
  }
@@ -808,7 +863,7 @@ class LayoutSnapshot {
808
863
 
809
864
  for (let i = 0, l = parsedChildren.length; i < l; i++) {
810
865
  const $el = parsedChildren[i];
811
- const node = this.nodes.get($el.dataset.layoutId);
866
+ const node = this.getNode($el);
812
867
  if (node) {
813
868
  let cur = node;
814
869
  while (cur) {
@@ -822,18 +877,52 @@ class LayoutSnapshot {
822
877
  this.scrollX = window.scrollX;
823
878
  this.scrollY = window.scrollY;
824
879
 
825
- const total = this.nodes.size;
826
-
827
880
  this.forEachNode(restoreNodeTransform);
828
- this.forEachNode((node, i) => {
829
- node.index = i;
830
- node.total = total;
831
- });
881
+
882
+ // Restore transition and transforms of root ancestors
883
+
884
+ for (let i = 0, l = rootAncestorTransformStore.length; i < l; i += 3) {
885
+ const $el = /** @type {DOMTarget} */(rootAncestorTransformStore[i]);
886
+ const inlineTransform = /** @type {String} */(rootAncestorTransformStore[i + 1]);
887
+ const inlineTransition = /** @type {String|null} */(rootAncestorTransformStore[i + 2]);
888
+ if (inlineTransform && inlineTransform !== '') {
889
+ $el.style.transform = inlineTransform;
890
+ } else {
891
+ $el.style.removeProperty('transform');
892
+ }
893
+ restoreElementTransition($el, inlineTransition);
894
+ }
832
895
 
833
896
  return this;
834
897
  }
835
898
  }
836
899
 
900
+ /**
901
+ * @param {LayoutStateParams} params
902
+ * @return {[LayoutStateAnimationProperties, LayoutAnimationTimingsParams]}
903
+ */
904
+ function splitPropertiesFromParams(params) {
905
+ /** @type {LayoutStateAnimationProperties} */
906
+ const properties = {};
907
+ /** @type {LayoutAnimationTimingsParams} */
908
+ const parameters = {};
909
+ for (let name in params) {
910
+ const value = params[name];
911
+ const isEase = name === 'ease';
912
+ const isTiming = name === 'duration' || name === 'delay';
913
+ if (isTiming || isEase) {
914
+ if (isEase) {
915
+ parameters[name] = /** @type {EasingParam} */(value);
916
+ } else {
917
+ parameters[name] = /** @type {Number|FunctionValue} */(value);
918
+ }
919
+ } else {
920
+ properties[name] = /** @type {Number|String} */(value);
921
+ }
922
+ }
923
+ return [properties, parameters];
924
+ }
925
+
837
926
  class AutoLayout {
838
927
  /**
839
928
  * @param {DOMTargetSelector} root
@@ -841,10 +930,16 @@ class AutoLayout {
841
930
  */
842
931
  constructor(root, params = {}) {
843
932
  if (scope.current) scope.current.register(this);
844
- const frozenParams = params.frozen;
845
- const addedParams = params.added;
846
- const removedParams = params.removed;
847
- const propsParams = params.properties;
933
+ const swapAtSplitParams = splitPropertiesFromParams(params.swapAt);
934
+ const enterFromSplitParams = splitPropertiesFromParams(params.enterFrom);
935
+ const leaveToSplitParams = splitPropertiesFromParams(params.leaveTo);
936
+ const transitionProperties = params.properties;
937
+ /** @type {Number|FunctionValue} */
938
+ params.duration = setValue(params.duration, 350);
939
+ /** @type {Number|FunctionValue} */
940
+ params.delay = setValue(params.delay, 0);
941
+ /** @type {EasingParam|FunctionValue} */
942
+ params.ease = setValue(params.ease, 'inOut(3.5)');
848
943
  /** @type {AutoLayoutParams} */
849
944
  this.params = params;
850
945
  /** @type {DOMTarget} */
@@ -855,29 +950,27 @@ class AutoLayout {
855
950
  this.children = params.children || '*';
856
951
  /** @type {Boolean} */
857
952
  this.absoluteCoords = false;
858
- /** @type {Number|FunctionValue} */
859
- this.duration = setValue(params.duration, 500);
860
- /** @type {Number|FunctionValue} */
861
- this.delay = setValue(params.delay, 0);
862
- /** @type {EasingParam} */
863
- this.ease = setValue(params.ease, 'inOut(3.5)');
864
- /** @type {Callback<this>} */
865
- this.onComplete = setValue(params.onComplete, /** @type {Callback<this>} */(noop));
866
953
  /** @type {LayoutStateParams} */
867
- this.frozenParams = frozenParams || { opacity: 0 };
954
+ this.swapAtParams = mergeObjects(params.swapAt || { opacity: 0 }, { ease: 'inOut(1.75)' });
868
955
  /** @type {LayoutStateParams} */
869
- this.addedParams = addedParams || { opacity: 0 };
956
+ this.enterFromParams = params.enterFrom || { opacity: 0 };
870
957
  /** @type {LayoutStateParams} */
871
- this.removedParams = removedParams || { opacity: 0 };
958
+ this.leaveToParams = params.leaveTo || { opacity: 0 };
872
959
  /** @type {Set<String>} */
873
960
  this.properties = new Set([
874
961
  'opacity',
962
+ 'fontSize',
963
+ 'color',
964
+ 'backgroundColor',
875
965
  'borderRadius',
966
+ 'border',
967
+ 'filter',
968
+ 'clipPath',
876
969
  ]);
877
- if (frozenParams) for (let name in frozenParams) this.properties.add(name);
878
- if (addedParams) for (let name in addedParams) this.properties.add(name);
879
- if (removedParams) for (let name in removedParams) this.properties.add(name);
880
- if (propsParams) for (let i = 0, l = propsParams.length; i < l; i++) this.properties.add(propsParams[i]);
970
+ if (swapAtSplitParams[0]) for (let name in swapAtSplitParams[0]) this.properties.add(name);
971
+ if (enterFromSplitParams[0]) for (let name in enterFromSplitParams[0]) this.properties.add(name);
972
+ if (leaveToSplitParams[0]) for (let name in leaveToSplitParams[0]) this.properties.add(name);
973
+ if (transitionProperties) for (let i = 0, l = transitionProperties.length; i < l; i++) this.properties.add(transitionProperties[i]);
881
974
  /** @type {Set<String>} */
882
975
  this.recordedProperties = new Set([
883
976
  'display',
@@ -897,24 +990,26 @@ class AutoLayout {
897
990
  ]);
898
991
  this.properties.forEach(prop => this.recordedProperties.add(prop));
899
992
  /** @type {WeakSet<DOMTarget>} */
900
- this.pendingRemoved = new WeakSet();
993
+ this.pendingRemoval = new WeakSet();
901
994
  /** @type {Map<DOMTarget, String|null>} */
902
995
  this.transitionMuteStore = new Map();
903
996
  /** @type {LayoutSnapshot} */
904
997
  this.oldState = new LayoutSnapshot(this);
905
998
  /** @type {LayoutSnapshot} */
906
999
  this.newState = new LayoutSnapshot(this);
907
- /** @type {Timeline|null} */
1000
+ /** @type {Timeline} */
908
1001
  this.timeline = null;
909
- /** @type {WAAPIAnimation|null} */
1002
+ /** @type {WAAPIAnimation} */
910
1003
  this.transformAnimation = null;
911
1004
  /** @type {Array<DOMTarget>} */
912
- this.frozen = [];
1005
+ this.animating = [];
913
1006
  /** @type {Array<DOMTarget>} */
914
- this.removed = [];
1007
+ this.swapping = [];
915
1008
  /** @type {Array<DOMTarget>} */
916
- this.added = [];
917
- // Record the current state as the old state to init the data attributes
1009
+ this.leaving = [];
1010
+ /** @type {Array<DOMTarget>} */
1011
+ this.entering = [];
1012
+ // Record the current state as the old state to init the data attributes and allow imediate .animate()
918
1013
  this.oldState.record();
919
1014
  // And all layout transition muted during the record
920
1015
  restoreLayoutTransition(this.transitionMuteStore);
@@ -924,6 +1019,7 @@ class AutoLayout {
924
1019
  * @return {this}
925
1020
  */
926
1021
  revert() {
1022
+ this.root.classList.remove('is-animated');
927
1023
  if (this.timeline) {
928
1024
  this.timeline.complete();
929
1025
  this.timeline = null;
@@ -932,8 +1028,7 @@ class AutoLayout {
932
1028
  this.transformAnimation.complete();
933
1029
  this.transformAnimation = null;
934
1030
  }
935
- this.root.classList.remove('is-animated');
936
- this.frozen.length = this.removed.length = this.added.length = 0;
1031
+ this.animating.length = this.swapping.length = this.leaving.length = this.entering.length = 0;
937
1032
  this.oldState.revert();
938
1033
  this.newState.revert();
939
1034
  requestAnimationFrame(() => restoreLayoutTransition(this.transitionMuteStore));
@@ -966,20 +1061,72 @@ class AutoLayout {
966
1061
  * @return {Timeline}
967
1062
  */
968
1063
  animate(params = {}) {
969
- const delay = setValue(params.delay, this.delay);
970
- const duration = setValue(params.duration, this.duration);
971
- const onComplete = setValue(params.onComplete, this.onComplete);
972
- const frozenParams = params.frozen ? mergeObjects(params.frozen, this.frozenParams) : this.frozenParams;
973
- const addedParams = params.added ? mergeObjects(params.added, this.addedParams) : this.addedParams;
974
- const removedParams = params.removed ? mergeObjects(params.removed, this.removedParams) : this.removedParams;
1064
+ /** @type { LayoutAnimationTimingsParams } */
1065
+ const animationTimings = {
1066
+ ease: setValue(params.ease, this.params.ease),
1067
+ delay: setValue(params.delay, this.params.delay),
1068
+ duration: setValue(params.duration, this.params.duration),
1069
+ };
1070
+ /** @type {TimelineParams} */
1071
+ const tlParams = {};
1072
+ const onComplete = setValue(params.onComplete, this.params.onComplete);
1073
+ const onPause = setValue(params.onPause, this.params.onPause);
1074
+ for (let name in defaults) {
1075
+ if (name !== 'ease' && name !== 'duration' && name !== 'delay') {
1076
+ if (!isUnd(params[name])) {
1077
+ tlParams[name] = params[name];
1078
+ } else if (!isUnd(this.params[name])) {
1079
+ tlParams[name] = this.params[name];
1080
+ }
1081
+ }
1082
+ }
1083
+ tlParams.onComplete = () => {
1084
+ // Make sure to call .cancel() after restoreNodeInlineStyles(node); otehrwise the commited styles get reverted
1085
+ if (this.transformAnimation) this.transformAnimation.cancel();
1086
+ newState.forEachRootNode(node => {
1087
+ restoreNodeVisualState(node);
1088
+ restoreNodeInlineStyles(node);
1089
+ });
1090
+ for (let i = 0, l = transformed.length; i < l; i++) {
1091
+ const $el = transformed[i];
1092
+ $el.style.transform = newState.getComputedValue($el, 'transform');
1093
+ }
1094
+ if (this.root.classList.contains('is-animated')) {
1095
+ this.root.classList.remove('is-animated');
1096
+ if (onComplete) onComplete(this.timeline);
1097
+ }
1098
+ // Avoid CSS transitions at the end of the animation by restoring them on the next frame
1099
+ requestAnimationFrame(() => {
1100
+ if (this.root.classList.contains('is-animated')) return;
1101
+ restoreLayoutTransition(this.transitionMuteStore);
1102
+ });
1103
+ };
1104
+ tlParams.onPause = () => {
1105
+ if (!this.root.classList.contains('is-animated')) return;
1106
+ if (this.transformAnimation) this.transformAnimation.cancel();
1107
+ newState.forEachRootNode(restoreNodeVisualState);
1108
+ this.root.classList.remove('is-animated');
1109
+ if (onComplete) onComplete(this.timeline);
1110
+ if (onPause) onPause(this.timeline);
1111
+ };
1112
+ tlParams.composition = false;
1113
+
1114
+ const swapAtParams = mergeObjects(mergeObjects(params.swapAt || {}, this.swapAtParams), animationTimings);
1115
+ const enterFromParams = mergeObjects(mergeObjects(params.enterFrom || {}, this.enterFromParams), animationTimings);
1116
+ const leaveToParams = mergeObjects(mergeObjects(params.leaveTo || {}, this.leaveToParams), animationTimings);
1117
+ const [ swapAtProps, swapAtTimings ] = splitPropertiesFromParams(swapAtParams);
1118
+ const [ enterFromProps, enterFromTimings ] = splitPropertiesFromParams(enterFromParams);
1119
+ const [ leaveToProps, leaveToTimings ] = splitPropertiesFromParams(leaveToParams);
1120
+
975
1121
  const oldState = this.oldState;
976
1122
  const newState = this.newState;
977
- const added = this.added;
978
- const removed = this.removed;
979
- const frozen = this.frozen;
980
- const pendingRemoved = this.pendingRemoved;
1123
+ const animating = this.animating;
1124
+ const swapping = this.swapping;
1125
+ const entering = this.entering;
1126
+ const leaving = this.leaving;
1127
+ const pendingRemoval = this.pendingRemoval;
981
1128
 
982
- added.length = removed.length = frozen.length = 0;
1129
+ animating.length = swapping.length = entering.length = leaving.length = 0;
983
1130
 
984
1131
  // Mute old state CSS transitions to prevent wrong properties calculation
985
1132
  oldState.forEachRootNode(muteNodeTransition);
@@ -990,10 +1137,12 @@ class AutoLayout {
990
1137
  const targets = [];
991
1138
  const animated = [];
992
1139
  const transformed = [];
993
- const animatedFrozen = [];
994
- const root = newState.rootNode.$el;
1140
+ const animatedSwap = [];
1141
+ const rootNode = newState.rootNode;
1142
+ const $root = rootNode.$el;
995
1143
 
996
1144
  newState.forEachRootNode(node => {
1145
+
997
1146
  const $el = node.$el;
998
1147
  const id = node.id;
999
1148
  const parent = node.parentNode;
@@ -1001,10 +1150,6 @@ class AutoLayout {
1001
1150
  const parentRemoved = parent ? parent.branchRemoved : false;
1002
1151
  const parentNotRendered = parent ? parent.branchNotRendered : false;
1003
1152
 
1004
- // Delay and duration must be calculated in the animate() call to support delay override
1005
- node.delay = +(isFnc(delay) ? delay($el, node.index, node.total) : delay);
1006
- node.duration = +(isFnc(duration) ? duration($el, node.index, node.total) : duration);
1007
-
1008
1153
  let oldStateNode = oldState.nodes.get(id);
1009
1154
 
1010
1155
  const hasNoOldState = !oldStateNode;
@@ -1028,26 +1173,16 @@ class AutoLayout {
1028
1173
 
1029
1174
  // Recalculate postion relative to their parent for elements that have been moved
1030
1175
  if (!oldStateNode.measuredIsRemoved && !isRemovedNow && !hasNoOldState && (parentChanged || elementChanged)) {
1031
- let offsetX = 0;
1032
- let offsetY = 0;
1033
- let current = node.parentNode;
1034
- while (current) {
1035
- offsetX += current.properties.x || 0;
1036
- offsetY += current.properties.y || 0;
1037
- if (current.parentNode === newState.rootNode) break;
1038
- current = current.parentNode;
1039
- }
1040
- let oldOffsetX = 0;
1041
- let oldOffsetY = 0;
1042
- let oldCurrent = oldStateNode.parentNode;
1043
- while (oldCurrent) {
1044
- oldOffsetX += oldCurrent.properties.x || 0;
1045
- oldOffsetY += oldCurrent.properties.y || 0;
1046
- if (oldCurrent.parentNode === oldState.rootNode) break;
1047
- oldCurrent = oldCurrent.parentNode;
1048
- }
1049
- oldStateNode.properties.x += oldOffsetX - offsetX;
1050
- oldStateNode.properties.y += oldOffsetY - offsetY;
1176
+ const oldAbsoluteLeft = oldStateNode.properties.left;
1177
+ const oldAbsoluteTop = oldStateNode.properties.top;
1178
+ const newParent = parent || newState.rootNode;
1179
+ const oldParent = newParent.id ? oldState.nodes.get(newParent.id) : null;
1180
+ const parentLeft = oldParent ? oldParent.properties.left : newParent.properties.left;
1181
+ const parentTop = oldParent ? oldParent.properties.top : newParent.properties.top;
1182
+ const borderLeft = oldParent ? oldParent.properties.clientLeft : newParent.properties.clientLeft;
1183
+ const borderTop = oldParent ? oldParent.properties.clientTop : newParent.properties.clientTop;
1184
+ oldStateNode.properties.x = oldAbsoluteLeft - parentLeft - borderLeft;
1185
+ oldStateNode.properties.y = oldAbsoluteTop - parentTop - borderTop;
1051
1186
  }
1052
1187
 
1053
1188
  if (node.hasVisibilitySwap) {
@@ -1064,7 +1199,7 @@ class AutoLayout {
1064
1199
  }
1065
1200
  }
1066
1201
 
1067
- const wasPendingRemoval = pendingRemoved.has($el);
1202
+ const wasPendingRemoval = pendingRemoval.has($el);
1068
1203
  const wasVisibleBefore = oldStateNode.measuredIsVisible;
1069
1204
  const isVisibleNow = node.measuredIsVisible;
1070
1205
  const becomeVisible = !wasVisibleBefore && isVisibleNow && !parentNotRendered;
@@ -1072,113 +1207,156 @@ class AutoLayout {
1072
1207
  const newlyRemoved = isRemovedNow && !wasRemovedBefore && !parentRemoved;
1073
1208
  const topLevelRemoved = newlyRemoved || isRemovedNow && wasPendingRemoval && !parentRemoved;
1074
1209
 
1075
- if (node.measuredIsRemoved && wasVisibleBefore) {
1210
+ node.branchAdded = parentAdded || topLevelAdded;
1211
+ node.branchRemoved = parentRemoved || topLevelRemoved;
1212
+ node.branchNotRendered = parentNotRendered || isRemovedNow;
1213
+
1214
+ if (isRemovedNow && wasVisibleBefore) {
1076
1215
  node.$el.style.display = oldStateNode.measuredDisplay;
1077
1216
  node.$el.style.visibility = 'visible';
1078
1217
  cloneNodeProperties(oldStateNode, node, newState);
1079
1218
  }
1080
1219
 
1220
+ // Node is leaving
1081
1221
  if (newlyRemoved) {
1082
- removed.push($el);
1083
- pendingRemoved.add($el);
1222
+ if (node.isTarget) {
1223
+ leaving.push($el);
1224
+ node.isLeaving = true;
1225
+ }
1226
+ pendingRemoval.add($el);
1084
1227
  } else if (!isRemovedNow && wasPendingRemoval) {
1085
- pendingRemoved.delete($el);
1228
+ pendingRemoval.delete($el);
1086
1229
  }
1087
1230
 
1088
- // Node is added
1231
+ // Node is entering
1089
1232
  if ((topLevelAdded && !parentNotRendered) || becomeVisible) {
1090
- updateNodeProperties(oldStateNode, addedParams);
1091
- added.push($el);
1092
- // Node is removed
1233
+ updateNodeProperties(oldStateNode, enterFromProps);
1234
+ if (node.isTarget) {
1235
+ entering.push($el);
1236
+ node.isEntering = true;
1237
+ }
1238
+ // Node is leaving
1093
1239
  } else if (topLevelRemoved && !parentNotRendered) {
1094
- updateNodeProperties(node, removedParams);
1240
+ updateNodeProperties(node, leaveToProps);
1095
1241
  }
1096
1242
 
1097
- // Compute function based propety values before cheking for changes
1098
- for (let name in node.properties) {
1099
- node.properties[name] = newState.getValue(node.$el, name);
1100
- // NOTE: I'm using node.$el to get the value of old state, make sure this is valid instead of oldStateNode.$el
1101
- oldStateNode.properties[name] = oldState.getValue(node.$el, name);
1243
+ // Node is animating
1244
+ // The animating array is used only to calculate delays and duration on root children
1245
+ if (node !== rootNode && node.isTarget && !node.isEntering && !node.isLeaving) {
1246
+ animating.push($el);
1102
1247
  }
1103
1248
 
1104
- const hiddenStateChanged = (topLevelAdded || newlyRemoved) && wasRemovedBefore !== isRemovedNow;
1105
- let propertyChanged = false;
1249
+ targets.push($el);
1106
1250
 
1251
+ });
1107
1252
 
1108
- if (node.isTarget && (!node.measuredIsRemoved && wasVisibleBefore || node.measuredIsRemoved && isVisibleNow)) {
1109
- if (!node.isInlined && (node.properties.transform !== 'none' || oldStateNode.properties.transform !== 'none')) {
1110
- node.hasTransform = true;
1111
- propertyChanged = true;
1112
- transformed.push($el);
1113
- }
1114
- for (let name in node.properties) {
1115
- if (name !== 'transform' && (node.properties[name] !== oldStateNode.properties[name] || hiddenStateChanged)) {
1116
- propertyChanged = true;
1117
- animated.push($el);
1118
- break;
1119
- }
1120
- }
1253
+ let enteringIndex = 0;
1254
+ let leavingIndex = 0;
1255
+ let animatingIndex = 0;
1256
+
1257
+ newState.forEachRootNode(node => {
1258
+
1259
+ const $el = node.$el;
1260
+ const parent = node.parentNode;
1261
+ const oldStateNode = oldState.nodes.get(node.id);
1262
+ const nodeProperties = node.properties;
1263
+ const oldStateNodeProperties = oldStateNode.properties;
1264
+
1265
+ // Use closest animated parent index and total values so that children staggered delays are in sync with their parent
1266
+ let animatedParent = parent !== rootNode && parent;
1267
+ while (animatedParent && !animatedParent.isTarget && animatedParent !== rootNode) {
1268
+ animatedParent = animatedParent.parentNode;
1121
1269
  }
1122
1270
 
1123
- const nodeHasChanged = (propertyChanged || topLevelAdded || topLevelRemoved || becomeVisible);
1124
- const nodeIsAnimated = node.isTarget && nodeHasChanged;
1271
+ const animatingTotal = animating.length;
1272
+
1273
+ // Root is always animated first in sync with the first child (animating.length is the total of children)
1274
+ if (node === rootNode) {
1275
+ node.index = 0;
1276
+ node.total = animatingTotal;
1277
+ updateNodeTimingParams(node, animationTimings);
1278
+ } else if (node.isEntering) {
1279
+ node.index = animatedParent ? animatedParent.index : enteringIndex;
1280
+ node.total = animatedParent ? animatingTotal : entering.length;
1281
+ updateNodeTimingParams(node, enterFromTimings);
1282
+ enteringIndex++;
1283
+ } else if (node.isLeaving) {
1284
+ node.index = animatedParent ? animatedParent.index : leavingIndex;
1285
+ node.total = animatedParent ? animatingTotal : leaving.length;
1286
+ leavingIndex++;
1287
+ updateNodeTimingParams(node, leaveToTimings);
1288
+ } else if (node.isTarget) {
1289
+ node.index = animatingIndex++;
1290
+ node.total = animatingTotal;
1291
+ updateNodeTimingParams(node, animationTimings);
1292
+ } else {
1293
+ node.index = animatedParent ? animatedParent.index : 0;
1294
+ node.total = animatingTotal;
1295
+ updateNodeTimingParams(node, swapAtTimings);
1296
+ }
1125
1297
 
1126
- node.isAnimated = nodeIsAnimated;
1127
- node.branchAdded = parentAdded || topLevelAdded;
1128
- node.branchRemoved = parentRemoved || topLevelRemoved;
1129
- node.branchNotRendered = parentNotRendered || node.measuredIsRemoved;
1298
+ // Make sure the old state node has its inex and total values up to date for valid "from" function values calculation
1299
+ oldStateNode.index = node.index;
1300
+ oldStateNode.total = node.total;
1301
+
1302
+ // Computes all values up front so we can check for changes and we don't have to re-compute them inside the animation props
1303
+ for (let prop in nodeProperties) {
1304
+ nodeProperties[prop] = getFunctionValue(nodeProperties[prop], $el, node.index, node.total);
1305
+ oldStateNodeProperties[prop] = getFunctionValue(oldStateNodeProperties[prop], $el, oldStateNode.index, oldStateNode.total);
1306
+ }
1130
1307
 
1308
+ // Use a 1px tolerance to detect dimensions changes to prevent width / height animations on barelly visible elements
1131
1309
  const sizeTolerance = 1;
1132
- const widthChanged = Math.abs(node.properties.width - oldStateNode.properties.width) > sizeTolerance;
1133
- const heightChanged = Math.abs(node.properties.height - oldStateNode.properties.height) > sizeTolerance;
1310
+ const widthChanged = Math.abs(nodeProperties.width - oldStateNodeProperties.width) > sizeTolerance;
1311
+ const heightChanged = Math.abs(nodeProperties.height - oldStateNodeProperties.height) > sizeTolerance;
1134
1312
 
1135
1313
  node.sizeChanged = (widthChanged || heightChanged);
1136
1314
 
1137
- targets.push($el);
1315
+ // const hiddenStateChanged = (topLevelAdded || newlyRemoved) && wasRemovedBefore !== isRemovedNow;
1316
+
1317
+ if (node.isTarget && (!node.measuredIsRemoved && oldStateNode.measuredIsVisible || node.measuredIsRemoved && node.measuredIsVisible)) {
1318
+ if (!node.isInlined && (nodeProperties.transform !== 'none' || oldStateNodeProperties.transform !== 'none')) {
1319
+ node.hasTransform = true;
1320
+ transformed.push($el);
1321
+ }
1322
+ for (let prop in nodeProperties) {
1323
+ // if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop] || hiddenStateChanged)) {
1324
+ if (prop !== 'transform' && (nodeProperties[prop] !== oldStateNodeProperties[prop])) {
1325
+ animated.push($el);
1326
+ break;
1327
+ }
1328
+ }
1329
+ }
1138
1330
 
1139
1331
  if (!node.isTarget) {
1140
- frozen.push($el);
1141
- if ((nodeHasChanged || node.sizeChanged) && parent && parent.isTarget && parent.isAnimated && parent.sizeChanged) {
1142
- animatedFrozen.push($el);
1332
+ swapping.push($el);
1333
+ if (node.sizeChanged && parent && parent.isTarget && parent.sizeChanged) {
1334
+ if (!node.isInlined && swapAtProps.transform) {
1335
+ node.hasTransform = true;
1336
+ transformed.push($el);
1337
+ }
1338
+ animatedSwap.push($el);
1143
1339
  }
1144
1340
  }
1341
+
1145
1342
  });
1146
1343
 
1147
- const defaults = {
1148
- ease: setValue(params.ease, this.ease),
1149
- duration: (/** @type {HTMLElement} */$el) => newState.nodes.get($el.dataset.layoutId).duration,
1150
- delay: (/** @type {HTMLElement} */$el) => newState.nodes.get($el.dataset.layoutId).delay,
1344
+ const timingParams = {
1345
+ delay: (/** @type {HTMLElement} */$el) => newState.getNode($el).delay,
1346
+ duration: (/** @type {HTMLElement} */$el) => newState.getNode($el).duration,
1347
+ ease: (/** @type {HTMLElement} */$el) => newState.getNode($el).ease,
1151
1348
  };
1152
1349
 
1153
- this.timeline = createTimeline({
1154
- onComplete: () => {
1155
- // Make sure to call .cancel() after restoreNodeInlineStyles(node); otehrwise the commited styles get reverted
1156
- if (this.transformAnimation) this.transformAnimation.cancel();
1157
- newState.forEachRootNode(node => {
1158
- restoreNodeVisualState(node);
1159
- restoreNodeInlineStyles(node);
1160
- });
1161
- for (let i = 0, l = transformed.length; i < l; i++) {
1162
- const $el = transformed[i];
1163
- $el.style.transform = newState.getValue($el, 'transform');
1164
- }
1165
- this.root.classList.remove('is-animated');
1166
- if (onComplete) onComplete(this);
1167
- // Avoid CSS transitions at the end of the animation by restoring them on the next frame
1168
- requestAnimationFrame(() => {
1169
- if (this.root.classList.contains('is-animated')) return;
1170
- restoreLayoutTransition(this.transitionMuteStore);
1171
- });
1172
- },
1173
- onPause: () => {
1174
- if (this.transformAnimation) this.transformAnimation.cancel();
1175
- newState.forEachRootNode(restoreNodeVisualState);
1176
- this.root.classList.remove('is-animated');
1177
- if (onComplete) onComplete(this);
1178
- },
1179
- composition: false,
1180
- defaults,
1181
- });
1350
+ tlParams.defaults = timingParams;
1351
+
1352
+ this.timeline = createTimeline(tlParams);
1353
+
1354
+ // Imediatly return the timeline if no layout changes detected
1355
+ if (!animated.length && !transformed.length && !swapping.length) {
1356
+ // Make sure to restore all CSS transition if no animation
1357
+ restoreLayoutTransition(this.transitionMuteStore);
1358
+ return this.timeline.complete();
1359
+ }
1182
1360
 
1183
1361
  if (targets.length) {
1184
1362
 
@@ -1191,16 +1369,14 @@ class AutoLayout {
1191
1369
  const newNode = newState.nodes.get(id);
1192
1370
  const oldNodeState = oldNode.properties;
1193
1371
 
1194
- // Make sure to mute all CSS transition before applying the oldState styles back
1195
- muteNodeTransition(newNode);
1372
+ // muteNodeTransition(newNode);
1196
1373
 
1197
1374
  // Don't animate dimensions and positions of inlined elements
1198
1375
  if (!newNode.isInlined) {
1199
1376
  // Display grid can mess with the absolute positioning, so set it to block during transition
1200
- // if (oldNode.measuredDisplay === 'grid' || newNode.measuredDisplay === 'grid') $el.style.display = 'block';
1201
- $el.style.display = 'block';
1202
- // All children must be in position absolue
1203
- if ($el !== root || this.absoluteCoords) {
1377
+ if (oldNode.measuredDisplay === 'grid' || newNode.measuredDisplay === 'grid') $el.style.setProperty('display', 'block', 'important');
1378
+ // All children must be in position absolute or fixed
1379
+ if ($el !== $root || this.absoluteCoords) {
1204
1380
  $el.style.position = this.absoluteCoords ? 'fixed' : 'absolute';
1205
1381
  $el.style.left = '0px';
1206
1382
  $el.style.top = '0px';
@@ -1208,7 +1384,7 @@ class AutoLayout {
1208
1384
  $el.style.marginTop = '0px';
1209
1385
  $el.style.translate = `${oldNodeState.x}px ${oldNodeState.y}px`;
1210
1386
  }
1211
- if ($el === root && newNode.measuredPosition === 'static') {
1387
+ if ($el === $root && newNode.measuredPosition === 'static') {
1212
1388
  $el.style.position = 'relative';
1213
1389
  // Cancel left / trop in case the static element had muted values now activated by potision relative
1214
1390
  $el.style.left = '0px';
@@ -1227,9 +1403,7 @@ class AutoLayout {
1227
1403
  // Restore the scroll position if the oldState differs from the current state
1228
1404
  if (oldState.scrollX !== window.scrollX || oldState.scrollY !== window.scrollY) {
1229
1405
  // Restoring in the next frame avoids race conditions if for example a waapi animation commit styles that affect the root height
1230
- requestAnimationFrame(() => {
1231
- window.scrollTo(oldState.scrollX, oldState.scrollY);
1232
- });
1406
+ requestAnimationFrame(() => window.scrollTo(oldState.scrollX, oldState.scrollY));
1233
1407
  }
1234
1408
 
1235
1409
  for (let i = 0, l = animated.length; i < l; i++) {
@@ -1239,25 +1413,25 @@ class AutoLayout {
1239
1413
  const newNode = newState.nodes.get(id);
1240
1414
  const oldNodeState = oldNode.properties;
1241
1415
  const newNodeState = newNode.properties;
1242
- let hasChanged = false;
1416
+ let nodeHasChanged = false;
1417
+ /** @type {AnimationParams} */
1243
1418
  const animatedProps = {
1244
1419
  composition: 'none',
1245
- // delay: (/** @type {HTMLElement} */$el) => newState.nodes.get($el.dataset.layoutId).delay,
1246
1420
  };
1247
1421
  if (!newNode.isInlined) {
1248
1422
  if (oldNodeState.width !== newNodeState.width) {
1249
1423
  animatedProps.width = [oldNodeState.width, newNodeState.width];
1250
- hasChanged = true;
1424
+ nodeHasChanged = true;
1251
1425
  }
1252
1426
  if (oldNodeState.height !== newNodeState.height) {
1253
1427
  animatedProps.height = [oldNodeState.height, newNodeState.height];
1254
- hasChanged = true;
1428
+ nodeHasChanged = true;
1255
1429
  }
1256
1430
  // If the node has transforms we handle the translate animation in wappi otherwise translate and other transforms can be out of sync
1257
1431
  // Always animate translate
1258
1432
  if (!newNode.hasTransform) {
1259
1433
  animatedProps.translate = [`${oldNodeState.x}px ${oldNodeState.y}px`, `${newNodeState.x}px ${newNodeState.y}px`];
1260
- hasChanged = true;
1434
+ nodeHasChanged = true;
1261
1435
  }
1262
1436
  }
1263
1437
  this.properties.forEach(prop => {
@@ -1265,73 +1439,77 @@ class AutoLayout {
1265
1439
  const newVal = newNodeState[prop];
1266
1440
  if (prop !== 'transform' && oldVal !== newVal) {
1267
1441
  animatedProps[prop] = [oldVal, newVal];
1268
- hasChanged = true;
1442
+ nodeHasChanged = true;
1269
1443
  }
1270
1444
  });
1271
- if (hasChanged) {
1445
+ if (nodeHasChanged) {
1272
1446
  this.timeline.add($el, animatedProps, 0);
1273
1447
  }
1274
1448
  }
1275
1449
 
1276
1450
  }
1277
1451
 
1278
- if (frozen.length) {
1452
+ if (swapping.length) {
1279
1453
 
1280
- for (let i = 0, l = frozen.length; i < l; i++) {
1281
- const $el = frozen[i];
1282
- const oldNode = oldState.nodes.get($el.dataset.layoutId);
1454
+ for (let i = 0, l = swapping.length; i < l; i++) {
1455
+ const $el = swapping[i];
1456
+ const oldNode = oldState.getNode($el);
1283
1457
  if (!oldNode.isInlined) {
1284
- const oldNodeState = oldState.get($el);
1285
- $el.style.width = `${oldNodeState.width}px`;
1286
- $el.style.height = `${oldNodeState.height}px`;
1458
+ const oldNodeProps = oldNode.properties;
1459
+ $el.style.width = `${oldNodeProps.width}px`;
1460
+ $el.style.height = `${oldNodeProps.height}px`;
1287
1461
  // Overrides user defined min and max to prevents width and height clamping
1288
1462
  $el.style.minWidth = `auto`;
1289
1463
  $el.style.minHeight = `auto`;
1290
1464
  $el.style.maxWidth = `none`;
1291
1465
  $el.style.maxHeight = `none`;
1292
- $el.style.translate = `${oldNodeState.x}px ${oldNodeState.y}px`;
1466
+ $el.style.translate = `${oldNodeProps.x}px ${oldNodeProps.y}px`;
1293
1467
  }
1294
1468
  this.properties.forEach(prop => {
1295
1469
  if (prop !== 'transform') {
1296
- $el.style[prop] = `${oldState.getValue($el, prop)}`;
1470
+ $el.style[prop] = `${oldState.getComputedValue($el, prop)}`;
1297
1471
  }
1298
1472
  });
1299
1473
  }
1300
1474
 
1301
- for (let i = 0, l = frozen.length; i < l; i++) {
1302
- const $el = frozen[i];
1303
- const newNode = newState.nodes.get($el.dataset.layoutId);
1304
- const newNodeState = newState.get($el);
1475
+ for (let i = 0, l = swapping.length; i < l; i++) {
1476
+ const $el = swapping[i];
1477
+ const newNode = newState.getNode($el);
1478
+ const newNodeProps = newNode.properties;
1305
1479
  this.timeline.call(() => {
1306
1480
  if (!newNode.isInlined) {
1307
- $el.style.width = `${newNodeState.width}px`;
1308
- $el.style.height = `${newNodeState.height}px`;
1481
+ $el.style.width = `${newNodeProps.width}px`;
1482
+ $el.style.height = `${newNodeProps.height}px`;
1309
1483
  // Overrides user defined min and max to prevents width and height clamping
1310
1484
  $el.style.minWidth = `auto`;
1311
1485
  $el.style.minHeight = `auto`;
1312
1486
  $el.style.maxWidth = `none`;
1313
1487
  $el.style.maxHeight = `none`;
1314
- $el.style.translate = `${newNodeState.x}px ${newNodeState.y}px`;
1488
+ $el.style.translate = `${newNodeProps.x}px ${newNodeProps.y}px`;
1315
1489
  }
1316
1490
  this.properties.forEach(prop => {
1317
1491
  if (prop !== 'transform') {
1318
- $el.style[prop] = `${newState.getValue($el, prop)}`;
1492
+ $el.style[prop] = `${newState.getComputedValue($el, prop)}`;
1319
1493
  }
1320
1494
  });
1321
1495
  }, newNode.delay + newNode.duration / 2);
1322
1496
  }
1323
1497
 
1324
- if (animatedFrozen.length) {
1325
- const animatedFrozenParams = /** @type {AnimationParams} */({});
1326
- if (frozenParams) {
1327
- for (let prop in frozenParams) {
1328
- animatedFrozenParams[prop] = [
1329
- { from: (/** @type {HTMLElement} */$el) => oldState.getValue($el, prop), ease: 'in(1.75)', to: frozenParams[prop] },
1330
- { from: frozenParams[prop], to: (/** @type {HTMLElement} */$el) => newState.getValue($el, prop), ease: 'out(1.75)' }
1331
- ];
1498
+ if (animatedSwap.length) {
1499
+ const ease = parseEase(newState.nodes.get(animatedSwap[0].dataset.layoutId).ease);
1500
+ const inverseEased = t => 1 - ease(1 - t);
1501
+ const animatedSwapParams = /** @type {AnimationParams} */({});
1502
+ if (swapAtProps) {
1503
+ for (let prop in swapAtProps) {
1504
+ if (prop !== 'transform') {
1505
+ animatedSwapParams[prop] = [
1506
+ { from: (/** @type {HTMLElement} */$el) => oldState.getComputedValue($el, prop), to: swapAtProps[prop] },
1507
+ { from: swapAtProps[prop], to: (/** @type {HTMLElement} */$el) => newState.getComputedValue($el, prop), ease: inverseEased }
1508
+ ];
1509
+ }
1332
1510
  }
1333
1511
  }
1334
- this.timeline.add(animatedFrozen, animatedFrozenParams, 0);
1512
+ this.timeline.add(animatedSwap, animatedSwapParams, 0);
1335
1513
  }
1336
1514
 
1337
1515
  }
@@ -1339,18 +1517,29 @@ class AutoLayout {
1339
1517
  const transformedLength = transformed.length;
1340
1518
 
1341
1519
  if (transformedLength) {
1342
- // We only need to set the transform property here since translate is alread defined the targets loop
1520
+ // We only need to set the transform property here since translate is alread defined in the targets loop
1343
1521
  for (let i = 0; i < transformedLength; i++) {
1344
1522
  const $el = transformed[i];
1345
- $el.style.translate = `${oldState.get($el).x}px ${oldState.get($el).y}px`,
1346
- $el.style.transform = oldState.getValue($el, 'transform');
1523
+ $el.style.translate = `${oldState.getComputedValue($el, 'x')}px ${oldState.getComputedValue($el, 'y')}px`,
1524
+ $el.style.transform = oldState.getComputedValue($el, 'transform');
1525
+ if (animatedSwap.includes($el)) {
1526
+ const node = newState.getNode($el);
1527
+ node.ease = getFunctionValue(swapAtParams.ease, $el, node.index, node.total);
1528
+ node.duration = getFunctionValue(swapAtParams.duration, $el, node.index, node.total);
1529
+ }
1347
1530
  }
1348
1531
  this.transformAnimation = waapi.animate(transformed, {
1349
- translate: (/** @type {HTMLElement} */$el) => `${newState.get($el).x}px ${newState.get($el).y}px`,
1350
- transform: (/** @type {HTMLElement} */$el) => newState.getValue($el, 'transform'),
1532
+ translate: (/** @type {HTMLElement} */$el) => `${newState.getComputedValue($el, 'x')}px ${newState.getComputedValue($el, 'y')}px`,
1533
+ transform: (/** @type {HTMLElement} */$el) => {
1534
+ const newValue = newState.getComputedValue($el, 'transform');
1535
+ if (!animatedSwap.includes($el)) return newValue;
1536
+ const oldValue = oldState.getComputedValue($el, 'transform');
1537
+ const node = newState.getNode($el);
1538
+ return [oldValue, getFunctionValue(swapAtProps.transform, $el, node.index, node.total), newValue]
1539
+ },
1351
1540
  autoplay: false,
1352
1541
  persist: true,
1353
- ...defaults,
1542
+ ...timingParams,
1354
1543
  });
1355
1544
  this.timeline.sync(this.transformAnimation, 0);
1356
1545
  }
@@ -1361,13 +1550,12 @@ class AutoLayout {
1361
1550
  /**
1362
1551
  * @param {(layout: this) => void} callback
1363
1552
  * @param {LayoutAnimationParams} [params]
1364
- * @return {this}
1553
+ * @return {Timeline}
1365
1554
  */
1366
1555
  update(callback, params = {}) {
1367
1556
  this.record();
1368
1557
  callback(this);
1369
- this.animate(params);
1370
- return this;
1558
+ return this.animate(params);
1371
1559
  }
1372
1560
  }
1373
1561