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