animejs 4.2.1 → 4.3.0-beta.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 (133) hide show
  1. package/README.md +82 -28
  2. package/dist/bundles/anime.esm.js +2707 -1301
  3. package/dist/bundles/anime.esm.min.js +2 -2
  4. package/dist/bundles/anime.umd.js +2717 -1309
  5. package/dist/bundles/anime.umd.min.js +2 -2
  6. package/dist/modules/animatable/animatable.cjs +1 -1
  7. package/dist/modules/animatable/animatable.js +1 -1
  8. package/dist/modules/animatable/index.cjs +1 -1
  9. package/dist/modules/animatable/index.js +1 -1
  10. package/dist/modules/animation/additive.cjs +1 -1
  11. package/dist/modules/animation/additive.js +1 -1
  12. package/dist/modules/animation/animation.cjs +18 -9
  13. package/dist/modules/animation/animation.js +19 -10
  14. package/dist/modules/animation/composition.cjs +1 -1
  15. package/dist/modules/animation/composition.js +1 -1
  16. package/dist/modules/animation/index.cjs +1 -1
  17. package/dist/modules/animation/index.js +1 -1
  18. package/dist/modules/core/clock.cjs +10 -10
  19. package/dist/modules/core/clock.d.ts +1 -1
  20. package/dist/modules/core/clock.js +10 -10
  21. package/dist/modules/core/colors.cjs +1 -1
  22. package/dist/modules/core/colors.js +1 -1
  23. package/dist/modules/core/consts.cjs +6 -4
  24. package/dist/modules/core/consts.d.ts +13 -5
  25. package/dist/modules/core/consts.js +6 -4
  26. package/dist/modules/core/globals.cjs +5 -2
  27. package/dist/modules/core/globals.d.ts +1 -0
  28. package/dist/modules/core/globals.js +5 -3
  29. package/dist/modules/core/helpers.cjs +1 -1
  30. package/dist/modules/core/helpers.js +1 -1
  31. package/dist/modules/core/render.cjs +1 -1
  32. package/dist/modules/core/render.js +1 -1
  33. package/dist/modules/core/styles.cjs +1 -1
  34. package/dist/modules/core/styles.js +1 -1
  35. package/dist/modules/core/targets.cjs +1 -1
  36. package/dist/modules/core/targets.js +1 -1
  37. package/dist/modules/core/transforms.cjs +1 -1
  38. package/dist/modules/core/transforms.js +1 -1
  39. package/dist/modules/core/units.cjs +1 -1
  40. package/dist/modules/core/units.js +1 -1
  41. package/dist/modules/core/values.cjs +1 -1
  42. package/dist/modules/core/values.js +1 -1
  43. package/dist/modules/draggable/draggable.cjs +1 -1
  44. package/dist/modules/draggable/draggable.js +1 -1
  45. package/dist/modules/draggable/index.cjs +1 -1
  46. package/dist/modules/draggable/index.js +1 -1
  47. package/dist/modules/easings/cubic-bezier/index.cjs +1 -1
  48. package/dist/modules/easings/cubic-bezier/index.js +1 -1
  49. package/dist/modules/easings/eases/index.cjs +1 -1
  50. package/dist/modules/easings/eases/index.js +1 -1
  51. package/dist/modules/easings/eases/parser.cjs +1 -1
  52. package/dist/modules/easings/eases/parser.js +1 -1
  53. package/dist/modules/easings/index.cjs +1 -1
  54. package/dist/modules/easings/index.js +1 -1
  55. package/dist/modules/easings/irregular/index.cjs +1 -1
  56. package/dist/modules/easings/irregular/index.js +1 -1
  57. package/dist/modules/easings/linear/index.cjs +1 -1
  58. package/dist/modules/easings/linear/index.js +1 -1
  59. package/dist/modules/easings/none.cjs +1 -1
  60. package/dist/modules/easings/none.js +1 -1
  61. package/dist/modules/easings/spring/index.cjs +1 -1
  62. package/dist/modules/easings/spring/index.js +1 -1
  63. package/dist/modules/easings/steps/index.cjs +1 -1
  64. package/dist/modules/easings/steps/index.js +1 -1
  65. package/dist/modules/engine/engine.cjs +2 -2
  66. package/dist/modules/engine/engine.js +2 -2
  67. package/dist/modules/engine/index.cjs +1 -1
  68. package/dist/modules/engine/index.js +1 -1
  69. package/dist/modules/events/index.cjs +1 -1
  70. package/dist/modules/events/index.js +1 -1
  71. package/dist/modules/events/scroll.cjs +1 -1
  72. package/dist/modules/events/scroll.js +1 -1
  73. package/dist/modules/index.cjs +4 -1
  74. package/dist/modules/index.d.ts +1 -0
  75. package/dist/modules/index.js +2 -1
  76. package/dist/modules/layout/index.cjs +15 -0
  77. package/dist/modules/layout/index.d.ts +1 -0
  78. package/dist/modules/layout/index.js +8 -0
  79. package/dist/modules/layout/layout.cjs +1384 -0
  80. package/dist/modules/layout/layout.d.ts +211 -0
  81. package/dist/modules/layout/layout.js +1381 -0
  82. package/dist/modules/scope/index.cjs +1 -1
  83. package/dist/modules/scope/index.js +1 -1
  84. package/dist/modules/scope/scope.cjs +1 -1
  85. package/dist/modules/scope/scope.js +1 -1
  86. package/dist/modules/svg/drawable.cjs +1 -1
  87. package/dist/modules/svg/drawable.js +1 -1
  88. package/dist/modules/svg/helpers.cjs +1 -1
  89. package/dist/modules/svg/helpers.js +1 -1
  90. package/dist/modules/svg/index.cjs +1 -1
  91. package/dist/modules/svg/index.js +1 -1
  92. package/dist/modules/svg/morphto.cjs +1 -1
  93. package/dist/modules/svg/morphto.js +1 -1
  94. package/dist/modules/svg/motionpath.cjs +11 -7
  95. package/dist/modules/svg/motionpath.js +11 -7
  96. package/dist/modules/text/index.cjs +1 -1
  97. package/dist/modules/text/index.js +1 -1
  98. package/dist/modules/text/split.cjs +23 -9
  99. package/dist/modules/text/split.js +23 -9
  100. package/dist/modules/timeline/index.cjs +1 -1
  101. package/dist/modules/timeline/index.js +1 -1
  102. package/dist/modules/timeline/position.cjs +1 -1
  103. package/dist/modules/timeline/position.js +1 -1
  104. package/dist/modules/timeline/timeline.cjs +14 -6
  105. package/dist/modules/timeline/timeline.d.ts +2 -0
  106. package/dist/modules/timeline/timeline.js +15 -7
  107. package/dist/modules/timer/index.cjs +1 -1
  108. package/dist/modules/timer/index.js +1 -1
  109. package/dist/modules/timer/timer.cjs +26 -13
  110. package/dist/modules/timer/timer.d.ts +1 -0
  111. package/dist/modules/timer/timer.js +27 -14
  112. package/dist/modules/types/index.d.ts +3 -1
  113. package/dist/modules/utils/chainable.cjs +1 -1
  114. package/dist/modules/utils/chainable.js +1 -1
  115. package/dist/modules/utils/index.cjs +1 -1
  116. package/dist/modules/utils/index.js +1 -1
  117. package/dist/modules/utils/number.cjs +1 -1
  118. package/dist/modules/utils/number.js +1 -1
  119. package/dist/modules/utils/random.cjs +1 -1
  120. package/dist/modules/utils/random.js +1 -1
  121. package/dist/modules/utils/stagger.cjs +1 -1
  122. package/dist/modules/utils/stagger.js +1 -1
  123. package/dist/modules/utils/target.cjs +1 -1
  124. package/dist/modules/utils/target.js +1 -1
  125. package/dist/modules/utils/time.cjs +1 -1
  126. package/dist/modules/utils/time.js +1 -1
  127. package/dist/modules/waapi/composition.cjs +1 -1
  128. package/dist/modules/waapi/composition.js +1 -1
  129. package/dist/modules/waapi/index.cjs +1 -1
  130. package/dist/modules/waapi/index.js +1 -1
  131. package/dist/modules/waapi/waapi.cjs +15 -7
  132. package/dist/modules/waapi/waapi.js +16 -8
  133. package/package.json +8 -2
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Anime.js - ESM bundle
3
- * @version v4.2.1
3
+ * @version v4.3.0-beta.0
4
4
  * @license MIT
5
5
  * @copyright 2025 - Julian Garnier
6
6
  */
@@ -37,7 +37,7 @@
37
37
  /** @typedef {JSAnimation|Timeline} Renderable */
38
38
  /** @typedef {Timer|Renderable} Tickable */
39
39
  /** @typedef {Timer&JSAnimation&Timeline} CallbackArgument */
40
- /** @typedef {Animatable|Tickable|WAAPIAnimation|Draggable|ScrollObserver|TextSplitter|Scope} Revertible */
40
+ /** @typedef {Animatable|Tickable|WAAPIAnimation|Draggable|ScrollObserver|TextSplitter|Scope|AutoLayout} Revertible */
41
41
 
42
42
  // Stagger types
43
43
 
@@ -361,6 +361,7 @@
361
361
  * @typedef {Object} TimelineOptions
362
362
  * @property {DefaultsParams} [defaults]
363
363
  * @property {EasingParam} [playbackEase]
364
+ * @property {Boolean} [composition]
364
365
  */
365
366
 
366
367
  /**
@@ -638,8 +639,10 @@
638
639
  // TODO: Do we need to check if we're running inside a worker ?
639
640
  const isBrowser = typeof window !== 'undefined';
640
641
 
641
- /** @type {Window & {AnimeJS: Array}|null} */
642
- const win = isBrowser ? /** @type {Window & {AnimeJS: Array}} */(/** @type {unknown} */(window)) : null;
642
+ /** @typedef {Window & {AnimeJS: Array} & {AnimeJSDevTools: any}|null} AnimeJSWindow
643
+
644
+ /** @type {AnimeJSWindow} */
645
+ const win = isBrowser ? /** @type {AnimeJSWindow} */(/** @type {unknown} */(window)) : null;
643
646
 
644
647
  /** @type {Document|null} */
645
648
  const doc = isBrowser ? document : null;
@@ -691,7 +694,7 @@ const proxyTargetSymbol = Symbol();
691
694
  const minValue = 1e-11;
692
695
  const maxValue = 1e12;
693
696
  const K = 1e3;
694
- const maxFps = 120;
697
+ const maxFps = 240;
695
698
 
696
699
  // Strings
697
700
 
@@ -796,7 +799,9 @@ const globals = {
796
799
  tickThreshold: 200,
797
800
  };
798
801
 
799
- const globalVersions = { version: '4.2.1', engine: null };
802
+ const devTools = isBrowser && win.AnimeJSDevTools;
803
+
804
+ const globalVersions = { version: '4.3.0-beta.0', engine: null };
800
805
 
801
806
  if (isBrowser) {
802
807
  if (!win.AnimeJS) win.AnimeJS = [];
@@ -1826,7 +1831,7 @@ class Clock {
1826
1831
  /** @type {Number} */
1827
1832
  this._currentTime = initTime;
1828
1833
  /** @type {Number} */
1829
- this._elapsedTime = initTime;
1834
+ this._lastTickTime = initTime;
1830
1835
  /** @type {Number} */
1831
1836
  this._startTime = initTime;
1832
1837
  /** @type {Number} */
@@ -1834,7 +1839,7 @@ class Clock {
1834
1839
  /** @type {Number} */
1835
1840
  this._scheduledTime = 0;
1836
1841
  /** @type {Number} */
1837
- this._frameDuration = round$1(K / maxFps, 0);
1842
+ this._frameDuration = K / maxFps;
1838
1843
  /** @type {Number} */
1839
1844
  this._fps = maxFps;
1840
1845
  /** @type {Number} */
@@ -1855,7 +1860,8 @@ class Clock {
1855
1860
  const previousFrameDuration = this._frameDuration;
1856
1861
  const fr = +frameRate;
1857
1862
  const fps = fr < minValue ? minValue : fr;
1858
- const frameDuration = round$1(K / fps, 0);
1863
+ const frameDuration = K / fps;
1864
+ if (fps > defaults.frameRate) defaults.frameRate = fps;
1859
1865
  this._fps = fps;
1860
1866
  this._frameDuration = frameDuration;
1861
1867
  this._scheduledTime += frameDuration - previousFrameDuration;
@@ -1876,14 +1882,13 @@ class Clock {
1876
1882
  */
1877
1883
  requestTick(time) {
1878
1884
  const scheduledTime = this._scheduledTime;
1879
- const elapsedTime = this._elapsedTime;
1880
- this._elapsedTime += (time - elapsedTime);
1881
- // If the elapsed time is lower than the scheduled time
1885
+ this._lastTickTime = time;
1886
+ // If the current time is lower than the scheduled time
1882
1887
  // this means not enough time has passed to hit one frameDuration
1883
1888
  // so skip that frame
1884
- if (elapsedTime < scheduledTime) return tickModes.NONE;
1889
+ if (time < scheduledTime) return tickModes.NONE;
1885
1890
  const frameDuration = this._frameDuration;
1886
- const frameDelta = elapsedTime - scheduledTime;
1891
+ const frameDelta = time - scheduledTime;
1887
1892
  // Ensures that _scheduledTime progresses in steps of at least 1 frameDuration.
1888
1893
  // Skips ahead if the actual elapsed time is higher.
1889
1894
  this._scheduledTime += frameDelta < frameDuration ? frameDuration : frameDelta;
@@ -2020,7 +2025,7 @@ class Engine extends Clock {
2020
2025
 
2021
2026
  wake() {
2022
2027
  if (this.useDefaultMainLoop && !this.reqId) {
2023
- // Imediatly request a tick to update engine._elapsedTime and get accurate offsetPosition calculation in timer.js
2028
+ // Imediatly request a tick to update engine._lastTickTime and get accurate offsetPosition calculation in timer.js
2024
2029
  this.requestTick(now());
2025
2030
  this.reqId = engineTickMethod(tickEngine);
2026
2031
  }
@@ -2508,6 +2513,8 @@ class Timer extends Clock {
2508
2513
 
2509
2514
  super(0);
2510
2515
 
2516
+ ++timerId;
2517
+
2511
2518
  const {
2512
2519
  id,
2513
2520
  delay,
@@ -2529,31 +2536,42 @@ class Timer extends Clock {
2529
2536
 
2530
2537
  if (scope.current) scope.current.register(this);
2531
2538
 
2532
- const timerInitTime = parent ? 0 : engine._elapsedTime;
2539
+ const timerInitTime = parent ? 0 : engine._lastTickTime;
2533
2540
  const timerDefaults = parent ? parent.defaults : globals.defaults;
2534
2541
  const timerDelay = /** @type {Number} */(isFnc(delay) || isUnd(delay) ? timerDefaults.delay : +delay);
2535
2542
  const timerDuration = isFnc(duration) || isUnd(duration) ? Infinity : +duration;
2536
2543
  const timerLoop = setValue(loop, timerDefaults.loop);
2537
2544
  const timerLoopDelay = setValue(loopDelay, timerDefaults.loopDelay);
2538
- const timerIterationCount = timerLoop === true ||
2539
- timerLoop === Infinity ||
2540
- /** @type {Number} */(timerLoop) < 0 ? Infinity :
2541
- /** @type {Number} */(timerLoop) + 1;
2545
+ let timerIterationCount = timerLoop === true ||
2546
+ timerLoop === Infinity ||
2547
+ /** @type {Number} */(timerLoop) < 0 ? Infinity :
2548
+ /** @type {Number} */(timerLoop) + 1;
2549
+
2550
+ if (devTools) {
2551
+ const isInfinite = timerIterationCount === Infinity;
2552
+ const registered = devTools.register(this, parameters, isInfinite);
2553
+ if (registered && isInfinite) {
2554
+ const minIterations = alternate ? 2 : 1;
2555
+ const iterations = parent ? devTools.maxNestedInfiniteLoops : devTools.maxInfiniteLoops;
2556
+ timerIterationCount = Math.max(iterations, minIterations);
2557
+ }
2558
+ }
2542
2559
 
2543
2560
  let offsetPosition = 0;
2544
2561
 
2545
2562
  if (parent) {
2546
2563
  offsetPosition = parentPosition;
2547
2564
  } else {
2548
- // Make sure to tick the engine once if not currently running to get up to date engine._elapsedTime
2565
+ // Make sure to tick the engine once if not currently running to get up to date engine._lastTickTime
2549
2566
  // to avoid big gaps with the following offsetPosition calculation
2550
2567
  if (!engine.reqId) engine.requestTick(now());
2551
2568
  // Make sure to scale the offset position with globals.timeScale to properly handle seconds unit
2552
- offsetPosition = (engine._elapsedTime - engine._startTime) * globals.timeScale;
2569
+ offsetPosition = (engine._lastTickTime - engine._startTime) * globals.timeScale;
2553
2570
  }
2554
2571
 
2555
2572
  // Timer's parameters
2556
- this.id = !isUnd(id) ? id : ++timerId;
2573
+ /** @type {String|Number} */
2574
+ this.id = !isUnd(id) ? id : timerId;
2557
2575
  /** @type {Timeline} */
2558
2576
  this.parent = parent;
2559
2577
  // Total duration of the timer
@@ -2613,7 +2631,7 @@ class Timer extends Clock {
2613
2631
 
2614
2632
  // Clock's parameters
2615
2633
  /** @type {Number} */
2616
- this._elapsedTime = timerInitTime;
2634
+ this._lastTickTime = timerInitTime;
2617
2635
  /** @type {Number} */
2618
2636
  this._startTime = timerInitTime;
2619
2637
  /** @type {Number} */
@@ -2644,7 +2662,7 @@ class Timer extends Clock {
2644
2662
  }
2645
2663
 
2646
2664
  get iterationCurrentTime() {
2647
- return round$1(this._iterationTime, globals.precision);
2665
+ return clamp$1(round$1(this._iterationTime, globals.precision), 0, this.iterationDuration);
2648
2666
  }
2649
2667
 
2650
2668
  set iterationCurrentTime(time) {
@@ -2742,9 +2760,9 @@ class Timer extends Clock {
2742
2760
  /** @return {this} */
2743
2761
  resetTime() {
2744
2762
  const timeScale = 1 / (this._speed * engine._speed);
2745
- // TODO: See if we can safely use engine._elapsedTime here
2763
+ // TODO: See if we can safely use engine._lastTickTime here
2746
2764
  // if (!engine.reqId) engine.requestTick(now())
2747
- // this._startTime = engine._elapsedTime - (this._currentTime + this._delay) * timeScale;
2765
+ // this._startTime = engine._lastTickTime - (this._currentTime + this._delay) * timeScale;
2748
2766
  this._startTime = now() - (this._currentTime + this._delay) * timeScale;
2749
2767
  return this;
2750
2768
  }
@@ -3258,6 +3276,7 @@ const fastSetValuesArray = [null, null];
3258
3276
  const keyObjectTarget = { to: null };
3259
3277
 
3260
3278
  let tweenId = 0;
3279
+ let JSAnimationId = 0;
3261
3280
  let keyframes;
3262
3281
  /** @type {TweenParamsOptions & TweenValues} */
3263
3282
  let key;
@@ -3372,6 +3391,8 @@ class JSAnimation extends Timer {
3372
3391
 
3373
3392
  super(/** @type {TimerParams & AnimationParams} */(parameters), parent, parentPosition);
3374
3393
 
3394
+ ++JSAnimationId;
3395
+
3375
3396
  const parsedTargets = registerTargets(targets);
3376
3397
  const targetsLength = parsedTargets.length;
3377
3398
 
@@ -3381,6 +3402,7 @@ class JSAnimation extends Timer {
3381
3402
  const params = /** @type {AnimationParams} */(kfParams ? mergeObjects(generateKeyframes(/** @type {DurationKeyframes} */(kfParams), parameters), parameters) : parameters);
3382
3403
 
3383
3404
  const {
3405
+ id,
3384
3406
  delay,
3385
3407
  duration,
3386
3408
  ease,
@@ -3391,11 +3413,12 @@ class JSAnimation extends Timer {
3391
3413
  } = params;
3392
3414
 
3393
3415
  const animDefaults = parent ? parent.defaults : globals.defaults;
3394
- const animaPlaybackEase = setValue(playbackEase, animDefaults.playbackEase);
3395
- const animEase = animaPlaybackEase ? parseEase(animaPlaybackEase) : null;
3396
- const hasSpring = !isUnd(ease) && !isUnd(/** @type {Spring} */(ease).ease);
3397
- const tEasing = hasSpring ? /** @type {Spring} */(ease).ease : setValue(ease, animEase ? 'linear' : animDefaults.ease);
3398
- const tDuration = hasSpring ? /** @type {Spring} */(ease).settlingDuration : setValue(duration, animDefaults.duration);
3416
+ const animEase = setValue(ease, animDefaults.ease);
3417
+ const animPlaybackEase = setValue(playbackEase, animDefaults.playbackEase);
3418
+ const parsedAnimPlaybackEase = animPlaybackEase ? parseEase(animPlaybackEase) : null;
3419
+ const hasSpring = !isUnd(/** @type {Spring} */(animEase).ease);
3420
+ const tEasing = hasSpring ? /** @type {Spring} */(animEase).ease : setValue(ease, parsedAnimPlaybackEase ? 'linear' : animDefaults.ease);
3421
+ const tDuration = hasSpring ? /** @type {Spring} */(animEase).settlingDuration : setValue(duration, animDefaults.duration);
3399
3422
  const tDelay = setValue(delay, animDefaults.delay);
3400
3423
  const tModifier = modifier || animDefaults.modifier;
3401
3424
  // If no composition is defined and the targets length is high (>= 1000) set the composition to 'none' (0) for faster tween creation
@@ -3403,7 +3426,7 @@ class JSAnimation extends Timer {
3403
3426
  // const absoluteOffsetTime = this._offset;
3404
3427
  const absoluteOffsetTime = this._offset + (parent ? parent._offset : 0);
3405
3428
  // This allows targeting the current animation in the spring onComplete callback
3406
- if (hasSpring) /** @type {Spring} */(ease).parent = this;
3429
+ if (hasSpring) /** @type {Spring} */(animEase).parent = this;
3407
3430
 
3408
3431
  let iterationDuration = NaN;
3409
3432
  let iterationDelay = NaN;
@@ -3547,6 +3570,8 @@ class JSAnimation extends Timer {
3547
3570
  if (isFromToValue) {
3548
3571
  decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[0], target, ti, tl) : tweenFromValue, fromTargetObject);
3549
3572
  decomposeRawValue(isFromToArray ? getFunctionValue(tweenToValue[1], target, ti, tl, toFunctionStore) : tweenToValue, toTargetObject);
3573
+ // Needed to force an inline style registration
3574
+ const originalValue = getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore);
3550
3575
  if (fromTargetObject.t === valueTypes.NUMBER) {
3551
3576
  if (prevSibling) {
3552
3577
  if (prevSibling._valueType === valueTypes.UNIT) {
@@ -3555,7 +3580,7 @@ class JSAnimation extends Timer {
3555
3580
  }
3556
3581
  } else {
3557
3582
  decomposeRawValue(
3558
- getOriginalAnimatableValue(target, propName, tweenType, inlineStylesStore),
3583
+ originalValue,
3559
3584
  decomposedOriginalValue
3560
3585
  );
3561
3586
  if (decomposedOriginalValue.t === valueTypes.UNIT) {
@@ -3774,12 +3799,14 @@ class JSAnimation extends Timer {
3774
3799
  }
3775
3800
  /** @type {TargetsArray} */
3776
3801
  this.targets = parsedTargets;
3802
+ /** @type {String|Number} */
3803
+ this.id = !isUnd(id) ? id : JSAnimationId;
3777
3804
  /** @type {Number} */
3778
3805
  this.duration = iterationDuration === minValue ? minValue : clampInfinity(((iterationDuration + this._loopDelay) * this.iterationCount) - this._loopDelay) || minValue;
3779
3806
  /** @type {Callback<this>} */
3780
3807
  this.onRender = onRender || animDefaults.onRender;
3781
3808
  /** @type {EasingFunction} */
3782
- this._ease = animEase;
3809
+ this._ease = parsedAnimPlaybackEase;
3783
3810
  /** @type {Number} */
3784
3811
  this._delay = iterationDelay;
3785
3812
  // NOTE: I'm keeping delay values separated from offsets in timelines because delays can override previous tweens and it could be confusing to debug a timeline with overridden tweens and no associated visible delays.
@@ -4117,11 +4144,11 @@ function addTlChild(childParams, tl, timePosition, targets, index, length) {
4117
4144
  const isSetter = isNum(childParams.duration) && /** @type {Number} */(childParams.duration) <= minValue;
4118
4145
  // Offset the tl position with -minValue for 0 duration animations or .set() calls in order to align their end value with the defined position
4119
4146
  const adjustedPosition = isSetter ? timePosition - minValue : timePosition;
4120
- tick(tl, adjustedPosition, 1, 1, tickModes.AUTO);
4147
+ if (tl.composition) tick(tl, adjustedPosition, 1, 1, tickModes.AUTO);
4121
4148
  const tlChild = targets ?
4122
4149
  new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, length) :
4123
4150
  new Timer(/** @type {TimerParams} */(childParams), tl, adjustedPosition);
4124
- tlChild.init(true);
4151
+ if (tl.composition) tlChild.init(true);
4125
4152
  // TODO: Might be better to insert at a position relative to startTime?
4126
4153
  addChild(tl, tlChild);
4127
4154
  forEachChildren(tl, (/** @type {Renderable} */child) => {
@@ -4133,6 +4160,8 @@ function addTlChild(childParams, tl, timePosition, targets, index, length) {
4133
4160
  return tl;
4134
4161
  }
4135
4162
 
4163
+ let TLId = 0;
4164
+
4136
4165
  class Timeline extends Timer {
4137
4166
 
4138
4167
  /**
@@ -4140,6 +4169,9 @@ class Timeline extends Timer {
4140
4169
  */
4141
4170
  constructor(parameters = {}) {
4142
4171
  super(/** @type {TimerParams&TimelineParams} */(parameters), null, 0);
4172
+ ++TLId;
4173
+ /** @type {String|Number} */
4174
+ this.id = !isUnd(parameters.id) ? parameters.id : TLId;
4143
4175
  /** @type {Number} */
4144
4176
  this.duration = 0; // TL duration starts at 0 and grows when adding children
4145
4177
  /** @type {Record<String, Number>} */
@@ -4148,6 +4180,8 @@ class Timeline extends Timer {
4148
4180
  const globalDefaults = globals.defaults;
4149
4181
  /** @type {DefaultsParams} */
4150
4182
  this.defaults = defaultsParams ? mergeObjects(defaultsParams, globalDefaults) : globalDefaults;
4183
+ /** @type {Boolean} */
4184
+ this.composition = setValue(parameters.composition, true);
4151
4185
  /** @type {Callback<this>} */
4152
4186
  this.onRender = parameters.onRender || globalDefaults.onRender;
4153
4187
  const tlPlaybackEase = setValue(parameters.playbackEase, globalDefaults.playbackEase);
@@ -4225,7 +4259,8 @@ class Timeline extends Timer {
4225
4259
  parseTimelinePosition(this,a2),
4226
4260
  );
4227
4261
  }
4228
- return this.init(true);
4262
+ if (this.composition) this.init(true);
4263
+ return this;
4229
4264
  }
4230
4265
  }
4231
4266
 
@@ -4252,7 +4287,7 @@ class Timeline extends Timer {
4252
4287
  if (isUnd(synced) || synced && isUnd(synced.pause)) return this;
4253
4288
  synced.pause();
4254
4289
  const duration = +(/** @type {globalThis.Animation} */(synced).effect ? /** @type {globalThis.Animation} */(synced).effect.getTiming().duration : /** @type {Tickable} */(synced).duration);
4255
- return this.add(synced, { currentTime: [0, duration], duration, ease: 'linear' }, position);
4290
+ return this.add(synced, { currentTime: [0, duration], duration, delay: 0, ease: 'linear', playbackEase: 'linear' }, position);
4256
4291
  }
4257
4292
 
4258
4293
  /**
@@ -4275,7 +4310,7 @@ class Timeline extends Timer {
4275
4310
  */
4276
4311
  call(callback, position) {
4277
4312
  if (isUnd(callback) || callback && !isFnc(callback)) return this;
4278
- return this.add({ duration: 0, onComplete: () => callback(this) }, position);
4313
+ return this.add({ duration: 0, delay: 0, onComplete: () => callback(this) }, position);
4279
4314
  }
4280
4315
 
4281
4316
  /**
@@ -7281,1513 +7316,2884 @@ var index$3 = /*#__PURE__*/Object.freeze({
7281
7316
  steps: steps
7282
7317
  });
7283
7318
 
7284
- // Chain-able utilities
7285
7319
 
7286
- const numberUtils = numberImports; // Needed to keep the import when bundling
7287
7320
 
7288
- const chainables = {};
7289
7321
 
7290
- /**
7291
- * @callback UtilityFunction
7292
- * @param {...*} args
7293
- * @return {Number|String}
7294
- *
7295
- * @param {UtilityFunction} fn
7296
- * @param {Number} [last=0]
7297
- * @return {function(...(Number|String)): function(Number|String): (Number|String)}
7298
- */
7299
- const curry = (fn, last = 0) => (...args) => last ? v => fn(...args, v) : v => fn(v, ...args);
7300
7322
 
7301
- /**
7302
- * @param {Function} fn
7303
- * @return {function(...(Number|String))}
7304
- */
7305
- const chain = fn => {
7306
- return (...args) => {
7307
- const result = fn(...args);
7308
- return new Proxy(noop, {
7309
- apply: (_, __, [v]) => result(v),
7310
- get: (_, prop) => chain(/**@param {...Number|String} nextArgs */(...nextArgs) => {
7311
- const nextResult = chainables[prop](...nextArgs);
7312
- return (/**@type {Number|String} */v) => nextResult(result(v));
7313
- })
7314
- });
7315
- }
7316
- };
7317
7323
 
7318
- /**
7319
- * @param {UtilityFunction} fn
7320
- * @param {String} name
7321
- * @param {Number} [right]
7322
- * @return {function(...(Number|String)): UtilityFunction}
7323
- */
7324
- const makeChainable = (name, fn, right = 0) => {
7325
- const chained = (...args) => (args.length < fn.length ? chain(curry(fn, right)) : fn)(...args);
7326
- if (!chainables[name]) chainables[name] = chained;
7327
- return chained;
7328
- };
7329
7324
 
7330
7325
  /**
7331
- * @typedef {Object} ChainablesMap
7332
- * @property {ChainedClamp} clamp
7333
- * @property {ChainedRound} round
7334
- * @property {ChainedSnap} snap
7335
- * @property {ChainedWrap} wrap
7336
- * @property {ChainedLerp} lerp
7337
- * @property {ChainedDamp} damp
7338
- * @property {ChainedMapRange} mapRange
7339
- * @property {ChainedRoundPad} roundPad
7340
- * @property {ChainedPadStart} padStart
7341
- * @property {ChainedPadEnd} padEnd
7342
- * @property {ChainedDegToRad} degToRad
7343
- * @property {ChainedRadToDeg} radToDeg
7326
+ * Converts an easing function into a valid CSS linear() timing function string
7327
+ * @param {EasingFunction} fn
7328
+ * @param {number} [samples=100]
7329
+ * @returns {string} CSS linear() timing function
7344
7330
  */
7331
+ const easingToLinear = (fn, samples = 100) => {
7332
+ const points = [];
7333
+ for (let i = 0; i <= samples; i++) points.push(round$1(fn(i / samples), 4));
7334
+ return `linear(${points.join(', ')})`;
7335
+ };
7345
7336
 
7346
- /**
7347
- * @callback ChainedUtilsResult
7348
- * @param {Number} value - The value to process through the chained operations
7349
- * @return {Number} The processed result
7350
- */
7337
+ const WAAPIEasesLookups = {};
7351
7338
 
7352
7339
  /**
7353
- * @typedef {ChainablesMap & ChainedUtilsResult} ChainableUtil
7340
+ * @param {EasingParam} ease
7341
+ * @return {String}
7354
7342
  */
7343
+ const parseWAAPIEasing = (ease) => {
7344
+ let parsedEase = WAAPIEasesLookups[ease];
7345
+ if (parsedEase) return parsedEase;
7346
+ parsedEase = 'linear';
7347
+ if (isStr(ease)) {
7348
+ if (
7349
+ stringStartsWith(ease, 'linear') ||
7350
+ stringStartsWith(ease, 'cubic-') ||
7351
+ stringStartsWith(ease, 'steps') ||
7352
+ stringStartsWith(ease, 'ease')
7353
+ ) {
7354
+ parsedEase = ease;
7355
+ } else if (stringStartsWith(ease, 'cubicB')) {
7356
+ parsedEase = toLowerCase(ease);
7357
+ } else {
7358
+ const parsed = parseEaseString(ease);
7359
+ if (isFnc(parsed)) parsedEase = parsed === none ? 'linear' : easingToLinear(parsed);
7360
+ }
7361
+ // Only cache string based easing name, otherwise function arguments get lost
7362
+ WAAPIEasesLookups[ease] = parsedEase;
7363
+ } else if (isFnc(ease)) {
7364
+ const easing = easingToLinear(ease);
7365
+ if (easing) parsedEase = easing;
7366
+ } else if (/** @type {Spring} */(ease).ease) {
7367
+ parsedEase = easingToLinear(/** @type {Spring} */(ease).ease);
7368
+ }
7369
+ return parsedEase;
7370
+ };
7355
7371
 
7356
- // Chainable
7372
+ const transformsShorthands = ['x', 'y', 'z'];
7373
+ const commonDefaultPXProperties = [
7374
+ 'perspective',
7375
+ 'width',
7376
+ 'height',
7377
+ 'margin',
7378
+ 'padding',
7379
+ 'top',
7380
+ 'right',
7381
+ 'bottom',
7382
+ 'left',
7383
+ 'borderWidth',
7384
+ 'fontSize',
7385
+ 'borderRadius',
7386
+ ...transformsShorthands
7387
+ ];
7357
7388
 
7358
- /**
7359
- * @callback ChainedRoundPad
7360
- * @param {Number} decimalLength - Number of decimal places
7361
- * @return {ChainableUtil}
7362
- */
7363
- const roundPad = /** @type {typeof numberUtils.roundPad & ChainedRoundPad} */(makeChainable('roundPad', numberUtils.roundPad));
7389
+ const validIndividualTransforms = /*#__PURE__*/ (() => [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))])();
7364
7390
 
7365
- /**
7366
- * @callback ChainedPadStart
7367
- * @param {Number} totalLength - Target length
7368
- * @param {String} padString - String to pad with
7369
- * @return {ChainableUtil}
7370
- */
7371
- const padStart = /** @type {typeof numberUtils.padStart & ChainedPadStart} */(makeChainable('padStart', numberUtils.padStart));
7391
+ let transformsPropertiesRegistered = null;
7372
7392
 
7373
7393
  /**
7374
- * @callback ChainedPadEnd
7375
- * @param {Number} totalLength - Target length
7376
- * @param {String} padString - String to pad with
7377
- * @return {ChainableUtil}
7394
+ * @param {String} propName
7395
+ * @param {WAAPIKeyframeValue} value
7396
+ * @param {DOMTarget} $el
7397
+ * @param {Number} i
7398
+ * @param {Number} targetsLength
7399
+ * @return {String}
7378
7400
  */
7379
- const padEnd = /** @type {typeof numberUtils.padEnd & ChainedPadEnd} */(makeChainable('padEnd', numberUtils.padEnd));
7401
+ const normalizeTweenValue = (propName, value, $el, i, targetsLength) => {
7402
+ // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables
7403
+ let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, targetsLength);
7404
+ if (!isNum(v)) return v;
7405
+ if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`;
7406
+ if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`;
7407
+ return `${v}`;
7408
+ };
7380
7409
 
7381
7410
  /**
7382
- * @callback ChainedWrap
7383
- * @param {Number} min - Minimum boundary
7384
- * @param {Number} max - Maximum boundary
7385
- * @return {ChainableUtil}
7411
+ * @param {DOMTarget} $el
7412
+ * @param {String} propName
7413
+ * @param {WAAPIKeyframeValue} from
7414
+ * @param {WAAPIKeyframeValue} to
7415
+ * @param {Number} i
7416
+ * @param {Number} targetsLength
7417
+ * @return {WAAPITweenValue}
7386
7418
  */
7387
- const wrap = /** @type {typeof numberUtils.wrap & ChainedWrap} */(makeChainable('wrap', numberUtils.wrap));
7419
+ const parseIndividualTweenValue = ($el, propName, from, to, i, targetsLength) => {
7420
+ /** @type {WAAPITweenValue} */
7421
+ let tweenValue = '0';
7422
+ const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, targetsLength) : getComputedStyle($el)[propName];
7423
+ if (!isUnd(from)) {
7424
+ const computedFrom = normalizeTweenValue(propName, from, $el, i, targetsLength);
7425
+ tweenValue = [computedFrom, computedTo];
7426
+ } else {
7427
+ tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, targetsLength)) : computedTo;
7428
+ }
7429
+ return tweenValue;
7430
+ };
7388
7431
 
7432
+ class WAAPIAnimation {
7389
7433
  /**
7390
- * @callback ChainedMapRange
7391
- * @param {Number} inLow - Input range minimum
7392
- * @param {Number} inHigh - Input range maximum
7393
- * @param {Number} outLow - Output range minimum
7394
- * @param {Number} outHigh - Output range maximum
7395
- * @return {ChainableUtil}
7434
+ * @param {DOMTargetsParam} targets
7435
+ * @param {WAAPIAnimationParams} params
7396
7436
  */
7397
- const mapRange = /** @type {typeof numberUtils.mapRange & ChainedMapRange} */(makeChainable('mapRange', numberUtils.mapRange));
7437
+ constructor(targets, params) {
7398
7438
 
7399
- /**
7400
- * @callback ChainedDegToRad
7401
- * @return {ChainableUtil}
7402
- */
7403
- const degToRad = /** @type {typeof numberUtils.degToRad & ChainedDegToRad} */(makeChainable('degToRad', numberUtils.degToRad));
7439
+ if (scope.current) scope.current.register(this);
7404
7440
 
7405
- /**
7406
- * @callback ChainedRadToDeg
7407
- * @return {ChainableUtil}
7408
- */
7409
- const radToDeg = /** @type {typeof numberUtils.radToDeg & ChainedRadToDeg} */(makeChainable('radToDeg', numberUtils.radToDeg));
7441
+ // Skip the registration and fallback to no animation in case CSS.registerProperty is not supported
7442
+ if (isNil(transformsPropertiesRegistered)) {
7443
+ if (isBrowser && (isUnd(CSS) || !Object.hasOwnProperty.call(CSS, 'registerProperty'))) {
7444
+ transformsPropertiesRegistered = false;
7445
+ } else {
7446
+ validTransforms.forEach(t => {
7447
+ const isSkew = stringStartsWith(t, 'skew');
7448
+ const isScale = stringStartsWith(t, 'scale');
7449
+ const isRotate = stringStartsWith(t, 'rotate');
7450
+ const isTranslate = stringStartsWith(t, 'translate');
7451
+ const isAngle = isRotate || isSkew;
7452
+ const syntax = isAngle ? '<angle>' : isScale ? "<number>" : isTranslate ? "<length-percentage>" : "*";
7453
+ try {
7454
+ CSS.registerProperty({
7455
+ name: '--' + t,
7456
+ syntax,
7457
+ inherits: false,
7458
+ initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0',
7459
+ });
7460
+ } catch {} });
7461
+ transformsPropertiesRegistered = true;
7462
+ }
7463
+ }
7410
7464
 
7411
- /**
7412
- * @callback ChainedSnap
7413
- * @param {Number|Array<Number>} increment - Step size or array of snap points
7414
- * @return {ChainableUtil}
7415
- */
7416
- const snap = /** @type {typeof numberUtils.snap & ChainedSnap} */(makeChainable('snap', numberUtils.snap));
7465
+ const parsedTargets = registerTargets(targets);
7466
+ const targetsLength = parsedTargets.length;
7417
7467
 
7418
- /**
7419
- * @callback ChainedClamp
7420
- * @param {Number} min - Minimum boundary
7421
- * @param {Number} max - Maximum boundary
7422
- * @return {ChainableUtil}
7423
- */
7424
- const clamp = /** @type {typeof numberUtils.clamp & ChainedClamp} */(makeChainable('clamp', numberUtils.clamp));
7468
+ if (!targetsLength) {
7469
+ console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`);
7470
+ }
7425
7471
 
7426
- /**
7427
- * @callback ChainedRound
7428
- * @param {Number} decimalLength - Number of decimal places
7429
- * @return {ChainableUtil}
7430
- */
7431
- const round = /** @type {typeof numberUtils.round & ChainedRound} */(makeChainable('round', numberUtils.round));
7472
+ const ease = setValue(params.ease, parseWAAPIEasing(globals.defaults.ease));
7473
+ const spring = /** @type {Spring} */(ease).ease && ease;
7474
+ const autoplay = setValue(params.autoplay, globals.defaults.autoplay);
7475
+ const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false;
7476
+ const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true;
7477
+ const reversed = params.reversed && /** @type {Boolean} */(params.reversed) === true;
7478
+ const loop = setValue(params.loop, globals.defaults.loop);
7479
+ const iterations = /** @type {Number} */((loop === true || loop === Infinity) ? Infinity : isNum(loop) ? loop + 1 : 1);
7480
+ /** @type {PlaybackDirection} */
7481
+ const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal';
7482
+ /** @type {FillMode} */
7483
+ const fill = 'both'; // We use 'both' here because the animation can be reversed during playback
7484
+ /** @type {String} */
7485
+ const easing = parseWAAPIEasing(ease);
7486
+ const timeScale = (globals.timeScale === 1 ? 1 : K);
7432
7487
 
7433
- /**
7434
- * @callback ChainedLerp
7435
- * @param {Number} start - Starting value
7436
- * @param {Number} end - Ending value
7437
- * @return {ChainableUtil}
7438
- */
7439
- const lerp = /** @type {typeof numberUtils.lerp & ChainedLerp} */(makeChainable('lerp', numberUtils.lerp, 1));
7488
+ /** @type {DOMTargetsArray}] */
7489
+ this.targets = parsedTargets;
7490
+ /** @type {Array<globalThis.Animation>}] */
7491
+ this.animations = [];
7492
+ /** @type {globalThis.Animation}] */
7493
+ this.controlAnimation = null;
7494
+ /** @type {Callback<this>} */
7495
+ this.onComplete = params.onComplete || /** @type {Callback<WAAPIAnimation>} */(/** @type {unknown} */(globals.defaults.onComplete));
7496
+ /** @type {Number} */
7497
+ this.duration = 0;
7498
+ /** @type {Boolean} */
7499
+ this.muteCallbacks = false;
7500
+ /** @type {Boolean} */
7501
+ this.completed = false;
7502
+ /** @type {Boolean} */
7503
+ this.paused = !autoplay || scroll !== false;
7504
+ /** @type {Boolean} */
7505
+ this.reversed = reversed;
7506
+ /** @type {Boolean} */
7507
+ this.persist = setValue(params.persist, globals.defaults.persist);
7508
+ /** @type {Boolean|ScrollObserver} */
7509
+ this.autoplay = autoplay;
7510
+ /** @type {Number} */
7511
+ this._speed = setValue(params.playbackRate, globals.defaults.playbackRate);
7512
+ /** @type {Function} */
7513
+ this._resolve = noop; // Used by .then()
7514
+ /** @type {Number} */
7515
+ this._completed = 0;
7516
+ /** @type {Array.<Object>} */
7517
+ this._inlineStyles = [];
7440
7518
 
7441
- /**
7442
- * @callback ChainedDamp
7443
- * @param {Number} start - Starting value
7444
- * @param {Number} end - Target value
7445
- * @param {Number} deltaTime - Delta time in ms
7446
- * @return {ChainableUtil}
7447
- */
7448
- const damp = /** @type {typeof numberUtils.damp & ChainedDamp} */(makeChainable('damp', numberUtils.damp, 1));
7519
+ parsedTargets.forEach(($el, i) => {
7449
7520
 
7450
- /**
7451
- * Generate a random number between optional min and max (inclusive) and decimal precision
7452
- *
7453
- * @callback RandomNumberGenerator
7454
- * @param {Number} [min=0] - The minimum value (inclusive)
7455
- * @param {Number} [max=1] - The maximum value (inclusive)
7456
- * @param {Number} [decimalLength=0] - Number of decimal places to round to
7457
- * @return {Number} A random number between min and max
7458
- */
7521
+ const cachedTransforms = $el[transformsSymbol];
7522
+ const hasIndividualTransforms = validIndividualTransforms.some(t => params.hasOwnProperty(t));
7523
+ const elStyle = $el.style;
7524
+ const inlineStyles = this._inlineStyles[i] = {};
7459
7525
 
7460
- /**
7461
- * Generates a random number between min and max (inclusive) with optional decimal precision
7462
- *
7463
- * @type {RandomNumberGenerator}
7464
- */
7465
- const random = (min = 0, max = 1, decimalLength = 0) => {
7466
- const m = 10 ** decimalLength;
7467
- return Math.floor((Math.random() * (max - min + (1 / m)) + min) * m) / m;
7468
- };
7526
+ /** @type {Number} */
7527
+ const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, targetsLength)) * timeScale;
7528
+ /** @type {Number} */
7529
+ const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, targetsLength) * timeScale;
7530
+ /** @type {CompositeOperation} */
7531
+ const composite = /** @type {CompositeOperation} */(setValue(params.composition, 'replace'));
7469
7532
 
7470
- let _seed = 0;
7533
+ for (let name in params) {
7534
+ if (!isKey(name)) continue;
7535
+ /** @type {PropertyIndexedKeyframes} */
7536
+ const keyframes = {};
7537
+ /** @type {KeyframeAnimationOptions} */
7538
+ const tweenParams = { iterations, direction, fill, easing, duration, delay, composite };
7539
+ const propertyValue = params[name];
7540
+ const individualTransformProperty = hasIndividualTransforms ? validTransforms.includes(name) ? name : shortTransforms.get(name) : false;
7471
7541
 
7472
- /**
7473
- * Creates a seeded pseudorandom number generator function
7474
- *
7475
- * @param {Number} [seed] - The seed value for the random number generator
7476
- * @param {Number} [seededMin=0] - The minimum default value (inclusive) of the returned function
7477
- * @param {Number} [seededMax=1] - The maximum default value (inclusive) of the returned function
7478
- * @param {Number} [seededDecimalLength=0] - Default number of decimal places to round to of the returned function
7479
- * @return {RandomNumberGenerator} A function to generate a random number between optional min and max (inclusive) and decimal precision
7480
- */
7481
- const createSeededRandom = (seed, seededMin = 0, seededMax = 1, seededDecimalLength = 0) => {
7482
- let t = seed === undefined ? _seed++ : seed;
7483
- return (min = seededMin, max = seededMax, decimalLength = seededDecimalLength) => {
7484
- t += 0x6D2B79F5;
7485
- t = Math.imul(t ^ t >>> 15, t | 1);
7486
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
7487
- const m = 10 ** decimalLength;
7488
- return Math.floor(((((t ^ t >>> 14) >>> 0) / 4294967296) * (max - min + (1 / m)) + min) * m) / m;
7542
+ const styleName = individualTransformProperty ? 'transform' : name;
7543
+ if (!inlineStyles[styleName]) {
7544
+ inlineStyles[styleName] = elStyle[styleName];
7545
+ }
7546
+
7547
+ let parsedPropertyValue;
7548
+ if (isObj(propertyValue)) {
7549
+ const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue);
7550
+ const tweenOptionsEase = setValue(tweenOptions.ease, ease);
7551
+ const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase;
7552
+ const to = /** @type {WAAPITweenOptions} */(tweenOptions).to;
7553
+ const from = /** @type {WAAPITweenOptions} */(tweenOptions).from;
7554
+ /** @type {Number} */
7555
+ tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, targetsLength)) * timeScale;
7556
+ /** @type {Number} */
7557
+ tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, targetsLength) * timeScale;
7558
+ /** @type {CompositeOperation} */
7559
+ tweenParams.composite = /** @type {CompositeOperation} */(setValue(tweenOptions.composition, composite));
7560
+ /** @type {String} */
7561
+ tweenParams.easing = parseWAAPIEasing(tweenOptionsEase);
7562
+ parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, targetsLength);
7563
+ if (individualTransformProperty) {
7564
+ keyframes[`--${individualTransformProperty}`] = parsedPropertyValue;
7565
+ cachedTransforms[individualTransformProperty] = parsedPropertyValue;
7566
+ } else {
7567
+ keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, targetsLength);
7568
+ }
7569
+ addWAAPIAnimation(this, $el, name, keyframes, tweenParams);
7570
+ if (!isUnd(from)) {
7571
+ if (!individualTransformProperty) {
7572
+ elStyle[name] = keyframes[name][0];
7573
+ } else {
7574
+ const key = `--${individualTransformProperty}`;
7575
+ elStyle.setProperty(key, keyframes[key][0]);
7576
+ }
7577
+ }
7578
+ } else {
7579
+ parsedPropertyValue = isArr(propertyValue) ?
7580
+ propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, targetsLength)) :
7581
+ normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, targetsLength);
7582
+ if (individualTransformProperty) {
7583
+ keyframes[`--${individualTransformProperty}`] = parsedPropertyValue;
7584
+ cachedTransforms[individualTransformProperty] = parsedPropertyValue;
7585
+ } else {
7586
+ keyframes[name] = parsedPropertyValue;
7587
+ }
7588
+ addWAAPIAnimation(this, $el, name, keyframes, tweenParams);
7589
+ }
7590
+ }
7591
+ if (hasIndividualTransforms) {
7592
+ let transforms = emptyString;
7593
+ for (let t in cachedTransforms) {
7594
+ transforms += `${transformsFragmentStrings[t]}var(--${t})) `;
7595
+ }
7596
+ elStyle.transform = transforms;
7597
+ }
7598
+ });
7599
+
7600
+ if (scroll) {
7601
+ /** @type {ScrollObserver} */(this.autoplay).link(this);
7602
+ }
7489
7603
  }
7490
- };
7491
7604
 
7492
- /**
7493
- * Picks a random element from an array or a string
7494
- *
7495
- * @template T
7496
- * @param {String|Array<T>} items - The array or string to pick from
7497
- * @return {String|T} A random element from the array or character from the string
7498
- */
7499
- const randomPick = items => items[random(0, items.length - 1)];
7605
+ /**
7606
+ * @callback forEachCallback
7607
+ * @param {globalThis.Animation} animation
7608
+ */
7500
7609
 
7501
- /**
7502
- * Shuffles an array in-place using the Fisher-Yates algorithm
7503
- * Adapted from https://bost.ocks.org/mike/shuffle/
7504
- *
7505
- * @param {Array} items - The array to shuffle (will be modified in-place)
7506
- * @return {Array} The same array reference, now shuffled
7507
- */
7508
- const shuffle = items => {
7509
- let m = items.length, t, i;
7510
- while (m) { i = random(0, --m); t = items[m]; items[m] = items[i]; items[i] = t; }
7511
- return items;
7512
- };
7610
+ /**
7611
+ * @param {forEachCallback|String} callback
7612
+ * @return {this}
7613
+ */
7614
+ forEach(callback) {
7615
+ try {
7616
+ const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback;
7617
+ this.animations.forEach(cb);
7618
+ } catch {} return this;
7619
+ }
7513
7620
 
7621
+ get speed() {
7622
+ return this._speed;
7623
+ }
7514
7624
 
7625
+ set speed(speed) {
7626
+ this._speed = +speed;
7627
+ this.forEach(anim => anim.playbackRate = speed);
7628
+ }
7515
7629
 
7630
+ get currentTime() {
7631
+ const controlAnimation = this.controlAnimation;
7632
+ const timeScale = globals.timeScale;
7633
+ return this.completed ? this.duration : controlAnimation ? +controlAnimation.currentTime * (timeScale === 1 ? 1 : timeScale) : 0;
7634
+ }
7516
7635
 
7636
+ set currentTime(time) {
7637
+ const t = time * (globals.timeScale === 1 ? 1 : K);
7638
+ this.forEach(anim => {
7639
+ // Make sure the animation playState is not 'paused' in order to properly trigger an onfinish callback.
7640
+ // The "paused" play state supersedes the "finished" play state; if the animation is both paused and finished, the "paused" state is the one that will be reported.
7641
+ // https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish_event
7642
+ // This is not needed for persisting animations since they never finish.
7643
+ if (!this.persist && t >= this.duration) anim.play();
7644
+ anim.currentTime = t;
7645
+ });
7646
+ }
7517
7647
 
7518
- /**
7519
- * @overload
7520
- * @param {Number} val
7521
- * @param {StaggerParams} [params]
7522
- * @return {StaggerFunction<Number>}
7523
- */
7524
- /**
7525
- * @overload
7526
- * @param {String} val
7527
- * @param {StaggerParams} [params]
7528
- * @return {StaggerFunction<String>}
7529
- */
7530
- /**
7531
- * @overload
7532
- * @param {[Number, Number]} val
7533
- * @param {StaggerParams} [params]
7534
- * @return {StaggerFunction<Number>}
7535
- */
7536
- /**
7537
- * @overload
7538
- * @param {[String, String]} val
7539
- * @param {StaggerParams} [params]
7540
- * @return {StaggerFunction<String>}
7541
- */
7542
- /**
7543
- * @param {Number|String|[Number, Number]|[String, String]} val The staggered value or range
7544
- * @param {StaggerParams} [params] The stagger parameters
7545
- * @return {StaggerFunction<Number|String>}
7546
- */
7547
- const stagger = (val, params = {}) => {
7548
- let values = [];
7549
- let maxValue = 0;
7550
- const from = params.from;
7551
- const reversed = params.reversed;
7552
- const ease = params.ease;
7553
- const hasEasing = !isUnd(ease);
7554
- const hasSpring = hasEasing && !isUnd(/** @type {Spring} */(ease).ease);
7555
- const staggerEase = hasSpring ? /** @type {Spring} */(ease).ease : hasEasing ? parseEase(ease) : null;
7556
- const grid = params.grid;
7557
- const axis = params.axis;
7558
- const customTotal = params.total;
7559
- const fromFirst = isUnd(from) || from === 0 || from === 'first';
7560
- const fromCenter = from === 'center';
7561
- const fromLast = from === 'last';
7562
- const fromRandom = from === 'random';
7563
- const isRange = isArr(val);
7564
- const useProp = params.use;
7565
- const val1 = isRange ? parseNumber(val[0]) : parseNumber(val);
7566
- const val2 = isRange ? parseNumber(val[1]) : 0;
7567
- const unitMatch = unitsExecRgx.exec((isRange ? val[1] : val) + emptyString);
7568
- const start = params.start || 0 + (isRange ? val1 : 0);
7569
- let fromIndex = fromFirst ? 0 : isNum(from) ? from : 0;
7570
- return (target, i, t, tl) => {
7571
- const [ registeredTarget ] = registerTargets(target);
7572
- const total = isUnd(customTotal) ? t : customTotal;
7573
- const customIndex = !isUnd(useProp) ? isFnc(useProp) ? useProp(registeredTarget, i, total) : getOriginalAnimatableValue(registeredTarget, useProp) : false;
7574
- const staggerIndex = isNum(customIndex) || isStr(customIndex) && isNum(+customIndex) ? +customIndex : i;
7575
- if (fromCenter) fromIndex = (total - 1) / 2;
7576
- if (fromLast) fromIndex = total - 1;
7577
- if (!values.length) {
7578
- for (let index = 0; index < total; index++) {
7579
- if (!grid) {
7580
- values.push(abs(fromIndex - index));
7648
+ get progress() {
7649
+ return this.currentTime / this.duration;
7650
+ }
7651
+
7652
+ set progress(progress) {
7653
+ this.forEach(anim => anim.currentTime = progress * this.duration || 0);
7654
+ }
7655
+
7656
+ resume() {
7657
+ if (!this.paused) return this;
7658
+ this.paused = false;
7659
+ // TODO: Store the current time, and seek back to the last position
7660
+ return this.forEach('play');
7661
+ }
7662
+
7663
+ pause() {
7664
+ if (this.paused) return this;
7665
+ this.paused = true;
7666
+ return this.forEach('pause');
7667
+ }
7668
+
7669
+ alternate() {
7670
+ this.reversed = !this.reversed;
7671
+ this.forEach('reverse');
7672
+ if (this.paused) this.forEach('pause');
7673
+ return this;
7674
+ }
7675
+
7676
+ play() {
7677
+ if (this.reversed) this.alternate();
7678
+ return this.resume();
7679
+ }
7680
+
7681
+ reverse() {
7682
+ if (!this.reversed) this.alternate();
7683
+ return this.resume();
7684
+ }
7685
+
7686
+ /**
7687
+ * @param {Number} time
7688
+ * @param {Boolean} muteCallbacks
7689
+ */
7690
+ seek(time, muteCallbacks = false) {
7691
+ if (muteCallbacks) this.muteCallbacks = true;
7692
+ if (time < this.duration) this.completed = false;
7693
+ this.currentTime = time;
7694
+ this.muteCallbacks = false;
7695
+ if (this.paused) this.pause();
7696
+ return this;
7697
+ }
7698
+
7699
+ restart() {
7700
+ this.completed = false;
7701
+ return this.seek(0, true).resume();
7702
+ }
7703
+
7704
+ commitStyles() {
7705
+ return this.forEach('commitStyles');
7706
+ }
7707
+
7708
+ complete() {
7709
+ return this.seek(this.duration);
7710
+ }
7711
+
7712
+ cancel() {
7713
+ this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise
7714
+ this.commitStyles().forEach('cancel');
7715
+ this.animations.length = 0; // Needed to release all animations from memory
7716
+ requestAnimationFrame(() => {
7717
+ this.targets.forEach(($el) => { // Needed to avoid unecessary inline transorms
7718
+ if ($el.style.transform === 'none') $el.style.removeProperty('transform');
7719
+ });
7720
+ });
7721
+ return this;
7722
+ }
7723
+
7724
+ revert() {
7725
+ // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted,
7726
+ // This means if you have multiple animations animating different transforms on the same target,
7727
+ // reverting one of them will also override the transform property of the other animations.
7728
+ // A better approach would be to store the original custom property values if they exist instead of the entire transform value,
7729
+ // and update the CSS variables with the orignal value
7730
+ this.cancel().targets.forEach(($el, i) => {
7731
+ const targetStyle = $el.style;
7732
+ const targetInlineStyles = this._inlineStyles[i];
7733
+ for (let name in targetInlineStyles) {
7734
+ const originalInlinedValue = targetInlineStyles[name];
7735
+ if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) {
7736
+ targetStyle.removeProperty(toLowerCase(name));
7581
7737
  } else {
7582
- const fromX = !fromCenter ? fromIndex % grid[0] : (grid[0] - 1) / 2;
7583
- const fromY = !fromCenter ? floor(fromIndex / grid[0]) : (grid[1] - 1) / 2;
7584
- const toX = index % grid[0];
7585
- const toY = floor(index / grid[0]);
7586
- const distanceX = fromX - toX;
7587
- const distanceY = fromY - toY;
7588
- let value = sqrt(distanceX * distanceX + distanceY * distanceY);
7589
- if (axis === 'x') value = -distanceX;
7590
- if (axis === 'y') value = -distanceY;
7591
- values.push(value);
7738
+ $el.style[name] = originalInlinedValue;
7592
7739
  }
7593
- maxValue = max(...values);
7594
7740
  }
7595
- if (staggerEase) values = values.map(val => staggerEase(val / maxValue) * maxValue);
7596
- if (reversed) values = values.map(val => axis ? (val < 0) ? val * -1 : -val : abs(maxValue - val));
7597
- if (fromRandom) values = shuffle(values);
7598
- }
7599
- const spacing = isRange ? (val2 - val1) / maxValue : val1;
7600
- const offset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start);
7601
- /** @type {String|Number} */
7602
- let output = offset + ((spacing * round$1(values[staggerIndex], 2)) || 0);
7603
- if (params.modifier) output = params.modifier(output);
7604
- if (unitMatch) output = `${output}${unitMatch[2]}`;
7605
- return output;
7741
+ // Remove style attribute if empty
7742
+ if ($el.getAttribute('style') === emptyString) $el.removeAttribute('style');
7743
+ });
7744
+ return this;
7606
7745
  }
7607
- };
7608
-
7609
- var index$2 = /*#__PURE__*/Object.freeze({
7610
- __proto__: null,
7611
- $: registerTargets,
7612
- clamp: clamp,
7613
- cleanInlineStyles: cleanInlineStyles,
7614
- createSeededRandom: createSeededRandom,
7615
- damp: damp,
7616
- degToRad: degToRad,
7617
- get: get,
7618
- keepTime: keepTime,
7619
- lerp: lerp,
7620
- mapRange: mapRange,
7621
- padEnd: padEnd,
7622
- padStart: padStart,
7623
- radToDeg: radToDeg,
7624
- random: random,
7625
- randomPick: randomPick,
7626
- remove: remove,
7627
- round: round,
7628
- roundPad: roundPad,
7629
- set: set,
7630
- shuffle: shuffle,
7631
- snap: snap,
7632
- stagger: stagger,
7633
- sync: sync,
7634
- wrap: wrap
7635
- });
7636
7746
 
7747
+ /**
7748
+ * @typedef {this & {then: null}} ResolvedWAAPIAnimation
7749
+ */
7637
7750
 
7751
+ /**
7752
+ * @param {Callback<ResolvedWAAPIAnimation>} [callback]
7753
+ * @return Promise<this>
7754
+ */
7755
+ then(callback = noop) {
7756
+ const then = this.then;
7757
+ const onResolve = () => {
7758
+ this.then = null;
7759
+ callback(/** @type {ResolvedWAAPIAnimation} */(this));
7760
+ this.then = then;
7761
+ this._resolve = noop;
7762
+ };
7763
+ return new Promise(r => {
7764
+ this._resolve = () => r(onResolve());
7765
+ if (this.completed) this._resolve();
7766
+ return this;
7767
+ });
7768
+ }
7769
+ }
7638
7770
 
7771
+ const waapi = {
7639
7772
  /**
7640
- * @param {TargetsParam} path
7641
- * @return {SVGGeometryElement|void}
7773
+ * @param {DOMTargetsParam} targets
7774
+ * @param {WAAPIAnimationParams} params
7775
+ * @return {WAAPIAnimation}
7642
7776
  */
7643
- const getPath = path => {
7644
- const parsedTargets = parseTargets(path);
7645
- const $parsedSvg = /** @type {SVGGeometryElement} */(parsedTargets[0]);
7646
- if (!$parsedSvg || !isSvg($parsedSvg)) return console.warn(`${path} is not a valid SVGGeometryElement`);
7647
- return $parsedSvg;
7777
+ animate: (targets, params) => new WAAPIAnimation(targets, params),
7778
+ convertEase: easingToLinear
7648
7779
  };
7649
7780
 
7650
7781
 
7651
7782
 
7652
- // Motion path animation
7783
+
7784
+
7785
+
7786
+
7787
+
7653
7788
 
7654
7789
  /**
7655
- * @param {SVGGeometryElement} $path
7656
- * @param {Number} totalLength
7657
- * @param {Number} progress
7658
- * @param {Number}lookup
7659
- * @return {DOMPoint}
7790
+ * @typedef {DOMTargetSelector|Array<DOMTargetSelector>} LayoutChildrenParam
7660
7791
  */
7661
- const getPathPoint = ($path, totalLength, progress, lookup = 0) => {
7662
- const point = progress + lookup;
7663
- const pointOnPath = (point % totalLength + totalLength) % totalLength;
7664
- return $path.getPointAtLength(pointOnPath);
7665
- };
7666
7792
 
7667
7793
  /**
7668
- * @param {SVGGeometryElement} $path
7669
- * @param {String} pathProperty
7670
- * @param {Number} [offset=0]
7671
- * @return {FunctionValue}
7794
+ * @typedef {Record<String, Number|String>} LayoutStateParams
7672
7795
  */
7673
- const getPathProgess = ($path, pathProperty, offset = 0) => {
7674
- return $el => {
7675
- const totalLength = +($path.getTotalLength());
7676
- const inSvg = $el[isSvgSymbol];
7677
- const ctm = $path.getCTM();
7678
- /** @type {TweenObjectValue} */
7679
- return {
7680
- from: 0,
7681
- to: totalLength,
7682
- /** @type {TweenModifier} */
7683
- modifier: progress => {
7684
- const offsetLength = offset * totalLength;
7685
- const newProgress = progress + offsetLength;
7686
- if (pathProperty === 'a') {
7687
- const p0 = getPathPoint($path, totalLength, newProgress, -1);
7688
- const p1 = getPathPoint($path, totalLength, newProgress, 1);
7689
- return atan2(p1.y - p0.y, p1.x - p0.x) * 180 / PI;
7690
- } else {
7691
- const p = getPathPoint($path, totalLength, newProgress, 0);
7692
- return pathProperty === 'x' ?
7693
- inSvg || !ctm ? p.x : p.x * ctm.a + p.y * ctm.c + ctm.e :
7694
- inSvg || !ctm ? p.y : p.x * ctm.b + p.y * ctm.d + ctm.f
7695
- }
7696
- }
7697
- }
7698
- }
7699
- };
7700
7796
 
7701
7797
  /**
7702
- * @param {TargetsParam} path
7703
- * @param {Number} [offset=0]
7798
+ * @typedef {Object} LayoutAnimationParams
7799
+ * @property {Number} [duration]
7800
+ * @property {Number|FunctionValue} [delay]
7801
+ * @property {EasingParam} [ease]
7802
+ * @property {LayoutStateParams} [frozen]
7803
+ * @property {LayoutStateParams} [added]
7804
+ * @property {LayoutStateParams} [removed]
7805
+ * @property {Callback<AutoLayout>} [onComplete]
7704
7806
  */
7705
- const createMotionPath = (path, offset = 0) => {
7706
- const $path = getPath(path);
7707
- if (!$path) return;
7708
- return {
7709
- translateX: getPathProgess($path, 'x', offset),
7710
- translateY: getPathProgess($path, 'y', offset),
7711
- rotate: getPathProgess($path, 'a', offset),
7712
- }
7713
- };
7714
-
7715
-
7716
7807
 
7717
7808
  /**
7718
- * @param {SVGGeometryElement} [$el]
7719
- * @return {Number}
7809
+ * @typedef {LayoutAnimationParams & {
7810
+ * children?: LayoutChildrenParam,
7811
+ * properties?: Array<String>,
7812
+ * }} AutoLayoutParams
7720
7813
  */
7721
- const getScaleFactor = $el => {
7722
- let scaleFactor = 1;
7723
- if ($el && $el.getCTM) {
7724
- const ctm = $el.getCTM();
7725
- if (ctm) {
7726
- const scaleX = sqrt(ctm.a * ctm.a + ctm.b * ctm.b);
7727
- const scaleY = sqrt(ctm.c * ctm.c + ctm.d * ctm.d);
7728
- scaleFactor = (scaleX + scaleY) / 2;
7729
- }
7730
- }
7731
- return scaleFactor;
7732
- };
7733
7814
 
7734
7815
  /**
7735
- * Creates a proxy that wraps an SVGGeometryElement and adds drawing functionality.
7736
- * @param {SVGGeometryElement} $el - The SVG element to transform into a drawable
7737
- * @param {number} start - Starting position (0-1)
7738
- * @param {number} end - Ending position (0-1)
7739
- * @return {DrawableSVGGeometry} - Returns a proxy that preserves the original element's type with additional 'draw' attribute functionality
7816
+ * @typedef {Record<String, Number|String> & {
7817
+ * transform: String,
7818
+ * x: Number,
7819
+ * y: Number,
7820
+ * left: Number,
7821
+ * top: Number,
7822
+ * clientLeft: Number,
7823
+ * clientTop: Number,
7824
+ * width: Number,
7825
+ * height: Number,
7826
+ * }} LayoutNodeProperties
7740
7827
  */
7741
- const createDrawableProxy = ($el, start, end) => {
7742
- const pathLength = K;
7743
- const computedStyles = getComputedStyle($el);
7744
- const strokeLineCap = computedStyles.strokeLinecap;
7745
- // @ts-ignore
7746
- const $scalled = computedStyles.vectorEffect === 'non-scaling-stroke' ? $el : null;
7747
- let currentCap = strokeLineCap;
7748
-
7749
- const proxy = new Proxy($el, {
7750
- get(target, property) {
7751
- const value = target[property];
7752
- if (property === proxyTargetSymbol) return target;
7753
- if (property === 'setAttribute') {
7754
- return (...args) => {
7755
- if (args[0] === 'draw') {
7756
- const value = args[1];
7757
- const values = value.split(' ');
7758
- const v1 = +values[0];
7759
- const v2 = +values[1];
7760
- // TOTO: Benchmark if performing two slices is more performant than one split
7761
- // const spaceIndex = value.indexOf(' ');
7762
- // const v1 = round(+value.slice(0, spaceIndex), precision);
7763
- // const v2 = round(+value.slice(spaceIndex + 1), precision);
7764
- const scaleFactor = getScaleFactor($scalled);
7765
- const os = v1 * -pathLength * scaleFactor;
7766
- const d1 = (v2 * pathLength * scaleFactor) + os;
7767
- const d2 = (pathLength * scaleFactor +
7768
- ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10 * scaleFactor) - d1);
7769
- if (strokeLineCap !== 'butt') {
7770
- const newCap = v1 === v2 ? 'butt' : strokeLineCap;
7771
- if (currentCap !== newCap) {
7772
- target.style.strokeLinecap = `${newCap}`;
7773
- currentCap = newCap;
7774
- }
7775
- }
7776
- target.setAttribute('stroke-dashoffset', `${os}`);
7777
- target.setAttribute('stroke-dasharray', `${d1} ${d2}`);
7778
- }
7779
- return Reflect.apply(value, target, args);
7780
- };
7781
- }
7782
7828
 
7783
- if (isFnc(value)) {
7784
- return (...args) => Reflect.apply(value, target, args);
7785
- } else {
7786
- return value;
7787
- }
7788
- }
7789
- });
7829
+ /**
7830
+ * @typedef {Object} LayoutNode
7831
+ * @property {String} id
7832
+ * @property {DOMTarget} $el
7833
+ * @property {Number} index
7834
+ * @property {Number} total
7835
+ * @property {Number} delay
7836
+ * @property {Number} duration
7837
+ * @property {DOMTarget} $measure
7838
+ * @property {LayoutSnapshot} state
7839
+ * @property {AutoLayout} layout
7840
+ * @property {LayoutNode|null} parentNode
7841
+ * @property {Boolean} isTarget
7842
+ * @property {Boolean} hasTransform
7843
+ * @property {Boolean} isAnimated
7844
+ * @property {Array<String>} inlineStyles
7845
+ * @property {String|null} inlineTransforms
7846
+ * @property {String|null} inlineTransition
7847
+ * @property {Boolean} branchAdded
7848
+ * @property {Boolean} branchRemoved
7849
+ * @property {Boolean} branchNotRendered
7850
+ * @property {Boolean} sizeChanged
7851
+ * @property {Boolean} isInlined
7852
+ * @property {Boolean} hasVisibilitySwap
7853
+ * @property {Boolean} hasDisplayNone
7854
+ * @property {Boolean} hasVisibilityHidden
7855
+ * @property {String|null} measuredInlineTransform
7856
+ * @property {String|null} measuredInlineTransition
7857
+ * @property {String|null} measuredDisplay
7858
+ * @property {String|null} measuredVisibility
7859
+ * @property {String|null} measuredPosition
7860
+ * @property {Boolean} measuredHasDisplayNone
7861
+ * @property {Boolean} measuredHasVisibilityHidden
7862
+ * @property {Boolean} measuredIsVisible
7863
+ * @property {Boolean} measuredIsRemoved
7864
+ * @property {Boolean} measuredIsInsideRoot
7865
+ * @property {LayoutNodeProperties} properties
7866
+ * @property {LayoutNode|null} _head
7867
+ * @property {LayoutNode|null} _tail
7868
+ * @property {LayoutNode|null} _prev
7869
+ * @property {LayoutNode|null} _next
7870
+ */
7871
+
7872
+ /**
7873
+ * @callback LayoutNodeIterator
7874
+ * @param {LayoutNode} node
7875
+ * @param {Number} index
7876
+ * @return {void}
7877
+ */
7790
7878
 
7791
- if ($el.getAttribute('pathLength') !== `${pathLength}`) {
7792
- $el.setAttribute('pathLength', `${pathLength}`);
7793
- proxy.setAttribute('draw', `${start} ${end}`);
7794
- }
7879
+ let layoutId = 0;
7880
+ let nodeId = 0;
7795
7881
 
7796
- return /** @type {DrawableSVGGeometry} */(proxy);
7882
+ /**
7883
+ * @param {DOMTarget} root
7884
+ * @param {DOMTarget} $el
7885
+ * @return {Boolean}
7886
+ */
7887
+ const isElementInRoot = (root, $el) => {
7888
+ if (!root || !$el) return false;
7889
+ return root === $el || root.contains($el);
7797
7890
  };
7798
7891
 
7799
7892
  /**
7800
- * Creates drawable proxies for multiple SVG elements.
7801
- * @param {TargetsParam} selector - CSS selector, SVG element, or array of elements and selectors
7802
- * @param {number} [start=0] - Starting position (0-1)
7803
- * @param {number} [end=0] - Ending position (0-1)
7804
- * @return {Array<DrawableSVGGeometry>} - Array of proxied elements with drawing functionality
7893
+ * @param {Node} node
7894
+ * @param {'previousSibling'|'nextSibling'} direction
7895
+ * @return {Boolean}
7805
7896
  */
7806
- const createDrawable = (selector, start = 0, end = 0) => {
7807
- const els = parseTargets(selector);
7808
- return els.map($el => createDrawableProxy(
7809
- /** @type {SVGGeometryElement} */($el),
7810
- start,
7811
- end
7812
- ));
7897
+ const hasTextSibling = (node, direction) => {
7898
+ let sibling = node[direction];
7899
+ while (sibling && sibling.nodeType === Node.TEXT_NODE && !sibling.textContent.trim()) {
7900
+ sibling = sibling[direction];
7901
+ }
7902
+ return sibling && sibling.nodeType === Node.TEXT_NODE;
7813
7903
  };
7814
7904
 
7815
-
7816
-
7817
7905
  /**
7818
- * @param {TargetsParam} path2
7819
- * @param {Number} [precision]
7820
- * @return {FunctionValue}
7906
+ * @param {DOMTarget} $el
7907
+ * @return {Boolean}
7821
7908
  */
7822
- const morphTo = (path2, precision = .33) => ($path1) => {
7823
- const tagName1 = ($path1.tagName || '').toLowerCase();
7824
- if (!tagName1.match(/^(path|polygon|polyline)$/)) {
7825
- throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use <path>, <polygon> or <polyline>.`);
7826
- }
7827
- const $path2 = /** @type {SVGGeometryElement} */(getPath(path2));
7828
- if (!$path2) {
7829
- throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing <path>, <polygon> or <polyline> SVG element.");
7830
- }
7831
- const tagName2 = ($path2.tagName || '').toLowerCase();
7832
- if (!tagName2.match(/^(path|polygon|polyline)$/)) {
7833
- throw new Error(`Can't morph a <${$path2.tagName}> SVG element. Use <path>, <polygon> or <polyline>.`);
7834
- }
7835
- const isPath = $path1.tagName === 'path';
7836
- const separator = isPath ? ' ' : ',';
7837
- const previousPoints = $path1[morphPointsSymbol];
7838
- if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints);
7909
+ const isElementSurroundedByText = $el => hasTextSibling($el, 'previousSibling') || hasTextSibling($el, 'nextSibling');
7839
7910
 
7840
- let v1 = '', v2 = '';
7911
+ /**
7912
+ * @param {DOMTarget|null} $el
7913
+ * @return {String|null}
7914
+ */
7915
+ const muteElementTransition = $el => {
7916
+ if (!$el) return null;
7917
+ const style = $el.style;
7918
+ const transition = style.transition || '';
7919
+ style.setProperty('transition', 'none', 'important');
7920
+ return transition;
7921
+ };
7841
7922
 
7842
- if (!precision) {
7843
- v1 = $path1.getAttribute(isPath ? 'd' : 'points');
7844
- v2 = $path2.getAttribute(isPath ? 'd' : 'points');
7923
+ /**
7924
+ * @param {DOMTarget|null} $el
7925
+ * @param {String|null} transition
7926
+ */
7927
+ const restoreElementTransition = ($el, transition) => {
7928
+ if (!$el) return;
7929
+ const style = $el.style;
7930
+ if (transition) {
7931
+ style.transition = transition;
7845
7932
  } else {
7846
- const length1 = /** @type {SVGGeometryElement} */($path1).getTotalLength();
7847
- const length2 = $path2.getTotalLength();
7848
- const maxPoints = Math.max(Math.ceil(length1 * precision), Math.ceil(length2 * precision));
7849
- for (let i = 0; i < maxPoints; i++) {
7850
- const t = i / (maxPoints - 1);
7851
- const pointOnPath1 = /** @type {SVGGeometryElement} */($path1).getPointAtLength(length1 * t);
7852
- const pointOnPath2 = $path2.getPointAtLength(length2 * t);
7853
- const prefix = isPath ? (i === 0 ? 'M' : 'L') : '';
7854
- v1 += prefix + round$1(pointOnPath1.x, 3) + separator + pointOnPath1.y + ' ';
7855
- v2 += prefix + round$1(pointOnPath2.x, 3) + separator + pointOnPath2.y + ' ';
7856
- }
7933
+ style.removeProperty('transition');
7857
7934
  }
7935
+ };
7858
7936
 
7859
- $path1[morphPointsSymbol] = v2;
7937
+ /**
7938
+ * @param {LayoutNode} node
7939
+ */
7940
+ const muteNodeTransition = node => {
7941
+ const store = node.layout.transitionMuteStore;
7942
+ const $el = node.$el;
7943
+ const $measure = node.$measure;
7944
+ if ($el && !store.has($el)) store.set($el, muteElementTransition($el));
7945
+ if ($measure && !store.has($measure)) store.set($measure, muteElementTransition($measure));
7946
+ };
7860
7947
 
7861
- return [v1, v2];
7948
+ /**
7949
+ * @param {Map<DOMTarget, String|null>} store
7950
+ */
7951
+ const restoreLayoutTransition = store => {
7952
+ store.forEach((value, $el) => restoreElementTransition($el, value));
7953
+ store.clear();
7862
7954
  };
7863
7955
 
7864
- var index$1 = /*#__PURE__*/Object.freeze({
7865
- __proto__: null,
7866
- createDrawable: createDrawable,
7867
- createMotionPath: createMotionPath,
7868
- morphTo: morphTo
7956
+ const hiddenComputedStyle = /** @type {CSSStyleDeclaration} */({
7957
+ display: 'none',
7958
+ visibility: 'hidden',
7959
+ opacity: '0',
7960
+ transform: 'none',
7961
+ position: 'static',
7869
7962
  });
7870
7963
 
7871
-
7872
-
7873
- const segmenter = (typeof Intl !== 'undefined') && Intl.Segmenter;
7874
- const valueRgx = /\{value\}/g;
7875
- const indexRgx = /\{i\}/g;
7876
- const whiteSpaceGroupRgx = /(\s+)/;
7877
- const whiteSpaceRgx = /^\s+$/;
7878
- const lineType = 'line';
7879
- const wordType = 'word';
7880
- const charType = 'char';
7881
- const dataLine = `data-line`;
7882
-
7883
7964
  /**
7884
- * @typedef {Object} Segment
7885
- * @property {String} segment
7886
- * @property {Boolean} [isWordLike]
7965
+ * @param {LayoutNode|null} node
7887
7966
  */
7967
+ const detachNode = node => {
7968
+ if (!node) return;
7969
+ const parent = node.parentNode;
7970
+ if (!parent) return;
7971
+ if (parent._head === node) parent._head = node._next;
7972
+ if (parent._tail === node) parent._tail = node._prev;
7973
+ if (node._prev) node._prev._next = node._next;
7974
+ if (node._next) node._next._prev = node._prev;
7975
+ node._prev = null;
7976
+ node._next = null;
7977
+ node.parentNode = null;
7978
+ };
7888
7979
 
7889
7980
  /**
7890
- * @typedef {Object} Segmenter
7891
- * @property {function(String): Iterable<Segment>} segment
7892
- */
7981
+ * @param {DOMTarget} $el
7982
+ * @param {LayoutNode|null} parentNode
7983
+ * @param {LayoutSnapshot} state
7984
+ * @param {LayoutNode} [recycledNode]
7985
+ * @return {LayoutNode}
7986
+ */
7987
+ const createNode = ($el, parentNode, state, recycledNode) => {
7988
+ let dataId = $el.dataset.layoutId;
7989
+ if (!dataId) dataId = $el.dataset.layoutId = `node-${nodeId++}`;
7990
+ const node = recycledNode ? recycledNode : /** @type {LayoutNode} */({});
7991
+ node.$el = $el;
7992
+ node.$measure = $el;
7993
+ node.id = dataId;
7994
+ node.index = 0;
7995
+ node.total = 1;
7996
+ node.delay = 0;
7997
+ node.duration = 0;
7998
+ node.state = state;
7999
+ node.layout = state.layout;
8000
+ node.parentNode = parentNode || null;
8001
+ node.isTarget = false;
8002
+ node.hasTransform = false;
8003
+ node.isAnimated = false;
8004
+ node.inlineStyles = [];
8005
+ node.inlineTransforms = null;
8006
+ node.inlineTransition = null;
8007
+ node.branchAdded = false;
8008
+ node.branchRemoved = false;
8009
+ node.branchNotRendered = false;
8010
+ node.sizeChanged = false;
8011
+ node.isInlined = false;
8012
+ node.hasVisibilitySwap = false;
8013
+ node.hasDisplayNone = false;
8014
+ node.hasVisibilityHidden = false;
8015
+ node.measuredInlineTransform = null;
8016
+ node.measuredInlineTransition = null;
8017
+ node.measuredDisplay = null;
8018
+ node.measuredVisibility = null;
8019
+ node.measuredPosition = null;
8020
+ node.measuredHasDisplayNone = false;
8021
+ node.measuredHasVisibilityHidden = false;
8022
+ node.measuredIsVisible = false;
8023
+ node.measuredIsRemoved = false;
8024
+ node.measuredIsInsideRoot = false;
8025
+ node.properties = /** @type {LayoutNodeProperties} */({
8026
+ transform: 'none',
8027
+ x: 0,
8028
+ y: 0,
8029
+ left: 0,
8030
+ top: 0,
8031
+ clientLeft: 0,
8032
+ clientTop: 0,
8033
+ width: 0,
8034
+ height: 0,
8035
+ });
8036
+ node.layout.properties.forEach(prop => node.properties[prop] = 0);
8037
+ node._head = null;
8038
+ node._tail = null;
8039
+ node._prev = null;
8040
+ node._next = null;
8041
+ return node;
8042
+ };
7893
8043
 
7894
- /** @type {Segmenter} */
7895
- let wordSegmenter = null;
7896
- /** @type {Segmenter} */
7897
- let graphemeSegmenter = null;
7898
- let $splitTemplate = null;
8044
+ /**
8045
+ * @param {LayoutNode} node
8046
+ * @param {DOMTarget} $measure
8047
+ * @param {CSSStyleDeclaration} computedStyle
8048
+ * @param {Boolean} skipMeasurements
8049
+ * @return {LayoutNode}
8050
+ */
8051
+ const recordNodeState = (node, $measure, computedStyle, skipMeasurements) => {
8052
+ const $el = node.$el;
8053
+ const root = node.layout.root;
8054
+ const isRoot = root === $el;
8055
+ const properties = node.properties;
8056
+ const rootNode = node.state.rootNode;
8057
+ const parentNode = node.parentNode;
8058
+ const computedTransforms = computedStyle.transform;
8059
+ const inlineTransforms = $el.style.transform;
8060
+ const parentNotRendered = parentNode ? parentNode.measuredIsRemoved : false;
8061
+ const position = computedStyle.position;
8062
+ if (isRoot) node.layout.absoluteCoords = position === 'fixed' || position === 'absolute';
8063
+ node.$measure = $measure;
8064
+ node.inlineTransforms = inlineTransforms;
8065
+ node.hasTransform = computedTransforms && computedTransforms !== 'none';
8066
+ node.measuredIsInsideRoot = isElementInRoot(root, $measure);
8067
+ node.measuredInlineTransform = null;
8068
+ node.measuredDisplay = computedStyle.display;
8069
+ node.measuredVisibility = computedStyle.visibility;
8070
+ node.measuredPosition = position;
8071
+ node.measuredHasDisplayNone = computedStyle.display === 'none';
8072
+ node.measuredHasVisibilityHidden = computedStyle.visibility === 'hidden';
8073
+ node.measuredIsVisible = !(node.measuredHasDisplayNone || node.measuredHasVisibilityHidden);
8074
+ node.measuredIsRemoved = node.measuredHasDisplayNone || node.measuredHasVisibilityHidden || parentNotRendered;
8075
+ node.isInlined = node.measuredDisplay.includes('inline') && isElementSurroundedByText($el);
8076
+
8077
+ // Mute transforms (and transition to avoid triggering an animation) before the position calculation
8078
+ if (node.hasTransform && !skipMeasurements) {
8079
+ const transitionMuteStore = node.layout.transitionMuteStore;
8080
+ if (!transitionMuteStore.get($el)) node.inlineTransition = muteElementTransition($el);
8081
+ if ($measure === $el) {
8082
+ $el.style.transform = 'none';
8083
+ } else {
8084
+ if (!transitionMuteStore.get($measure)) node.measuredInlineTransition = muteElementTransition($measure);
8085
+ node.measuredInlineTransform = $measure.style.transform;
8086
+ $measure.style.transform = 'none';
8087
+ }
8088
+ }
8089
+
8090
+ let left = 0;
8091
+ let top = 0;
8092
+ let width = 0;
8093
+ let height = 0;
8094
+
8095
+ if (!skipMeasurements) {
8096
+ const rect = $measure.getBoundingClientRect();
8097
+ left = rect.left;
8098
+ top = rect.top;
8099
+ width = rect.width;
8100
+ height = rect.height;
8101
+ }
8102
+
8103
+ for (let name in properties) {
8104
+ const computedProp = name === 'transform' ? computedTransforms : computedStyle[name] || (computedStyle.getPropertyValue && computedStyle.getPropertyValue(name));
8105
+ if (!isUnd(computedProp)) properties[name] = computedProp;
8106
+ }
8107
+
8108
+ properties.left = left;
8109
+ properties.top = top;
8110
+ properties.clientLeft = skipMeasurements ? 0 : $measure.clientLeft;
8111
+ properties.clientTop = skipMeasurements ? 0 : $measure.clientTop;
8112
+ // Compute local x/y relative to parent
8113
+ let absoluteLeft, absoluteTop;
8114
+ if (isRoot) {
8115
+ if (!node.layout.absoluteCoords) {
8116
+ absoluteLeft = 0;
8117
+ absoluteTop = 0;
8118
+ } else {
8119
+ absoluteLeft = left;
8120
+ absoluteTop = top;
8121
+ }
8122
+ } else {
8123
+ const p = parentNode || rootNode;
8124
+ const parentLeft = p.properties.left;
8125
+ const parentTop = p.properties.top;
8126
+ const borderLeft = p.properties.clientLeft;
8127
+ const borderTop = p.properties.clientTop;
8128
+ if (!node.layout.absoluteCoords) {
8129
+ if (p === rootNode) {
8130
+ const rootLeft = rootNode.properties.left;
8131
+ const rootTop = rootNode.properties.top;
8132
+ const rootBorderLeft = rootNode.properties.clientLeft;
8133
+ const rootBorderTop = rootNode.properties.clientTop;
8134
+ absoluteLeft = left - rootLeft - rootBorderLeft;
8135
+ absoluteTop = top - rootTop - rootBorderTop;
8136
+ } else {
8137
+ absoluteLeft = left - parentLeft - borderLeft;
8138
+ absoluteTop = top - parentTop - borderTop;
8139
+ }
8140
+ } else {
8141
+ absoluteLeft = left - parentLeft - borderLeft;
8142
+ absoluteTop = top - parentTop - borderTop;
8143
+ }
8144
+ }
8145
+ properties.x = absoluteLeft;
8146
+ properties.y = absoluteTop;
8147
+ properties.width = width;
8148
+ properties.height = height;
8149
+ return node;
8150
+ };
7899
8151
 
7900
8152
  /**
7901
- * @param {Segment} seg
7902
- * @return {Boolean}
8153
+ * @param {LayoutNode} node
8154
+ * @param {LayoutStateParams} [props]
7903
8155
  */
7904
- const isSegmentWordLike = seg => {
7905
- return seg.isWordLike ||
7906
- seg.segment === ' ' || // Consider spaces as words first, then handle them diffrently later
7907
- isNum(+seg.segment); // Safari doesn't considers numbers as words
8156
+ const updateNodeProperties = (node, props) => {
8157
+ if (!props) return;
8158
+ for (let name in props) {
8159
+ node.properties[name] = props[name];
8160
+ }
7908
8161
  };
7909
8162
 
7910
8163
  /**
7911
- * @param {HTMLElement} $el
8164
+ * @param {LayoutNode} node
7912
8165
  */
7913
- const setAriaHidden = $el => $el.setAttribute('aria-hidden', 'true');
8166
+ const recordNodeInlineStyles = node => {
8167
+ const style = node.$el.style;
8168
+ const stylesStore = node.inlineStyles;
8169
+ stylesStore.length = 0;
8170
+ node.layout.recordedProperties.forEach(prop => {
8171
+ stylesStore.push(prop, style[prop] || '');
8172
+ });
8173
+ };
7914
8174
 
7915
8175
  /**
7916
- * @param {DOMTarget} $el
7917
- * @param {String} type
7918
- * @return {Array<HTMLElement>}
8176
+ * @param {LayoutNode} node
7919
8177
  */
7920
- const getAllTopLevelElements = ($el, type) => [.../** @type {*} */($el.querySelectorAll(`[data-${type}]:not([data-${type}] [data-${type}])`))];
7921
-
7922
- const debugColors = { line: '#00D672', word: '#FF4B4B', char: '#5A87FF' };
8178
+ const restoreNodeInlineStyles = node => {
8179
+ const style = node.$el.style;
8180
+ const stylesStore = node.inlineStyles;
8181
+ for (let i = 0, l = stylesStore.length; i < l; i += 2) {
8182
+ const property = stylesStore[i];
8183
+ const styleValue = stylesStore[i + 1];
8184
+ if (styleValue && styleValue !== '') {
8185
+ style[property] = styleValue;
8186
+ } else {
8187
+ style[property] = '';
8188
+ style.removeProperty(property);
8189
+ }
8190
+ }
8191
+ };
7923
8192
 
7924
8193
  /**
7925
- * @param {HTMLElement} $el
8194
+ * @param {LayoutNode} node
7926
8195
  */
7927
- const filterEmptyElements = $el => {
7928
- if (!$el.childElementCount && !$el.textContent.trim()) {
7929
- const $parent = $el.parentElement;
7930
- $el.remove();
7931
- if ($parent) filterEmptyElements($parent);
8196
+ const restoreNodeTransform = node => {
8197
+ const inlineTransforms = node.inlineTransforms;
8198
+ const nodeStyle = node.$el.style;
8199
+ if (!node.hasTransform || !inlineTransforms || (node.hasTransform && nodeStyle.transform === 'none') || (inlineTransforms && inlineTransforms === 'none')) {
8200
+ nodeStyle.removeProperty('transform');
8201
+ } else if (inlineTransforms) {
8202
+ nodeStyle.transform = inlineTransforms;
8203
+ }
8204
+ const $measure = node.$measure;
8205
+ if (node.hasTransform && $measure !== node.$el) {
8206
+ const measuredStyle = $measure.style;
8207
+ const measuredInline = node.measuredInlineTransform;
8208
+ if (measuredInline && measuredInline !== '') {
8209
+ measuredStyle.transform = measuredInline;
8210
+ } else {
8211
+ measuredStyle.removeProperty('transform');
8212
+ }
8213
+ }
8214
+ node.measuredInlineTransform = null;
8215
+ if (node.inlineTransition !== null) {
8216
+ restoreElementTransition(node.$el, node.inlineTransition);
8217
+ node.inlineTransition = null;
8218
+ }
8219
+ if ($measure !== node.$el && node.measuredInlineTransition !== null) {
8220
+ restoreElementTransition($measure, node.measuredInlineTransition);
8221
+ node.measuredInlineTransition = null;
7932
8222
  }
7933
8223
  };
7934
8224
 
7935
8225
  /**
7936
- * @param {HTMLElement} $el
7937
- * @param {Number} lineIndex
7938
- * @param {Set<HTMLElement>} bin
7939
- * @returns {Set<HTMLElement>}
8226
+ * @param {LayoutNode} node
7940
8227
  */
7941
- const filterLineElements = ($el, lineIndex, bin) => {
7942
- const dataLineAttr = $el.getAttribute(dataLine);
7943
- if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') bin.add($el);
7944
- let i = $el.childElementCount;
7945
- while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin);
7946
- return bin;
8228
+ const restoreNodeVisualState = node => {
8229
+ if (node.measuredIsRemoved || node.hasVisibilitySwap) {
8230
+ node.$el.style.removeProperty('display');
8231
+ node.$el.style.removeProperty('visibility');
8232
+ if (node.hasVisibilitySwap) {
8233
+ node.$measure.style.removeProperty('display');
8234
+ node.$measure.style.removeProperty('visibility');
8235
+ }
8236
+ }
8237
+ if (node.measuredIsRemoved) {
8238
+ node.layout.pendingRemoved.delete(node.$el);
8239
+ }
7947
8240
  };
7948
8241
 
7949
8242
  /**
7950
- * @param {'line'|'word'|'char'} type
7951
- * @param {SplitTemplateParams} params
7952
- * @return {String}
7953
- */
7954
- const generateTemplate = (type, params = {}) => {
7955
- let template = ``;
7956
- const classString = isStr(params.class) ? ` class="${params.class}"` : '';
7957
- const cloneType = setValue(params.clone, false);
7958
- const wrapType = setValue(params.wrap, false);
7959
- const overflow = wrapType ? wrapType === true ? 'clip' : wrapType : cloneType ? 'clip' : false;
7960
- if (wrapType) template += `<span${overflow ? ` style="overflow:${overflow};"` : ''}>`;
7961
- template += `<span${classString}${cloneType ? ` style="position:relative;"` : ''} data-${type}="{i}">`;
7962
- if (cloneType) {
7963
- const left = cloneType === 'left' ? '-100%' : cloneType === 'right' ? '100%' : '0';
7964
- const top = cloneType === 'top' ? '-100%' : cloneType === 'bottom' ? '100%' : '0';
7965
- template += `<span>{value}</span>`;
7966
- template += `<span inert style="position:absolute;top:${top};left:${left};white-space:nowrap;">{value}</span>`;
7967
- } else {
7968
- template += `{value}`;
7969
- }
7970
- template += `</span>`;
7971
- if (wrapType) template += `</span>`;
7972
- return template;
8243
+ * @param {LayoutNode} node
8244
+ * @param {LayoutNode} targetNode
8245
+ * @param {LayoutSnapshot} newState
8246
+ * @return {LayoutNode}
8247
+ */
8248
+ const cloneNodeProperties = (node, targetNode, newState) => {
8249
+ targetNode.properties = /** @type {LayoutNodeProperties} */({ ...node.properties });
8250
+ targetNode.state = newState;
8251
+ targetNode.isTarget = node.isTarget;
8252
+ targetNode.hasTransform = node.hasTransform;
8253
+ targetNode.inlineTransforms = node.inlineTransforms;
8254
+ targetNode.measuredIsVisible = node.measuredIsVisible;
8255
+ targetNode.measuredDisplay = node.measuredDisplay;
8256
+ targetNode.measuredIsRemoved = node.measuredIsRemoved;
8257
+ targetNode.measuredHasDisplayNone = node.measuredHasDisplayNone;
8258
+ targetNode.measuredHasVisibilityHidden = node.measuredHasVisibilityHidden;
8259
+ targetNode.hasDisplayNone = node.hasDisplayNone;
8260
+ targetNode.isInlined = node.isInlined;
8261
+ targetNode.hasVisibilityHidden = node.hasVisibilityHidden;
8262
+ return targetNode;
7973
8263
  };
7974
8264
 
7975
- /**
7976
- * @param {String|SplitFunctionValue} htmlTemplate
7977
- * @param {Array<HTMLElement>} store
7978
- * @param {Node|HTMLElement} node
7979
- * @param {DocumentFragment} $parentFragment
7980
- * @param {'line'|'word'|'char'} type
7981
- * @param {Boolean} debug
7982
- * @param {Number} lineIndex
7983
- * @param {Number} [wordIndex]
7984
- * @param {Number} [charIndex]
7985
- * @return {HTMLElement}
7986
- */
7987
- const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, debug, lineIndex, wordIndex, charIndex) => {
7988
- const isLine = type === lineType;
7989
- const isChar = type === charType;
7990
- const className = `_${type}_`;
7991
- const template = isFnc(htmlTemplate) ? htmlTemplate(node) : htmlTemplate;
7992
- const displayStyle = isLine ? 'block' : 'inline-block';
7993
- $splitTemplate.innerHTML = template
7994
- .replace(valueRgx, `<i class="${className}"></i>`)
7995
- .replace(indexRgx, `${isChar ? charIndex : isLine ? lineIndex : wordIndex}`);
7996
- const $content = $splitTemplate.content;
7997
- const $highestParent = /** @type {HTMLElement} */($content.firstElementChild);
7998
- const $split = /** @type {HTMLElement} */($content.querySelector(`[data-${type}]`)) || $highestParent;
7999
- const $replacables = /** @type {NodeListOf<HTMLElement>} */($content.querySelectorAll(`i.${className}`));
8000
- const replacablesLength = $replacables.length;
8001
- if (replacablesLength) {
8002
- $highestParent.style.display = displayStyle;
8003
- $split.style.display = displayStyle;
8004
- $split.setAttribute(dataLine, `${lineIndex}`);
8005
- if (!isLine) {
8006
- $split.setAttribute('data-word', `${wordIndex}`);
8007
- if (isChar) $split.setAttribute('data-char', `${charIndex}`);
8265
+ class LayoutSnapshot {
8266
+ /**
8267
+ * @param {AutoLayout} layout
8268
+ */
8269
+ constructor(layout) {
8270
+ /** @type {AutoLayout} */
8271
+ this.layout = layout;
8272
+ /** @type {LayoutNode|null} */
8273
+ this.rootNode = null;
8274
+ /** @type {Set<LayoutNode>} */
8275
+ this.rootNodes = new Set();
8276
+ /** @type {Map<String, LayoutNode>} */
8277
+ this.nodes = new Map();
8278
+ /** @type {Number} */
8279
+ this.scrollX = 0;
8280
+ /** @type {Number} */
8281
+ this.scrollY = 0;
8282
+ }
8283
+
8284
+ /**
8285
+ * @return {this}
8286
+ */
8287
+ revert() {
8288
+ this.forEachNode(node => {
8289
+ node.$el.removeAttribute('data-layout-id');
8290
+ node.$measure.removeAttribute('data-layout-id');
8291
+ });
8292
+ this.rootNode = null;
8293
+ this.rootNodes.clear();
8294
+ this.nodes.clear();
8295
+ return this;
8296
+ }
8297
+
8298
+ /**
8299
+ * @param {DOMTarget} $el
8300
+ * @return {LayoutNodeProperties|undefined}
8301
+ */
8302
+ get($el) {
8303
+ const node = this.nodes.get($el.dataset.layoutId);
8304
+ if (!node) {
8305
+ console.warn(`No node found on state`);
8306
+ return;
8008
8307
  }
8009
- let i = replacablesLength;
8010
- while (i--) {
8011
- const $replace = $replacables[i];
8012
- const $closestParent = $replace.parentElement;
8013
- $closestParent.style.display = displayStyle;
8014
- if (isLine) {
8015
- $closestParent.innerHTML = /** @type {HTMLElement} */(node).innerHTML;
8308
+ return node.properties;
8309
+ }
8310
+
8311
+ /**
8312
+ * @param {DOMTarget} $el
8313
+ * @param {String} prop
8314
+ * @return {Number|String|undefined}
8315
+ */
8316
+ getValue($el, prop) {
8317
+ if (!$el || !$el.dataset) {
8318
+ console.warn(`No element found on state (${$el})`);
8319
+ return;
8320
+ }
8321
+ const node = this.nodes.get($el.dataset.layoutId);
8322
+ if (!node) {
8323
+ console.warn(`No node found on state`);
8324
+ return;
8325
+ }
8326
+ const value = node.properties[prop];
8327
+ if (!isUnd(value)) return getFunctionValue(value, $el, node.index, node.total);
8328
+ }
8329
+
8330
+ /**
8331
+ * @param {LayoutNode|null} rootNode
8332
+ * @param {LayoutNodeIterator} cb
8333
+ */
8334
+ forEach(rootNode, cb) {
8335
+ let node = rootNode;
8336
+ let i = 0;
8337
+ while (node) {
8338
+ cb(node, i++);
8339
+ if (node._head) {
8340
+ node = node._head;
8341
+ } else if (node._next) {
8342
+ node = node._next;
8016
8343
  } else {
8017
- $closestParent.replaceChild(node.cloneNode(true), $replace);
8344
+ while (node && !node._next) {
8345
+ node = node.parentNode;
8346
+ }
8347
+ if (node) node = node._next;
8018
8348
  }
8019
8349
  }
8020
- store.push($split);
8021
- $parentFragment.appendChild($content);
8022
- } else {
8023
- console.warn(`The expression "{value}" is missing from the provided template.`);
8024
8350
  }
8025
- if (debug) $highestParent.style.outline = `1px dotted ${debugColors[type]}`;
8026
- return $highestParent;
8027
- };
8028
8351
 
8029
- /**
8030
- * A class that splits text into words and wraps them in span elements while preserving the original HTML structure.
8031
- * @class
8032
- */
8033
- class TextSplitter {
8034
8352
  /**
8035
- * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
8036
- * @param {TextSplitterParams} [parameters]
8353
+ * @param {LayoutNodeIterator} cb
8037
8354
  */
8038
- constructor(target, parameters = {}) {
8039
- // Only init segmenters when needed
8040
- if (!wordSegmenter) wordSegmenter = segmenter ? new segmenter([], { granularity: wordType }) : {
8041
- segment: (text) => {
8042
- const segments = [];
8043
- const words = text.split(whiteSpaceGroupRgx);
8044
- for (let i = 0, l = words.length; i < l; i++) {
8045
- const segment = words[i];
8046
- segments.push({
8047
- segment,
8048
- isWordLike: !whiteSpaceRgx.test(segment), // Consider non-whitespace as word-like
8049
- });
8355
+ forEachRootNode(cb) {
8356
+ this.forEach(this.rootNode, cb);
8357
+ }
8358
+
8359
+ /**
8360
+ * @param {LayoutNodeIterator} cb
8361
+ */
8362
+ forEachNode(cb) {
8363
+ for (const rootNode of this.rootNodes) {
8364
+ this.forEach(rootNode, cb);
8365
+ }
8366
+ }
8367
+
8368
+ /**
8369
+ * @param {DOMTarget} $el
8370
+ * @param {LayoutNode|null} parentNode
8371
+ * @return {LayoutNode|null}
8372
+ */
8373
+ registerElement($el, parentNode) {
8374
+ if (!$el || $el.nodeType !== 1) return null;
8375
+
8376
+ if (!this.layout.transitionMuteStore.has($el)) this.layout.transitionMuteStore.set($el, muteElementTransition($el));
8377
+
8378
+ /** @type {Array<DOMTarget|LayoutNode|null>} */
8379
+ const stack = [$el, parentNode];
8380
+ const root = this.layout.root;
8381
+ let firstNode = null;
8382
+
8383
+ while (stack.length) {
8384
+ /** @type {LayoutNode|null} */
8385
+ const $parent = /** @type {LayoutNode|null} */(stack.pop());
8386
+ /** @type {DOMTarget|null} */
8387
+ const $current = /** @type {DOMTarget|null} */(stack.pop());
8388
+ if (!$current || $current.nodeType !== 1 || isSvg($current)) continue;
8389
+
8390
+ const skipMeasurements = $parent ? $parent.measuredIsRemoved : false;
8391
+
8392
+ const computedStyle = skipMeasurements ? hiddenComputedStyle : getComputedStyle($current);
8393
+ const hasDisplayNone = skipMeasurements ? true : computedStyle.display === 'none';
8394
+ const hasVisibilityHidden = skipMeasurements ? true : computedStyle.visibility === 'hidden';
8395
+ const isVisible = !hasDisplayNone && !hasVisibilityHidden;
8396
+ const existingId = $current.dataset.layoutId;
8397
+ const isInsideRoot = isElementInRoot(root, $current);
8398
+
8399
+ let node = existingId ? this.nodes.get(existingId) : null;
8400
+
8401
+ if (node && node.$el !== $current) {
8402
+ const nodeInsideRoot = isElementInRoot(root, node.$el);
8403
+ const measuredVisible = node.measuredIsVisible;
8404
+ const shouldReassignNode = !nodeInsideRoot && (isInsideRoot || (!isInsideRoot && !measuredVisible && isVisible));
8405
+ const shouldReuseMeasurements = nodeInsideRoot && !measuredVisible && isVisible;
8406
+ // Rebind nodes that move into the root or whose detached twin just became visible
8407
+ if (shouldReassignNode) {
8408
+ detachNode(node);
8409
+ node = createNode($current, $parent, this, node);
8410
+ // for hidden element with in-root sibling, keep the hidden node but borrow measurements from its visible in-root twin element
8411
+ } else if (shouldReuseMeasurements) {
8412
+ recordNodeState(node, $current, computedStyle, skipMeasurements);
8413
+ let $child = $current.lastElementChild;
8414
+ while ($child) {
8415
+ stack.push(/** @type {DOMTarget} */($child), node);
8416
+ $child = $child.previousElementSibling;
8417
+ }
8418
+ if (!firstNode) firstNode = node;
8419
+ continue;
8420
+ // No reassignment needed so keep walking descendants under the current parent
8421
+ } else {
8422
+ let $child = $current.lastElementChild;
8423
+ while ($child) {
8424
+ stack.push(/** @type {DOMTarget} */($child), $parent);
8425
+ $child = $child.previousElementSibling;
8426
+ }
8427
+ if (!firstNode) firstNode = node;
8428
+ continue;
8050
8429
  }
8051
- return segments;
8430
+ } else {
8431
+ node = createNode($current, $parent, this, node);
8052
8432
  }
8053
- };
8054
- if (!graphemeSegmenter) graphemeSegmenter = segmenter ? new segmenter([], { granularity: 'grapheme' }) : {
8055
- segment: text => [...text].map(char => ({ segment: char }))
8056
- };
8057
- if (!$splitTemplate && isBrowser) $splitTemplate = doc.createElement('template');
8058
- if (scope.current) scope.current.register(this);
8059
- const { words, chars, lines, accessible, includeSpaces, debug } = parameters;
8060
- const $target = /** @type {HTMLElement} */((target = isArr(target) ? target[0] : target) && /** @type {Node} */(target).nodeType ? target : (getNodeList(target) || [])[0]);
8061
- const lineParams = lines === true ? {} : lines;
8062
- const wordParams = words === true || isUnd(words) ? {} : words;
8063
- const charParams = chars === true ? {} : chars;
8064
- this.debug = setValue(debug, false);
8065
- this.includeSpaces = setValue(includeSpaces, false);
8066
- this.accessible = setValue(accessible, true);
8067
- this.linesOnly = lineParams && (!wordParams && !charParams);
8068
- /** @type {String|false|SplitFunctionValue} */
8069
- this.lineTemplate = isObj(lineParams) ? generateTemplate(lineType, /** @type {SplitTemplateParams} */(lineParams)) : lineParams;
8070
- /** @type {String|false|SplitFunctionValue} */
8071
- this.wordTemplate = isObj(wordParams) || this.linesOnly ? generateTemplate(wordType, /** @type {SplitTemplateParams} */(wordParams)) : wordParams;
8072
- /** @type {String|false|SplitFunctionValue} */
8073
- this.charTemplate = isObj(charParams) ? generateTemplate(charType, /** @type {SplitTemplateParams} */(charParams)) : charParams;
8074
- this.$target = $target;
8075
- this.html = $target && $target.innerHTML;
8076
- this.lines = [];
8077
- this.words = [];
8078
- this.chars = [];
8079
- this.effects = [];
8080
- this.effectsCleanups = [];
8081
- this.cache = null;
8082
- this.ready = false;
8083
- this.width = 0;
8084
- this.resizeTimeout = null;
8085
- const handleSplit = () => this.html && (lineParams || wordParams || charParams) && this.split();
8086
- // Make sure this is declared before calling handleSplit() in case revert() is called inside an effect callback
8087
- this.resizeObserver = new ResizeObserver(() => {
8088
- // Use a setTimeout instead of a Timer for better tree shaking
8089
- clearTimeout(this.resizeTimeout);
8090
- this.resizeTimeout = setTimeout(() => {
8091
- const currentWidth = /** @type {HTMLElement} */($target).offsetWidth;
8092
- if (currentWidth === this.width) return;
8093
- this.width = currentWidth;
8094
- handleSplit();
8095
- }, 150);
8096
- });
8097
- // Only declare the font ready promise when splitting by lines and not alreay split
8098
- if (this.lineTemplate && !this.ready) {
8099
- doc.fonts.ready.then(handleSplit);
8100
- } else {
8101
- handleSplit();
8433
+
8434
+ node.branchAdded = false;
8435
+ node.branchRemoved = false;
8436
+ node.branchNotRendered = false;
8437
+ node.isTarget = false;
8438
+ node.isAnimated = false;
8439
+ node.hasVisibilityHidden = hasVisibilityHidden;
8440
+ node.hasDisplayNone = hasDisplayNone;
8441
+ node.hasVisibilitySwap = (hasVisibilityHidden && !node.measuredHasVisibilityHidden) || (hasDisplayNone && !node.measuredHasDisplayNone);
8442
+ // node.hasVisibilitySwap = (hasVisibilityHidden !== node.measuredHasVisibilityHidden) || (hasDisplayNone !== node.measuredHasDisplayNone);
8443
+
8444
+ this.nodes.set(node.id, node);
8445
+
8446
+ node.parentNode = $parent || null;
8447
+ node._prev = null;
8448
+ node._next = null;
8449
+
8450
+ if ($parent) {
8451
+ this.rootNodes.delete(node);
8452
+ if (!$parent._head) {
8453
+ $parent._head = node;
8454
+ $parent._tail = node;
8455
+ } else {
8456
+ $parent._tail._next = node;
8457
+ node._prev = $parent._tail;
8458
+ $parent._tail = node;
8459
+ }
8460
+ } else {
8461
+ this.rootNodes.add(node);
8462
+ }
8463
+
8464
+ recordNodeState(node, node.$el, computedStyle, skipMeasurements);
8465
+
8466
+ let $child = $current.lastElementChild;
8467
+ while ($child) {
8468
+ stack.push(/** @type {DOMTarget} */($child), node);
8469
+ $child = $child.previousElementSibling;
8470
+ }
8471
+
8472
+ if (!firstNode) firstNode = node;
8102
8473
  }
8103
- $target ? this.resizeObserver.observe($target) : console.warn('No Text Splitter target found.');
8474
+
8475
+ return firstNode;
8104
8476
  }
8105
8477
 
8106
8478
  /**
8107
- * @param {(...args: any[]) => Tickable | (() => void)} effect
8108
- * @return this
8479
+ * @param {DOMTarget} $el
8480
+ * @param {Set<DOMTarget>} candidates
8481
+ * @return {LayoutNode|null}
8109
8482
  */
8110
- addEffect(effect) {
8111
- if (!isFnc(effect)) return console.warn('Effect must return a function.');
8112
- const refreshableEffect = keepTime(effect);
8113
- this.effects.push(refreshableEffect);
8114
- if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this);
8483
+ ensureDetachedNode($el, candidates) {
8484
+ if (!$el || $el === this.layout.root) return null;
8485
+ const existingId = $el.dataset.layoutId;
8486
+ const existingNode = existingId ? this.nodes.get(existingId) : null;
8487
+ if (existingNode && existingNode.$el === $el) return existingNode;
8488
+ let parentNode = null;
8489
+ let $ancestor = $el.parentElement;
8490
+ while ($ancestor && $ancestor !== this.layout.root) {
8491
+ if (candidates.has($ancestor)) {
8492
+ parentNode = this.ensureDetachedNode($ancestor, candidates);
8493
+ break;
8494
+ }
8495
+ $ancestor = $ancestor.parentElement;
8496
+ }
8497
+ return this.registerElement($el, parentNode);
8498
+ }
8499
+
8500
+ /**
8501
+ * @return {this}
8502
+ */
8503
+ record() {
8504
+ const { children, root } = this.layout;
8505
+ const toParse = isArr(children) ? children : [children];
8506
+ const scoped = [];
8507
+ const scopeRoot = children === '*' ? root : scope.root;
8508
+
8509
+ for (let i = 0, l = toParse.length; i < l; i++) {
8510
+ const child = toParse[i];
8511
+ scoped[i] = isStr(child) ? scopeRoot.querySelectorAll(child) : child;
8512
+ }
8513
+
8514
+ const parsedChildren = registerTargets(scoped);
8515
+
8516
+ this.nodes.clear();
8517
+ this.rootNodes.clear();
8518
+
8519
+ const rootNode = this.registerElement(root, null);
8520
+ // Root node are always targets
8521
+ rootNode.isTarget = true;
8522
+ this.rootNode = rootNode;
8523
+
8524
+ // Track ids of nodes that belong to the current root to filter detached matches
8525
+ const inRootNodeIds = new Set();
8526
+ this.nodes.forEach((node, id) => {
8527
+ if (node && node.measuredIsInsideRoot) {
8528
+ inRootNodeIds.add(id);
8529
+ }
8530
+ });
8531
+
8532
+ // Elements with a layout id outside the root that match the children selector
8533
+ const detachedElementsLookup = new Set();
8534
+ const orderedDetachedElements = [];
8535
+
8536
+ for (let i = 0, l = parsedChildren.length; i < l; i++) {
8537
+ const $el = parsedChildren[i];
8538
+ if (!$el || $el.nodeType !== 1 || $el === root) continue;
8539
+ const insideRoot = isElementInRoot(root, $el);
8540
+ if (!insideRoot) {
8541
+ const layoutNodeId = $el.dataset.layoutId;
8542
+ if (!layoutNodeId || !inRootNodeIds.has(layoutNodeId)) continue;
8543
+ }
8544
+ if (!detachedElementsLookup.has($el)) {
8545
+ detachedElementsLookup.add($el);
8546
+ orderedDetachedElements.push($el);
8547
+ }
8548
+ }
8549
+
8550
+ for (let i = 0, l = orderedDetachedElements.length; i < l; i++) {
8551
+ this.ensureDetachedNode(orderedDetachedElements[i], detachedElementsLookup);
8552
+ }
8553
+
8554
+ for (let i = 0, l = parsedChildren.length; i < l; i++) {
8555
+ const $el = parsedChildren[i];
8556
+ const node = this.nodes.get($el.dataset.layoutId);
8557
+ if (node) {
8558
+ let cur = node;
8559
+ while (cur) {
8560
+ if (cur.isTarget) break;
8561
+ cur.isTarget = true;
8562
+ cur = cur.parentNode;
8563
+ }
8564
+ }
8565
+ }
8566
+
8567
+ this.scrollX = window.scrollX;
8568
+ this.scrollY = window.scrollY;
8569
+
8570
+ const total = this.nodes.size;
8571
+
8572
+ this.forEachNode(restoreNodeTransform);
8573
+ this.forEachNode((node, i) => {
8574
+ node.index = i;
8575
+ node.total = total;
8576
+ });
8577
+
8115
8578
  return this;
8116
8579
  }
8580
+ }
8581
+
8582
+ class AutoLayout {
8583
+ /**
8584
+ * @param {DOMTargetSelector} root
8585
+ * @param {AutoLayoutParams} [params]
8586
+ */
8587
+ constructor(root, params = {}) {
8588
+ if (scope.current) scope.current.register(this);
8589
+ const frozenParams = params.frozen;
8590
+ const addedParams = params.added;
8591
+ const removedParams = params.removed;
8592
+ const propsParams = params.properties;
8593
+ /** @type {AutoLayoutParams} */
8594
+ this.params = params;
8595
+ /** @type {DOMTarget} */
8596
+ this.root = /** @type {DOMTarget} */(registerTargets(root)[0]);
8597
+ /** @type {Number} */
8598
+ this.id = layoutId++;
8599
+ /** @type {LayoutChildrenParam} */
8600
+ this.children = params.children || '*';
8601
+ /** @type {Boolean} */
8602
+ this.absoluteCoords = false;
8603
+ /** @type {Number} */
8604
+ this.duration = setValue(params.duration, 500);
8605
+ /** @type {Number|FunctionValue} */
8606
+ this.delay = setValue(params.delay, 0);
8607
+ /** @type {EasingParam} */
8608
+ this.ease = setValue(params.ease, 'inOutExpo');
8609
+ /** @type {Callback<this>} */
8610
+ this.onComplete = setValue(params.onComplete, /** @type {Callback<this>} */(noop));
8611
+ /** @type {LayoutStateParams} */
8612
+ this.frozenParams = frozenParams || { opacity: 0 };
8613
+ /** @type {LayoutStateParams} */
8614
+ this.addedParams = addedParams || { opacity: 0 };
8615
+ /** @type {LayoutStateParams} */
8616
+ this.removedParams = removedParams || { opacity: 0 };
8617
+ /** @type {Set<String>} */
8618
+ this.properties = new Set([
8619
+ 'opacity',
8620
+ 'borderRadius',
8621
+ ]);
8622
+ if (frozenParams) for (let name in frozenParams) this.properties.add(name);
8623
+ if (addedParams) for (let name in addedParams) this.properties.add(name);
8624
+ if (removedParams) for (let name in removedParams) this.properties.add(name);
8625
+ if (propsParams) for (let i = 0, l = propsParams.length; i < l; i++) this.properties.add(propsParams[i]);
8626
+ /** @type {Set<String>} */
8627
+ this.recordedProperties = new Set([
8628
+ 'display',
8629
+ 'visibility',
8630
+ 'translate',
8631
+ 'position',
8632
+ 'left',
8633
+ 'top',
8634
+ 'marginLeft',
8635
+ 'marginTop',
8636
+ 'width',
8637
+ 'height',
8638
+ 'maxWidth',
8639
+ 'maxHeight',
8640
+ 'minWidth',
8641
+ 'minHeight',
8642
+ ]);
8643
+ this.properties.forEach(prop => this.recordedProperties.add(prop));
8644
+ /** @type {WeakSet<DOMTarget>} */
8645
+ this.pendingRemoved = new WeakSet();
8646
+ /** @type {Map<DOMTarget, String|null>} */
8647
+ this.transitionMuteStore = new Map();
8648
+ /** @type {LayoutSnapshot} */
8649
+ this.oldState = new LayoutSnapshot(this);
8650
+ /** @type {LayoutSnapshot} */
8651
+ this.newState = new LayoutSnapshot(this);
8652
+ /** @type {Timeline|null} */
8653
+ this.timeline = null;
8654
+ /** @type {WAAPIAnimation|null} */
8655
+ this.transformAnimation = null;
8656
+ /** @type {Array<DOMTarget>} */
8657
+ this.frozen = [];
8658
+ /** @type {Array<DOMTarget>} */
8659
+ this.removed = [];
8660
+ /** @type {Array<DOMTarget>} */
8661
+ this.added = [];
8662
+ // Record the current state as the old state to init the data attributes
8663
+ this.oldState.record();
8664
+ // And all layout transition muted during the record
8665
+ restoreLayoutTransition(this.transitionMuteStore);
8666
+ }
8117
8667
 
8668
+ /**
8669
+ * @return {this}
8670
+ */
8118
8671
  revert() {
8119
- clearTimeout(this.resizeTimeout);
8120
- this.lines.length = this.words.length = this.chars.length = 0;
8121
- this.resizeObserver.disconnect();
8122
- // Make sure to revert the effects after disconnecting the resizeObserver to avoid triggering it in the process
8123
- this.effectsCleanups.forEach(cleanup => isFnc(cleanup) ? cleanup(this) : cleanup.revert && cleanup.revert());
8124
- this.$target.innerHTML = this.html;
8672
+ if (this.timeline) {
8673
+ this.timeline.complete();
8674
+ this.timeline = null;
8675
+ }
8676
+ if (this.transformAnimation) {
8677
+ this.transformAnimation.complete();
8678
+ this.transformAnimation = null;
8679
+ }
8680
+ this.root.classList.remove('is-animated');
8681
+ this.frozen.length = this.removed.length = this.added.length = 0;
8682
+ this.oldState.revert();
8683
+ this.newState.revert();
8684
+ requestAnimationFrame(() => restoreLayoutTransition(this.transitionMuteStore));
8125
8685
  return this;
8126
8686
  }
8127
8687
 
8128
8688
  /**
8129
- * Recursively processes a node and its children
8130
- * @param {Node} node
8689
+ * @return {this}
8131
8690
  */
8132
- splitNode(node) {
8133
- const wordTemplate = this.wordTemplate;
8134
- const charTemplate = this.charTemplate;
8135
- const includeSpaces = this.includeSpaces;
8136
- const debug = this.debug;
8137
- const nodeType = node.nodeType;
8138
- if (nodeType === 3) {
8139
- const nodeText = node.nodeValue;
8140
- // If the nodeText is only whitespace, leave it as is
8141
- if (nodeText.trim()) {
8142
- const tempWords = [];
8143
- const words = this.words;
8144
- const chars = this.chars;
8145
- const wordSegments = wordSegmenter.segment(nodeText);
8146
- const $wordsFragment = doc.createDocumentFragment();
8147
- let prevSeg = null;
8148
- for (const wordSegment of wordSegments) {
8149
- const segment = wordSegment.segment;
8150
- const isWordLike = isSegmentWordLike(wordSegment);
8151
- // Determine if this segment should be a new word, first segment always becomes a new word
8152
- if (!prevSeg || (isWordLike && (prevSeg && (isSegmentWordLike(prevSeg))))) {
8153
- tempWords.push(segment);
8154
- } else {
8155
- // Only concatenate if both current and previous are non-word-like and don't contain spaces
8156
- const lastWordIndex = tempWords.length - 1;
8157
- const lastWord = tempWords[lastWordIndex];
8158
- if (!lastWord.includes(' ') && !segment.includes(' ')) {
8159
- tempWords[lastWordIndex] += segment;
8160
- } else {
8161
- tempWords.push(segment);
8162
- }
8691
+ record() {
8692
+ // Commit transforms before measuring
8693
+ if (this.transformAnimation) {
8694
+ this.transformAnimation.cancel();
8695
+ this.transformAnimation = null;
8696
+ }
8697
+ // Record the old state
8698
+ this.oldState.record();
8699
+ // Cancel any running timeline
8700
+ if (this.timeline) {
8701
+ this.timeline.cancel();
8702
+ this.timeline = null;
8703
+ }
8704
+ // Restore previously captured inline styles
8705
+ this.newState.forEachRootNode(restoreNodeInlineStyles);
8706
+ return this;
8707
+ }
8708
+
8709
+ /**
8710
+ * @param {LayoutAnimationParams} [params]
8711
+ * @return {Timeline}
8712
+ */
8713
+ animate(params = {}) {
8714
+ const delay = setValue(params.delay, this.delay);
8715
+ const duration = setValue(params.duration, this.duration);
8716
+ const onComplete = setValue(params.onComplete, this.onComplete);
8717
+ const frozenParams = params.frozen ? mergeObjects(params.frozen, this.frozenParams) : this.frozenParams;
8718
+ const addedParams = params.added ? mergeObjects(params.added, this.addedParams) : this.addedParams;
8719
+ const removedParams = params.removed ? mergeObjects(params.removed, this.removedParams) : this.removedParams;
8720
+ const oldState = this.oldState;
8721
+ const newState = this.newState;
8722
+ const added = this.added;
8723
+ const removed = this.removed;
8724
+ const frozen = this.frozen;
8725
+ const pendingRemoved = this.pendingRemoved;
8726
+
8727
+ added.length = removed.length = frozen.length = 0;
8728
+
8729
+ // Mute old state CSS transitions to prevent wrong properties calculation
8730
+ oldState.forEachRootNode(muteNodeTransition);
8731
+ // Capture the new state before animation
8732
+ newState.record();
8733
+ newState.forEachRootNode(recordNodeInlineStyles);
8734
+
8735
+ const targets = [];
8736
+ const animated = [];
8737
+ const transformed = [];
8738
+ const animatedFrozen = [];
8739
+ const root = newState.rootNode.$el;
8740
+
8741
+ newState.forEachRootNode(node => {
8742
+ const $el = node.$el;
8743
+ const id = node.id;
8744
+ const parent = node.parentNode;
8745
+ const parentAdded = parent ? parent.branchAdded : false;
8746
+ const parentRemoved = parent ? parent.branchRemoved : false;
8747
+ const parentNotRendered = parent ? parent.branchNotRendered : false;
8748
+
8749
+ // Delay and duration must be calculated in the animate() call to support delay override
8750
+ node.delay = +(isFnc(delay) ? delay($el, node.index, node.total) : delay);
8751
+ node.duration = +(isFnc(duration) ? duration($el, node.index, node.total) : duration);
8752
+
8753
+ let oldStateNode = oldState.nodes.get(id);
8754
+
8755
+ const hasNoOldState = !oldStateNode;
8756
+
8757
+ if (hasNoOldState) {
8758
+ oldStateNode = cloneNodeProperties(node, /** @type {LayoutNode} */({}), oldState);
8759
+ oldState.nodes.set(id, oldStateNode);
8760
+ oldStateNode.measuredIsRemoved = true;
8761
+ } else if (oldStateNode.measuredIsRemoved && !node.measuredIsRemoved) {
8762
+ cloneNodeProperties(node, oldStateNode, oldState);
8763
+ oldStateNode.measuredIsRemoved = true;
8764
+ }
8765
+
8766
+ const oldParentNode = oldStateNode.parentNode;
8767
+ const oldParentId = oldParentNode ? oldParentNode.id : null;
8768
+ const newParentId = parent ? parent.id : null;
8769
+ const parentChanged = oldParentId !== newParentId;
8770
+ const elementChanged = oldStateNode.$el !== node.$el;
8771
+ const wasRemovedBefore = oldStateNode.measuredIsRemoved;
8772
+ const isRemovedNow = node.measuredIsRemoved;
8773
+
8774
+ // Recalculate postion relative to their parent for elements that have been moved
8775
+ if (!oldStateNode.measuredIsRemoved && !isRemovedNow && !hasNoOldState && (parentChanged || elementChanged)) {
8776
+ let offsetX = 0;
8777
+ let offsetY = 0;
8778
+ let current = node.parentNode;
8779
+ while (current) {
8780
+ offsetX += current.properties.x || 0;
8781
+ offsetY += current.properties.y || 0;
8782
+ if (current.parentNode === newState.rootNode) break;
8783
+ current = current.parentNode;
8784
+ }
8785
+ let oldOffsetX = 0;
8786
+ let oldOffsetY = 0;
8787
+ let oldCurrent = oldStateNode.parentNode;
8788
+ while (oldCurrent) {
8789
+ oldOffsetX += oldCurrent.properties.x || 0;
8790
+ oldOffsetY += oldCurrent.properties.y || 0;
8791
+ if (oldCurrent.parentNode === oldState.rootNode) break;
8792
+ oldCurrent = oldCurrent.parentNode;
8793
+ }
8794
+ oldStateNode.properties.x += oldOffsetX - offsetX;
8795
+ oldStateNode.properties.y += oldOffsetY - offsetY;
8796
+ }
8797
+
8798
+ if (node.hasVisibilitySwap) {
8799
+ if (node.hasVisibilityHidden) {
8800
+ node.$el.style.visibility = 'visible';
8801
+ node.$measure.style.visibility = 'hidden';
8802
+ }
8803
+ if (node.hasDisplayNone) {
8804
+ node.$el.style.display = oldStateNode.measuredDisplay || node.measuredDisplay || '';
8805
+ // Setting visibility 'hidden' instead of display none to avoid calculation issues
8806
+ node.$measure.style.visibility = 'hidden';
8807
+ // @TODO: check why setting display here can cause calculation issues
8808
+ // node.$measure.style.display = 'none';
8809
+ }
8810
+ }
8811
+
8812
+ const wasPendingRemoval = pendingRemoved.has($el);
8813
+ const wasVisibleBefore = oldStateNode.measuredIsVisible;
8814
+ const isVisibleNow = node.measuredIsVisible;
8815
+ const becomeVisible = !wasVisibleBefore && isVisibleNow && !parentNotRendered;
8816
+ const topLevelAdded = !isRemovedNow && (wasRemovedBefore || wasPendingRemoval) && !parentAdded;
8817
+ const newlyRemoved = isRemovedNow && !wasRemovedBefore && !parentRemoved;
8818
+ const topLevelRemoved = newlyRemoved || isRemovedNow && wasPendingRemoval && !parentRemoved;
8819
+
8820
+ if (node.measuredIsRemoved && wasVisibleBefore) {
8821
+ node.$el.style.display = oldStateNode.measuredDisplay;
8822
+ node.$el.style.visibility = 'visible';
8823
+ cloneNodeProperties(oldStateNode, node, newState);
8824
+ }
8825
+
8826
+ if (newlyRemoved) {
8827
+ removed.push($el);
8828
+ pendingRemoved.add($el);
8829
+ } else if (!isRemovedNow && wasPendingRemoval) {
8830
+ pendingRemoved.delete($el);
8831
+ }
8832
+
8833
+ // Node is added
8834
+ if ((topLevelAdded && !parentNotRendered) || becomeVisible) {
8835
+ updateNodeProperties(oldStateNode, addedParams);
8836
+ added.push($el);
8837
+ // Node is removed
8838
+ } else if (topLevelRemoved && !parentNotRendered) {
8839
+ updateNodeProperties(node, removedParams);
8840
+ }
8841
+
8842
+ // Compute function based propety values before cheking for changes
8843
+ for (let name in node.properties) {
8844
+ node.properties[name] = newState.getValue(node.$el, name);
8845
+ // NOTE: I'm using node.$el to get the value of old state, make sure this is valid instead of oldStateNode.$el
8846
+ oldStateNode.properties[name] = oldState.getValue(node.$el, name);
8847
+ }
8848
+
8849
+ const hiddenStateChanged = (topLevelAdded || newlyRemoved) && wasRemovedBefore !== isRemovedNow;
8850
+ let propertyChanged = false;
8851
+
8852
+
8853
+ if (node.isTarget && (!node.measuredIsRemoved && wasVisibleBefore || node.measuredIsRemoved && isVisibleNow)) {
8854
+ if (!node.isInlined && (node.properties.transform !== 'none' || oldStateNode.properties.transform !== 'none')) {
8855
+ node.hasTransform = true;
8856
+ propertyChanged = true;
8857
+ transformed.push($el);
8858
+ }
8859
+ for (let name in node.properties) {
8860
+ if (name !== 'transform' && (node.properties[name] !== oldStateNode.properties[name] || hiddenStateChanged)) {
8861
+ propertyChanged = true;
8862
+ animated.push($el);
8863
+ break;
8163
8864
  }
8164
- prevSeg = wordSegment;
8165
8865
  }
8866
+ }
8166
8867
 
8167
- for (let i = 0, l = tempWords.length; i < l; i++) {
8168
- const word = tempWords[i];
8169
- if (!word.trim()) {
8170
- // Preserve whitespace only if includeSpaces is false and if the current space is not the first node
8171
- if (i && includeSpaces) continue;
8172
- $wordsFragment.appendChild(doc.createTextNode(word));
8173
- } else {
8174
- const nextWord = tempWords[i + 1];
8175
- const hasWordFollowingSpace = includeSpaces && nextWord && !nextWord.trim();
8176
- const wordToProcess = word;
8177
- const charSegments = charTemplate ? graphemeSegmenter.segment(wordToProcess) : null;
8178
- const $charsFragment = charTemplate ? doc.createDocumentFragment() : doc.createTextNode(hasWordFollowingSpace ? word + '\xa0' : word);
8179
- if (charTemplate) {
8180
- const charSegmentsArray = [...charSegments];
8181
- for (let j = 0, jl = charSegmentsArray.length; j < jl; j++) {
8182
- const charSegment = charSegmentsArray[j];
8183
- const isLastChar = j === jl - 1;
8184
- // If this is the last character and includeSpaces is true with a following space, append the space
8185
- const charText = isLastChar && hasWordFollowingSpace ? charSegment.segment + '\xa0' : charSegment.segment;
8186
- const $charNode = doc.createTextNode(charText);
8187
- processHTMLTemplate(charTemplate, chars, $charNode, /** @type {DocumentFragment} */($charsFragment), charType, debug, -1, words.length, chars.length);
8188
- }
8189
- }
8190
- if (wordTemplate) {
8191
- processHTMLTemplate(wordTemplate, words, $charsFragment, $wordsFragment, wordType, debug, -1, words.length, chars.length);
8192
- // Chars elements must be re-parsed in the split() method if both words and chars are parsed
8193
- } else if (charTemplate) {
8194
- $wordsFragment.appendChild($charsFragment);
8195
- } else {
8196
- $wordsFragment.appendChild(doc.createTextNode(word));
8197
- }
8198
- // Skip the next iteration if we included a space
8199
- if (hasWordFollowingSpace) i++;
8868
+ const nodeHasChanged = (propertyChanged || topLevelAdded || topLevelRemoved || becomeVisible);
8869
+ const nodeIsAnimated = node.isTarget && nodeHasChanged;
8870
+
8871
+ node.isAnimated = nodeIsAnimated;
8872
+ node.branchAdded = parentAdded || topLevelAdded;
8873
+ node.branchRemoved = parentRemoved || topLevelRemoved;
8874
+ node.branchNotRendered = parentNotRendered || node.measuredIsRemoved;
8875
+
8876
+ const sizeTolerance = 1;
8877
+ const widthChanged = Math.abs(node.properties.width - oldStateNode.properties.width) > sizeTolerance;
8878
+ const heightChanged = Math.abs(node.properties.height - oldStateNode.properties.height) > sizeTolerance;
8879
+
8880
+ node.sizeChanged = (widthChanged || heightChanged);
8881
+
8882
+ targets.push($el);
8883
+
8884
+ if (!node.isTarget) {
8885
+ frozen.push($el);
8886
+ if ((nodeHasChanged || node.sizeChanged) && parent && parent.isTarget && parent.isAnimated && parent.sizeChanged) {
8887
+ animatedFrozen.push($el);
8888
+ }
8889
+ }
8890
+ });
8891
+
8892
+ const defaults = {
8893
+ ease: setValue(params.ease, this.ease),
8894
+ duration: (/** @type {HTMLElement} */$el) => newState.nodes.get($el.dataset.layoutId).duration,
8895
+ delay: (/** @type {HTMLElement} */$el) => newState.nodes.get($el.dataset.layoutId).delay,
8896
+ };
8897
+
8898
+ this.timeline = createTimeline({
8899
+ onComplete: () => {
8900
+ // Make sure to call .cancel() after restoreNodeInlineStyles(node); otehrwise the commited styles get reverted
8901
+ if (this.transformAnimation) this.transformAnimation.cancel();
8902
+ newState.forEachRootNode(node => {
8903
+ restoreNodeVisualState(node);
8904
+ restoreNodeInlineStyles(node);
8905
+ });
8906
+ for (let i = 0, l = transformed.length; i < l; i++) {
8907
+ const $el = transformed[i];
8908
+ $el.style.transform = newState.getValue($el, 'transform');
8909
+ }
8910
+ this.root.classList.remove('is-animated');
8911
+ if (onComplete) onComplete(this);
8912
+ // Avoid CSS transitions at the end of the animation by restoring them on the next frame
8913
+ requestAnimationFrame(() => {
8914
+ if (this.root.classList.contains('is-animated')) return;
8915
+ restoreLayoutTransition(this.transitionMuteStore);
8916
+ });
8917
+ },
8918
+ onPause: () => {
8919
+ if (this.transformAnimation) this.transformAnimation.cancel();
8920
+ newState.forEachRootNode(restoreNodeVisualState);
8921
+ this.root.classList.remove('is-animated');
8922
+ if (onComplete) onComplete(this);
8923
+ },
8924
+ composition: false,
8925
+ defaults,
8926
+ });
8927
+
8928
+ if (targets.length) {
8929
+
8930
+ this.root.classList.add('is-animated');
8931
+
8932
+ for (let i = 0, l = targets.length; i < l; i++) {
8933
+ const $el = targets[i];
8934
+ const id = $el.dataset.layoutId;
8935
+ const oldNode = oldState.nodes.get(id);
8936
+ const newNode = newState.nodes.get(id);
8937
+ const oldNodeState = oldNode.properties;
8938
+
8939
+ // Make sure to mute all CSS transition before applying the oldState styles back
8940
+ muteNodeTransition(newNode);
8941
+
8942
+ // Don't animate dimensions and positions of inlined elements
8943
+ if (!newNode.isInlined) {
8944
+ // Display grid can mess with the absolute positioning, so set it to block during transition
8945
+ // if (oldNode.measuredDisplay === 'grid' || newNode.measuredDisplay === 'grid') $el.style.display = 'block';
8946
+ $el.style.display = 'block';
8947
+ // All children must be in position absolue
8948
+ if ($el !== root || this.absoluteCoords) {
8949
+ $el.style.position = this.absoluteCoords ? 'fixed' : 'absolute';
8950
+ $el.style.left = '0px';
8951
+ $el.style.top = '0px';
8952
+ $el.style.marginLeft = '0px';
8953
+ $el.style.marginTop = '0px';
8954
+ $el.style.translate = `${oldNodeState.x}px ${oldNodeState.y}px`;
8955
+ }
8956
+ if ($el === root && newNode.measuredPosition === 'static') {
8957
+ $el.style.position = 'relative';
8958
+ // Cancel left / trop in case the static element had muted values now activated by potision relative
8959
+ $el.style.left = '0px';
8960
+ $el.style.top = '0px';
8200
8961
  }
8962
+ $el.style.width = `${oldNodeState.width}px`;
8963
+ $el.style.height = `${oldNodeState.height}px`;
8964
+ // Overrides user defined min and max to prevents width and height clamping
8965
+ $el.style.minWidth = `auto`;
8966
+ $el.style.minHeight = `auto`;
8967
+ $el.style.maxWidth = `none`;
8968
+ $el.style.maxHeight = `none`;
8201
8969
  }
8202
- node.parentNode.replaceChild($wordsFragment, node);
8203
8970
  }
8204
- } else if (nodeType === 1) {
8205
- // Converting to an array is necessary to work around childNodes pottential mutation
8206
- const childNodes = /** @type {Array<Node>} */([.../** @type {*} */(node.childNodes)]);
8207
- for (let i = 0, l = childNodes.length; i < l; i++) this.splitNode(childNodes[i]);
8208
- }
8209
- }
8210
8971
 
8211
- /**
8212
- * @param {Boolean} clearCache
8213
- * @return {this}
8214
- */
8215
- split(clearCache = false) {
8216
- const $el = this.$target;
8217
- const isCached = !!this.cache && !clearCache;
8218
- const lineTemplate = this.lineTemplate;
8219
- const wordTemplate = this.wordTemplate;
8220
- const charTemplate = this.charTemplate;
8221
- const fontsReady = doc.fonts.status !== 'loading';
8222
- const canSplitLines = lineTemplate && fontsReady;
8223
- this.ready = !lineTemplate || fontsReady;
8224
- if (canSplitLines || clearCache) {
8225
- // No need to revert effects animations here since it's already taken care by the refreshable
8226
- this.effectsCleanups.forEach(cleanup => isFnc(cleanup) && cleanup(this));
8227
- }
8228
- if (!isCached) {
8229
- if (clearCache) {
8230
- $el.innerHTML = this.html;
8231
- this.words.length = this.chars.length = 0;
8972
+ // Restore the scroll position if the oldState differs from the current state
8973
+ if (oldState.scrollX !== window.scrollX || oldState.scrollY !== window.scrollY) {
8974
+ // Restoring in the next frame avoids race conditions if for example a waapi animation commit styles that affect the root height
8975
+ requestAnimationFrame(() => {
8976
+ window.scrollTo(oldState.scrollX, oldState.scrollY);
8977
+ });
8232
8978
  }
8233
- this.splitNode($el);
8234
- this.cache = $el.innerHTML;
8235
- }
8236
- if (canSplitLines) {
8237
- if (isCached) $el.innerHTML = this.cache;
8238
- this.lines.length = 0;
8239
- if (wordTemplate) this.words = getAllTopLevelElements($el, wordType);
8240
- }
8241
- // Always reparse characters after a line reset or if both words and chars are activated
8242
- if (charTemplate && (canSplitLines || wordTemplate)) {
8243
- this.chars = getAllTopLevelElements($el, charType);
8244
- }
8245
- // Words are used when lines only and prioritized over chars
8246
- const elementsArray = this.words.length ? this.words : this.chars;
8247
- let y, linesCount = 0;
8248
- for (let i = 0, l = elementsArray.length; i < l; i++) {
8249
- const $el = elementsArray[i];
8250
- const { top, height } = $el.getBoundingClientRect();
8251
- if (y && top - y > height * .5) linesCount++;
8252
- $el.setAttribute(dataLine, `${linesCount}`);
8253
- const nested = $el.querySelectorAll(`[${dataLine}]`);
8254
- let c = nested.length;
8255
- while (c--) nested[c].setAttribute(dataLine, `${linesCount}`);
8256
- y = top;
8979
+
8980
+ for (let i = 0, l = animated.length; i < l; i++) {
8981
+ const $el = animated[i];
8982
+ const id = $el.dataset.layoutId;
8983
+ const oldNode = oldState.nodes.get(id);
8984
+ const newNode = newState.nodes.get(id);
8985
+ const oldNodeState = oldNode.properties;
8986
+ const newNodeState = newNode.properties;
8987
+ let hasChanged = false;
8988
+ const animatedProps = {
8989
+ composition: 'none',
8990
+ // delay: (/** @type {HTMLElement} */$el) => newState.nodes.get($el.dataset.layoutId).delay,
8991
+ };
8992
+ if (!newNode.isInlined) {
8993
+ if (oldNodeState.width !== newNodeState.width) {
8994
+ animatedProps.width = [oldNodeState.width, newNodeState.width];
8995
+ hasChanged = true;
8996
+ }
8997
+ if (oldNodeState.height !== newNodeState.height) {
8998
+ animatedProps.height = [oldNodeState.height, newNodeState.height];
8999
+ hasChanged = true;
9000
+ }
9001
+ // If the node has transforms we handle the translate animation in wappi otherwise translate and other transforms can be out of sync
9002
+ // Always animate translate
9003
+ if (!newNode.hasTransform) {
9004
+ animatedProps.translate = [`${oldNodeState.x}px ${oldNodeState.y}px`, `${newNodeState.x}px ${newNodeState.y}px`];
9005
+ hasChanged = true;
9006
+ }
9007
+ }
9008
+ this.properties.forEach(prop => {
9009
+ const oldVal = oldNodeState[prop];
9010
+ const newVal = newNodeState[prop];
9011
+ if (prop !== 'transform' && oldVal !== newVal) {
9012
+ animatedProps[prop] = [oldVal, newVal];
9013
+ hasChanged = true;
9014
+ }
9015
+ });
9016
+ if (hasChanged) {
9017
+ this.timeline.add($el, animatedProps, 0);
9018
+ }
9019
+ }
9020
+
8257
9021
  }
8258
- if (canSplitLines) {
8259
- const linesFragment = doc.createDocumentFragment();
8260
- const parents = new Set();
8261
- const clones = [];
8262
- for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) {
8263
- const $clone = /** @type {HTMLElement} */($el.cloneNode(true));
8264
- filterLineElements($clone, lineIndex, new Set()).forEach($el => {
8265
- const $parent = $el.parentElement;
8266
- if ($parent) parents.add($parent);
8267
- $el.remove();
9022
+
9023
+ if (frozen.length) {
9024
+
9025
+ for (let i = 0, l = frozen.length; i < l; i++) {
9026
+ const $el = frozen[i];
9027
+ const oldNode = oldState.nodes.get($el.dataset.layoutId);
9028
+ if (!oldNode.isInlined) {
9029
+ const oldNodeState = oldState.get($el);
9030
+ $el.style.width = `${oldNodeState.width}px`;
9031
+ $el.style.height = `${oldNodeState.height}px`;
9032
+ // Overrides user defined min and max to prevents width and height clamping
9033
+ $el.style.minWidth = `auto`;
9034
+ $el.style.minHeight = `auto`;
9035
+ $el.style.maxWidth = `none`;
9036
+ $el.style.maxHeight = `none`;
9037
+ $el.style.translate = `${oldNodeState.x}px ${oldNodeState.y}px`;
9038
+ }
9039
+ this.properties.forEach(prop => {
9040
+ if (prop !== 'transform') {
9041
+ $el.style[prop] = `${oldState.getValue($el, prop)}`;
9042
+ }
8268
9043
  });
8269
- clones.push($clone);
8270
9044
  }
8271
- parents.forEach(filterEmptyElements);
8272
- for (let cloneIndex = 0, clonesLength = clones.length; cloneIndex < clonesLength; cloneIndex++) {
8273
- processHTMLTemplate(lineTemplate, this.lines, clones[cloneIndex], linesFragment, lineType, this.debug, cloneIndex);
9045
+
9046
+ for (let i = 0, l = frozen.length; i < l; i++) {
9047
+ const $el = frozen[i];
9048
+ const newNode = newState.nodes.get($el.dataset.layoutId);
9049
+ const newNodeState = newState.get($el);
9050
+ this.timeline.call(() => {
9051
+ if (!newNode.isInlined) {
9052
+ $el.style.width = `${newNodeState.width}px`;
9053
+ $el.style.height = `${newNodeState.height}px`;
9054
+ // Overrides user defined min and max to prevents width and height clamping
9055
+ $el.style.minWidth = `auto`;
9056
+ $el.style.minHeight = `auto`;
9057
+ $el.style.maxWidth = `none`;
9058
+ $el.style.maxHeight = `none`;
9059
+ $el.style.translate = `${newNodeState.x}px ${newNodeState.y}px`;
9060
+ }
9061
+ this.properties.forEach(prop => {
9062
+ if (prop !== 'transform') {
9063
+ $el.style[prop] = `${newState.getValue($el, prop)}`;
9064
+ }
9065
+ });
9066
+ }, newNode.delay + newNode.duration / 2);
8274
9067
  }
8275
- $el.innerHTML = '';
8276
- $el.appendChild(linesFragment);
8277
- if (wordTemplate) this.words = getAllTopLevelElements($el, wordType);
8278
- if (charTemplate) this.chars = getAllTopLevelElements($el, charType);
8279
- }
8280
- // Remove the word wrappers and clear the words array if lines split only
8281
- if (this.linesOnly) {
8282
- const words = this.words;
8283
- let w = words.length;
8284
- while (w--) {
8285
- const $word = words[w];
8286
- $word.replaceWith($word.textContent);
9068
+
9069
+ if (animatedFrozen.length) {
9070
+ const animatedFrozenParams = /** @type {AnimationParams} */({});
9071
+ if (frozenParams) {
9072
+ for (let prop in frozenParams) {
9073
+ animatedFrozenParams[prop] = [
9074
+ { from: (/** @type {HTMLElement} */$el) => oldState.getValue($el, prop), ease: 'in(1.75)', to: frozenParams[prop] },
9075
+ { from: frozenParams[prop], to: (/** @type {HTMLElement} */$el) => newState.getValue($el, prop), ease: 'out(1.75)' }
9076
+ ];
9077
+ }
9078
+ }
9079
+ this.timeline.add(animatedFrozen, animatedFrozenParams, 0);
8287
9080
  }
8288
- words.length = 0;
8289
- }
8290
- if (this.accessible && (canSplitLines || !isCached)) {
8291
- const $accessible = doc.createElement('span');
8292
- // Make the accessible element visually-hidden (https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html)
8293
- $accessible.style.cssText = `position:absolute;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);width:1px;height:1px;white-space:nowrap;`;
8294
- // $accessible.setAttribute('tabindex', '-1');
8295
- $accessible.innerHTML = this.html;
8296
- $el.insertBefore($accessible, $el.firstChild);
8297
- this.lines.forEach(setAriaHidden);
8298
- this.words.forEach(setAriaHidden);
8299
- this.chars.forEach(setAriaHidden);
9081
+
8300
9082
  }
8301
- this.width = /** @type {HTMLElement} */($el).offsetWidth;
8302
- if (canSplitLines || clearCache) {
8303
- this.effects.forEach((effect, i) => this.effectsCleanups[i] = effect(this));
9083
+
9084
+ const transformedLength = transformed.length;
9085
+
9086
+ if (transformedLength) {
9087
+ // We only need to set the transform property here since translate is alread defined the targets loop
9088
+ for (let i = 0; i < transformedLength; i++) {
9089
+ const $el = transformed[i];
9090
+ $el.style.translate = `${oldState.get($el).x}px ${oldState.get($el).y}px`,
9091
+ $el.style.transform = oldState.getValue($el, 'transform');
9092
+ }
9093
+ this.transformAnimation = waapi.animate(transformed, {
9094
+ translate: (/** @type {HTMLElement} */$el) => `${newState.get($el).x}px ${newState.get($el).y}px`,
9095
+ transform: (/** @type {HTMLElement} */$el) => newState.getValue($el, 'transform'),
9096
+ autoplay: false,
9097
+ persist: true,
9098
+ ...defaults,
9099
+ });
9100
+ this.timeline.sync(this.transformAnimation, 0);
8304
9101
  }
8305
- return this;
9102
+
9103
+ return this.timeline.init();
8306
9104
  }
8307
9105
 
8308
- refresh() {
8309
- this.split(true);
9106
+ /**
9107
+ * @param {(layout: this) => void} callback
9108
+ * @param {LayoutAnimationParams} [params]
9109
+ * @return {this}
9110
+ */
9111
+ update(callback, params = {}) {
9112
+ this.record();
9113
+ callback(this);
9114
+ this.animate(params);
9115
+ return this;
8310
9116
  }
8311
9117
  }
8312
9118
 
8313
9119
  /**
8314
- * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
8315
- * @param {TextSplitterParams} [parameters]
8316
- * @return {TextSplitter}
9120
+ * @param {DOMTargetSelector} root
9121
+ * @param {AutoLayoutParams} [params]
9122
+ * @return {AutoLayout}
8317
9123
  */
8318
- const splitText = (target, parameters) => new TextSplitter(target, parameters);
9124
+ const createLayout = (root, params) => new AutoLayout(root, params);
9125
+
9126
+ // Chain-able utilities
9127
+
9128
+ const numberUtils = numberImports; // Needed to keep the import when bundling
9129
+
9130
+ const chainables = {};
8319
9131
 
8320
9132
  /**
8321
- * @deprecated text.split() is deprecated, import splitText() directly, or text.splitText()
9133
+ * @callback UtilityFunction
9134
+ * @param {...*} args
9135
+ * @return {Number|String}
8322
9136
  *
8323
- * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
8324
- * @param {TextSplitterParams} [parameters]
8325
- * @return {TextSplitter}
9137
+ * @param {UtilityFunction} fn
9138
+ * @param {Number} [last=0]
9139
+ * @return {function(...(Number|String)): function(Number|String): (Number|String)}
8326
9140
  */
8327
- const split = (target, parameters) => {
8328
- console.warn('text.split() is deprecated, import splitText() directly, or text.splitText()');
8329
- return new TextSplitter(target, parameters);
9141
+ const curry = (fn, last = 0) => (...args) => last ? v => fn(...args, v) : v => fn(v, ...args);
9142
+
9143
+ /**
9144
+ * @param {Function} fn
9145
+ * @return {function(...(Number|String))}
9146
+ */
9147
+ const chain = fn => {
9148
+ return (...args) => {
9149
+ const result = fn(...args);
9150
+ return new Proxy(noop, {
9151
+ apply: (_, __, [v]) => result(v),
9152
+ get: (_, prop) => chain(/**@param {...Number|String} nextArgs */(...nextArgs) => {
9153
+ const nextResult = chainables[prop](...nextArgs);
9154
+ return (/**@type {Number|String} */v) => nextResult(result(v));
9155
+ })
9156
+ });
9157
+ }
9158
+ };
9159
+
9160
+ /**
9161
+ * @param {UtilityFunction} fn
9162
+ * @param {String} name
9163
+ * @param {Number} [right]
9164
+ * @return {function(...(Number|String)): UtilityFunction}
9165
+ */
9166
+ const makeChainable = (name, fn, right = 0) => {
9167
+ const chained = (...args) => (args.length < fn.length ? chain(curry(fn, right)) : fn)(...args);
9168
+ if (!chainables[name]) chainables[name] = chained;
9169
+ return chained;
9170
+ };
9171
+
9172
+ /**
9173
+ * @typedef {Object} ChainablesMap
9174
+ * @property {ChainedClamp} clamp
9175
+ * @property {ChainedRound} round
9176
+ * @property {ChainedSnap} snap
9177
+ * @property {ChainedWrap} wrap
9178
+ * @property {ChainedLerp} lerp
9179
+ * @property {ChainedDamp} damp
9180
+ * @property {ChainedMapRange} mapRange
9181
+ * @property {ChainedRoundPad} roundPad
9182
+ * @property {ChainedPadStart} padStart
9183
+ * @property {ChainedPadEnd} padEnd
9184
+ * @property {ChainedDegToRad} degToRad
9185
+ * @property {ChainedRadToDeg} radToDeg
9186
+ */
9187
+
9188
+ /**
9189
+ * @callback ChainedUtilsResult
9190
+ * @param {Number} value - The value to process through the chained operations
9191
+ * @return {Number} The processed result
9192
+ */
9193
+
9194
+ /**
9195
+ * @typedef {ChainablesMap & ChainedUtilsResult} ChainableUtil
9196
+ */
9197
+
9198
+ // Chainable
9199
+
9200
+ /**
9201
+ * @callback ChainedRoundPad
9202
+ * @param {Number} decimalLength - Number of decimal places
9203
+ * @return {ChainableUtil}
9204
+ */
9205
+ const roundPad = /** @type {typeof numberUtils.roundPad & ChainedRoundPad} */(makeChainable('roundPad', numberUtils.roundPad));
9206
+
9207
+ /**
9208
+ * @callback ChainedPadStart
9209
+ * @param {Number} totalLength - Target length
9210
+ * @param {String} padString - String to pad with
9211
+ * @return {ChainableUtil}
9212
+ */
9213
+ const padStart = /** @type {typeof numberUtils.padStart & ChainedPadStart} */(makeChainable('padStart', numberUtils.padStart));
9214
+
9215
+ /**
9216
+ * @callback ChainedPadEnd
9217
+ * @param {Number} totalLength - Target length
9218
+ * @param {String} padString - String to pad with
9219
+ * @return {ChainableUtil}
9220
+ */
9221
+ const padEnd = /** @type {typeof numberUtils.padEnd & ChainedPadEnd} */(makeChainable('padEnd', numberUtils.padEnd));
9222
+
9223
+ /**
9224
+ * @callback ChainedWrap
9225
+ * @param {Number} min - Minimum boundary
9226
+ * @param {Number} max - Maximum boundary
9227
+ * @return {ChainableUtil}
9228
+ */
9229
+ const wrap = /** @type {typeof numberUtils.wrap & ChainedWrap} */(makeChainable('wrap', numberUtils.wrap));
9230
+
9231
+ /**
9232
+ * @callback ChainedMapRange
9233
+ * @param {Number} inLow - Input range minimum
9234
+ * @param {Number} inHigh - Input range maximum
9235
+ * @param {Number} outLow - Output range minimum
9236
+ * @param {Number} outHigh - Output range maximum
9237
+ * @return {ChainableUtil}
9238
+ */
9239
+ const mapRange = /** @type {typeof numberUtils.mapRange & ChainedMapRange} */(makeChainable('mapRange', numberUtils.mapRange));
9240
+
9241
+ /**
9242
+ * @callback ChainedDegToRad
9243
+ * @return {ChainableUtil}
9244
+ */
9245
+ const degToRad = /** @type {typeof numberUtils.degToRad & ChainedDegToRad} */(makeChainable('degToRad', numberUtils.degToRad));
9246
+
9247
+ /**
9248
+ * @callback ChainedRadToDeg
9249
+ * @return {ChainableUtil}
9250
+ */
9251
+ const radToDeg = /** @type {typeof numberUtils.radToDeg & ChainedRadToDeg} */(makeChainable('radToDeg', numberUtils.radToDeg));
9252
+
9253
+ /**
9254
+ * @callback ChainedSnap
9255
+ * @param {Number|Array<Number>} increment - Step size or array of snap points
9256
+ * @return {ChainableUtil}
9257
+ */
9258
+ const snap = /** @type {typeof numberUtils.snap & ChainedSnap} */(makeChainable('snap', numberUtils.snap));
9259
+
9260
+ /**
9261
+ * @callback ChainedClamp
9262
+ * @param {Number} min - Minimum boundary
9263
+ * @param {Number} max - Maximum boundary
9264
+ * @return {ChainableUtil}
9265
+ */
9266
+ const clamp = /** @type {typeof numberUtils.clamp & ChainedClamp} */(makeChainable('clamp', numberUtils.clamp));
9267
+
9268
+ /**
9269
+ * @callback ChainedRound
9270
+ * @param {Number} decimalLength - Number of decimal places
9271
+ * @return {ChainableUtil}
9272
+ */
9273
+ const round = /** @type {typeof numberUtils.round & ChainedRound} */(makeChainable('round', numberUtils.round));
9274
+
9275
+ /**
9276
+ * @callback ChainedLerp
9277
+ * @param {Number} start - Starting value
9278
+ * @param {Number} end - Ending value
9279
+ * @return {ChainableUtil}
9280
+ */
9281
+ const lerp = /** @type {typeof numberUtils.lerp & ChainedLerp} */(makeChainable('lerp', numberUtils.lerp, 1));
9282
+
9283
+ /**
9284
+ * @callback ChainedDamp
9285
+ * @param {Number} start - Starting value
9286
+ * @param {Number} end - Target value
9287
+ * @param {Number} deltaTime - Delta time in ms
9288
+ * @return {ChainableUtil}
9289
+ */
9290
+ const damp = /** @type {typeof numberUtils.damp & ChainedDamp} */(makeChainable('damp', numberUtils.damp, 1));
9291
+
9292
+ /**
9293
+ * Generate a random number between optional min and max (inclusive) and decimal precision
9294
+ *
9295
+ * @callback RandomNumberGenerator
9296
+ * @param {Number} [min=0] - The minimum value (inclusive)
9297
+ * @param {Number} [max=1] - The maximum value (inclusive)
9298
+ * @param {Number} [decimalLength=0] - Number of decimal places to round to
9299
+ * @return {Number} A random number between min and max
9300
+ */
9301
+
9302
+ /**
9303
+ * Generates a random number between min and max (inclusive) with optional decimal precision
9304
+ *
9305
+ * @type {RandomNumberGenerator}
9306
+ */
9307
+ const random = (min = 0, max = 1, decimalLength = 0) => {
9308
+ const m = 10 ** decimalLength;
9309
+ return Math.floor((Math.random() * (max - min + (1 / m)) + min) * m) / m;
9310
+ };
9311
+
9312
+ let _seed = 0;
9313
+
9314
+ /**
9315
+ * Creates a seeded pseudorandom number generator function
9316
+ *
9317
+ * @param {Number} [seed] - The seed value for the random number generator
9318
+ * @param {Number} [seededMin=0] - The minimum default value (inclusive) of the returned function
9319
+ * @param {Number} [seededMax=1] - The maximum default value (inclusive) of the returned function
9320
+ * @param {Number} [seededDecimalLength=0] - Default number of decimal places to round to of the returned function
9321
+ * @return {RandomNumberGenerator} A function to generate a random number between optional min and max (inclusive) and decimal precision
9322
+ */
9323
+ const createSeededRandom = (seed, seededMin = 0, seededMax = 1, seededDecimalLength = 0) => {
9324
+ let t = seed === undefined ? _seed++ : seed;
9325
+ return (min = seededMin, max = seededMax, decimalLength = seededDecimalLength) => {
9326
+ t += 0x6D2B79F5;
9327
+ t = Math.imul(t ^ t >>> 15, t | 1);
9328
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
9329
+ const m = 10 ** decimalLength;
9330
+ return Math.floor(((((t ^ t >>> 14) >>> 0) / 4294967296) * (max - min + (1 / m)) + min) * m) / m;
9331
+ }
9332
+ };
9333
+
9334
+ /**
9335
+ * Picks a random element from an array or a string
9336
+ *
9337
+ * @template T
9338
+ * @param {String|Array<T>} items - The array or string to pick from
9339
+ * @return {String|T} A random element from the array or character from the string
9340
+ */
9341
+ const randomPick = items => items[random(0, items.length - 1)];
9342
+
9343
+ /**
9344
+ * Shuffles an array in-place using the Fisher-Yates algorithm
9345
+ * Adapted from https://bost.ocks.org/mike/shuffle/
9346
+ *
9347
+ * @param {Array} items - The array to shuffle (will be modified in-place)
9348
+ * @return {Array} The same array reference, now shuffled
9349
+ */
9350
+ const shuffle = items => {
9351
+ let m = items.length, t, i;
9352
+ while (m) { i = random(0, --m); t = items[m]; items[m] = items[i]; items[i] = t; }
9353
+ return items;
9354
+ };
9355
+
9356
+
9357
+
9358
+
9359
+
9360
+ /**
9361
+ * @overload
9362
+ * @param {Number} val
9363
+ * @param {StaggerParams} [params]
9364
+ * @return {StaggerFunction<Number>}
9365
+ */
9366
+ /**
9367
+ * @overload
9368
+ * @param {String} val
9369
+ * @param {StaggerParams} [params]
9370
+ * @return {StaggerFunction<String>}
9371
+ */
9372
+ /**
9373
+ * @overload
9374
+ * @param {[Number, Number]} val
9375
+ * @param {StaggerParams} [params]
9376
+ * @return {StaggerFunction<Number>}
9377
+ */
9378
+ /**
9379
+ * @overload
9380
+ * @param {[String, String]} val
9381
+ * @param {StaggerParams} [params]
9382
+ * @return {StaggerFunction<String>}
9383
+ */
9384
+ /**
9385
+ * @param {Number|String|[Number, Number]|[String, String]} val The staggered value or range
9386
+ * @param {StaggerParams} [params] The stagger parameters
9387
+ * @return {StaggerFunction<Number|String>}
9388
+ */
9389
+ const stagger = (val, params = {}) => {
9390
+ let values = [];
9391
+ let maxValue = 0;
9392
+ const from = params.from;
9393
+ const reversed = params.reversed;
9394
+ const ease = params.ease;
9395
+ const hasEasing = !isUnd(ease);
9396
+ const hasSpring = hasEasing && !isUnd(/** @type {Spring} */(ease).ease);
9397
+ const staggerEase = hasSpring ? /** @type {Spring} */(ease).ease : hasEasing ? parseEase(ease) : null;
9398
+ const grid = params.grid;
9399
+ const axis = params.axis;
9400
+ const customTotal = params.total;
9401
+ const fromFirst = isUnd(from) || from === 0 || from === 'first';
9402
+ const fromCenter = from === 'center';
9403
+ const fromLast = from === 'last';
9404
+ const fromRandom = from === 'random';
9405
+ const isRange = isArr(val);
9406
+ const useProp = params.use;
9407
+ const val1 = isRange ? parseNumber(val[0]) : parseNumber(val);
9408
+ const val2 = isRange ? parseNumber(val[1]) : 0;
9409
+ const unitMatch = unitsExecRgx.exec((isRange ? val[1] : val) + emptyString);
9410
+ const start = params.start || 0 + (isRange ? val1 : 0);
9411
+ let fromIndex = fromFirst ? 0 : isNum(from) ? from : 0;
9412
+ return (target, i, t, tl) => {
9413
+ const [ registeredTarget ] = registerTargets(target);
9414
+ const total = isUnd(customTotal) ? t : customTotal;
9415
+ const customIndex = !isUnd(useProp) ? isFnc(useProp) ? useProp(registeredTarget, i, total) : getOriginalAnimatableValue(registeredTarget, useProp) : false;
9416
+ const staggerIndex = isNum(customIndex) || isStr(customIndex) && isNum(+customIndex) ? +customIndex : i;
9417
+ if (fromCenter) fromIndex = (total - 1) / 2;
9418
+ if (fromLast) fromIndex = total - 1;
9419
+ if (!values.length) {
9420
+ for (let index = 0; index < total; index++) {
9421
+ if (!grid) {
9422
+ values.push(abs(fromIndex - index));
9423
+ } else {
9424
+ const fromX = !fromCenter ? fromIndex % grid[0] : (grid[0] - 1) / 2;
9425
+ const fromY = !fromCenter ? floor(fromIndex / grid[0]) : (grid[1] - 1) / 2;
9426
+ const toX = index % grid[0];
9427
+ const toY = floor(index / grid[0]);
9428
+ const distanceX = fromX - toX;
9429
+ const distanceY = fromY - toY;
9430
+ let value = sqrt(distanceX * distanceX + distanceY * distanceY);
9431
+ if (axis === 'x') value = -distanceX;
9432
+ if (axis === 'y') value = -distanceY;
9433
+ values.push(value);
9434
+ }
9435
+ maxValue = max(...values);
9436
+ }
9437
+ if (staggerEase) values = values.map(val => staggerEase(val / maxValue) * maxValue);
9438
+ if (reversed) values = values.map(val => axis ? (val < 0) ? val * -1 : -val : abs(maxValue - val));
9439
+ if (fromRandom) values = shuffle(values);
9440
+ }
9441
+ const spacing = isRange ? (val2 - val1) / maxValue : val1;
9442
+ const offset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */(start);
9443
+ /** @type {String|Number} */
9444
+ let output = offset + ((spacing * round$1(values[staggerIndex], 2)) || 0);
9445
+ if (params.modifier) output = params.modifier(output);
9446
+ if (unitMatch) output = `${output}${unitMatch[2]}`;
9447
+ return output;
9448
+ }
8330
9449
  };
8331
9450
 
8332
- var index = /*#__PURE__*/Object.freeze({
9451
+ var index$2 = /*#__PURE__*/Object.freeze({
8333
9452
  __proto__: null,
8334
- TextSplitter: TextSplitter,
8335
- split: split,
8336
- splitText: splitText
9453
+ $: registerTargets,
9454
+ clamp: clamp,
9455
+ cleanInlineStyles: cleanInlineStyles,
9456
+ createSeededRandom: createSeededRandom,
9457
+ damp: damp,
9458
+ degToRad: degToRad,
9459
+ get: get,
9460
+ keepTime: keepTime,
9461
+ lerp: lerp,
9462
+ mapRange: mapRange,
9463
+ padEnd: padEnd,
9464
+ padStart: padStart,
9465
+ radToDeg: radToDeg,
9466
+ random: random,
9467
+ randomPick: randomPick,
9468
+ remove: remove,
9469
+ round: round,
9470
+ roundPad: roundPad,
9471
+ set: set,
9472
+ shuffle: shuffle,
9473
+ snap: snap,
9474
+ stagger: stagger,
9475
+ sync: sync,
9476
+ wrap: wrap
8337
9477
  });
8338
9478
 
8339
9479
 
8340
9480
 
8341
-
8342
-
8343
-
8344
-
8345
- /**
8346
- * Converts an easing function into a valid CSS linear() timing function string
8347
- * @param {EasingFunction} fn
8348
- * @param {number} [samples=100]
8349
- * @returns {string} CSS linear() timing function
8350
- */
8351
- const easingToLinear = (fn, samples = 100) => {
8352
- const points = [];
8353
- for (let i = 0; i <= samples; i++) points.push(round$1(fn(i / samples), 4));
8354
- return `linear(${points.join(', ')})`;
8355
- };
8356
-
8357
- const WAAPIEasesLookups = {};
8358
-
8359
9481
  /**
8360
- * @param {EasingParam} ease
8361
- * @return {String}
9482
+ * @param {TargetsParam} path
9483
+ * @return {SVGGeometryElement|void}
8362
9484
  */
8363
- const parseWAAPIEasing = (ease) => {
8364
- let parsedEase = WAAPIEasesLookups[ease];
8365
- if (parsedEase) return parsedEase;
8366
- parsedEase = 'linear';
8367
- if (isStr(ease)) {
8368
- if (
8369
- stringStartsWith(ease, 'linear') ||
8370
- stringStartsWith(ease, 'cubic-') ||
8371
- stringStartsWith(ease, 'steps') ||
8372
- stringStartsWith(ease, 'ease')
8373
- ) {
8374
- parsedEase = ease;
8375
- } else if (stringStartsWith(ease, 'cubicB')) {
8376
- parsedEase = toLowerCase(ease);
8377
- } else {
8378
- const parsed = parseEaseString(ease);
8379
- if (isFnc(parsed)) parsedEase = parsed === none ? 'linear' : easingToLinear(parsed);
8380
- }
8381
- // Only cache string based easing name, otherwise function arguments get lost
8382
- WAAPIEasesLookups[ease] = parsedEase;
8383
- } else if (isFnc(ease)) {
8384
- const easing = easingToLinear(ease);
8385
- if (easing) parsedEase = easing;
8386
- } else if (/** @type {Spring} */(ease).ease) {
8387
- parsedEase = easingToLinear(/** @type {Spring} */(ease).ease);
8388
- }
8389
- return parsedEase;
9485
+ const getPath = path => {
9486
+ const parsedTargets = parseTargets(path);
9487
+ const $parsedSvg = /** @type {SVGGeometryElement} */(parsedTargets[0]);
9488
+ if (!$parsedSvg || !isSvg($parsedSvg)) return console.warn(`${path} is not a valid SVGGeometryElement`);
9489
+ return $parsedSvg;
8390
9490
  };
8391
9491
 
8392
- const transformsShorthands = ['x', 'y', 'z'];
8393
- const commonDefaultPXProperties = [
8394
- 'perspective',
8395
- 'width',
8396
- 'height',
8397
- 'margin',
8398
- 'padding',
8399
- 'top',
8400
- 'right',
8401
- 'bottom',
8402
- 'left',
8403
- 'borderWidth',
8404
- 'fontSize',
8405
- 'borderRadius',
8406
- ...transformsShorthands
8407
- ];
8408
9492
 
8409
- const validIndividualTransforms = /*#__PURE__*/ (() => [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))])();
8410
9493
 
8411
- let transformsPropertiesRegistered = null;
9494
+ // Motion path animation
8412
9495
 
8413
9496
  /**
8414
- * @param {String} propName
8415
- * @param {WAAPIKeyframeValue} value
8416
- * @param {DOMTarget} $el
8417
- * @param {Number} i
8418
- * @param {Number} targetsLength
8419
- * @return {String}
9497
+ * @param {SVGGeometryElement} $path
9498
+ * @param {Number} totalLength
9499
+ * @param {Number} progress
9500
+ * @param {Number} lookup
9501
+ * @param {Boolean} shouldClamp
9502
+ * @return {DOMPoint}
8420
9503
  */
8421
- const normalizeTweenValue = (propName, value, $el, i, targetsLength) => {
8422
- // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables
8423
- let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, targetsLength);
8424
- if (!isNum(v)) return v;
8425
- if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`;
8426
- if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`;
8427
- return `${v}`;
9504
+ const getPathPoint = ($path, totalLength, progress, lookup, shouldClamp) => {
9505
+ const point = progress + lookup;
9506
+ const pointOnPath = shouldClamp
9507
+ ? Math.max(0, Math.min(point, totalLength)) // Clamp between 0 and totalLength
9508
+ : (point % totalLength + totalLength) % totalLength; // Wrap around
9509
+ return $path.getPointAtLength(pointOnPath);
8428
9510
  };
8429
9511
 
8430
9512
  /**
8431
- * @param {DOMTarget} $el
8432
- * @param {String} propName
8433
- * @param {WAAPIKeyframeValue} from
8434
- * @param {WAAPIKeyframeValue} to
8435
- * @param {Number} i
8436
- * @param {Number} targetsLength
8437
- * @return {WAAPITweenValue}
9513
+ * @param {SVGGeometryElement} $path
9514
+ * @param {String} pathProperty
9515
+ * @param {Number} [offset=0]
9516
+ * @return {FunctionValue}
8438
9517
  */
8439
- const parseIndividualTweenValue = ($el, propName, from, to, i, targetsLength) => {
8440
- /** @type {WAAPITweenValue} */
8441
- let tweenValue = '0';
8442
- const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, targetsLength) : getComputedStyle($el)[propName];
8443
- if (!isUnd(from)) {
8444
- const computedFrom = normalizeTweenValue(propName, from, $el, i, targetsLength);
8445
- tweenValue = [computedFrom, computedTo];
8446
- } else {
8447
- tweenValue = isArr(to) ? to.map((/** @type {any} */v) => normalizeTweenValue(propName, v, $el, i, targetsLength)) : computedTo;
9518
+ const getPathProgess = ($path, pathProperty, offset = 0) => {
9519
+ return $el => {
9520
+ const totalLength = +($path.getTotalLength());
9521
+ const inSvg = $el[isSvgSymbol];
9522
+ const ctm = $path.getCTM();
9523
+ const shouldClamp = offset === 0;
9524
+ /** @type {TweenObjectValue} */
9525
+ return {
9526
+ from: 0,
9527
+ to: totalLength,
9528
+ /** @type {TweenModifier} */
9529
+ modifier: progress => {
9530
+ const offsetLength = offset * totalLength;
9531
+ const newProgress = progress + offsetLength;
9532
+ if (pathProperty === 'a') {
9533
+ const p0 = getPathPoint($path, totalLength, newProgress, -1, shouldClamp);
9534
+ const p1 = getPathPoint($path, totalLength, newProgress, 1, shouldClamp);
9535
+ return atan2(p1.y - p0.y, p1.x - p0.x) * 180 / PI;
9536
+ } else {
9537
+ const p = getPathPoint($path, totalLength, newProgress, 0, shouldClamp);
9538
+ return pathProperty === 'x' ?
9539
+ inSvg || !ctm ? p.x : p.x * ctm.a + p.y * ctm.c + ctm.e :
9540
+ inSvg || !ctm ? p.y : p.x * ctm.b + p.y * ctm.d + ctm.f
9541
+ }
9542
+ }
9543
+ }
8448
9544
  }
8449
- return tweenValue;
8450
9545
  };
8451
9546
 
8452
- class WAAPIAnimation {
8453
9547
  /**
8454
- * @param {DOMTargetsParam} targets
8455
- * @param {WAAPIAnimationParams} params
9548
+ * @param {TargetsParam} path
9549
+ * @param {Number} [offset=0]
8456
9550
  */
8457
- constructor(targets, params) {
8458
-
8459
- if (scope.current) scope.current.register(this);
8460
-
8461
- // Skip the registration and fallback to no animation in case CSS.registerProperty is not supported
8462
- if (isNil(transformsPropertiesRegistered)) {
8463
- if (isBrowser && (isUnd(CSS) || !Object.hasOwnProperty.call(CSS, 'registerProperty'))) {
8464
- transformsPropertiesRegistered = false;
8465
- } else {
8466
- validTransforms.forEach(t => {
8467
- const isSkew = stringStartsWith(t, 'skew');
8468
- const isScale = stringStartsWith(t, 'scale');
8469
- const isRotate = stringStartsWith(t, 'rotate');
8470
- const isTranslate = stringStartsWith(t, 'translate');
8471
- const isAngle = isRotate || isSkew;
8472
- const syntax = isAngle ? '<angle>' : isScale ? "<number>" : isTranslate ? "<length-percentage>" : "*";
8473
- try {
8474
- CSS.registerProperty({
8475
- name: '--' + t,
8476
- syntax,
8477
- inherits: false,
8478
- initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0',
8479
- });
8480
- } catch {} });
8481
- transformsPropertiesRegistered = true;
8482
- }
8483
- }
8484
-
8485
- const parsedTargets = registerTargets(targets);
8486
- const targetsLength = parsedTargets.length;
8487
-
8488
- if (!targetsLength) {
8489
- console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`);
8490
- }
8491
-
8492
- const ease = setValue(params.ease, parseWAAPIEasing(globals.defaults.ease));
8493
- const spring = /** @type {Spring} */(ease).ease && ease;
8494
- const autoplay = setValue(params.autoplay, globals.defaults.autoplay);
8495
- const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false;
8496
- const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true;
8497
- const reversed = params.reversed && /** @type {Boolean} */(params.reversed) === true;
8498
- const loop = setValue(params.loop, globals.defaults.loop);
8499
- const iterations = /** @type {Number} */((loop === true || loop === Infinity) ? Infinity : isNum(loop) ? loop + 1 : 1);
8500
- /** @type {PlaybackDirection} */
8501
- const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal';
8502
- /** @type {FillMode} */
8503
- const fill = 'both'; // We use 'both' here because the animation can be reversed during playback
8504
- /** @type {String} */
8505
- const easing = parseWAAPIEasing(ease);
8506
- const timeScale = (globals.timeScale === 1 ? 1 : K);
8507
-
8508
- /** @type {DOMTargetsArray}] */
8509
- this.targets = parsedTargets;
8510
- /** @type {Array<globalThis.Animation>}] */
8511
- this.animations = [];
8512
- /** @type {globalThis.Animation}] */
8513
- this.controlAnimation = null;
8514
- /** @type {Callback<this>} */
8515
- this.onComplete = params.onComplete || /** @type {Callback<WAAPIAnimation>} */(/** @type {unknown} */(globals.defaults.onComplete));
8516
- /** @type {Number} */
8517
- this.duration = 0;
8518
- /** @type {Boolean} */
8519
- this.muteCallbacks = false;
8520
- /** @type {Boolean} */
8521
- this.completed = false;
8522
- /** @type {Boolean} */
8523
- this.paused = !autoplay || scroll !== false;
8524
- /** @type {Boolean} */
8525
- this.reversed = reversed;
8526
- /** @type {Boolean} */
8527
- this.persist = setValue(params.persist, globals.defaults.persist);
8528
- /** @type {Boolean|ScrollObserver} */
8529
- this.autoplay = autoplay;
8530
- /** @type {Number} */
8531
- this._speed = setValue(params.playbackRate, globals.defaults.playbackRate);
8532
- /** @type {Function} */
8533
- this._resolve = noop; // Used by .then()
8534
- /** @type {Number} */
8535
- this._completed = 0;
8536
- /** @type {Array.<Object>} */
8537
- this._inlineStyles = [];
8538
-
8539
- parsedTargets.forEach(($el, i) => {
9551
+ const createMotionPath = (path, offset = 0) => {
9552
+ const $path = getPath(path);
9553
+ if (!$path) return;
9554
+ return {
9555
+ translateX: getPathProgess($path, 'x', offset),
9556
+ translateY: getPathProgess($path, 'y', offset),
9557
+ rotate: getPathProgess($path, 'a', offset),
9558
+ }
9559
+ };
8540
9560
 
8541
- const cachedTransforms = $el[transformsSymbol];
8542
- const hasIndividualTransforms = validIndividualTransforms.some(t => params.hasOwnProperty(t));
8543
- const elStyle = $el.style;
8544
- const inlineStyles = this._inlineStyles[i] = {};
8545
9561
 
8546
- /** @type {Number} */
8547
- const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, targetsLength)) * timeScale;
8548
- /** @type {Number} */
8549
- const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, targetsLength) * timeScale;
8550
- /** @type {CompositeOperation} */
8551
- const composite = /** @type {CompositeOperation} */(setValue(params.composition, 'replace'));
8552
9562
 
8553
- for (let name in params) {
8554
- if (!isKey(name)) continue;
8555
- /** @type {PropertyIndexedKeyframes} */
8556
- const keyframes = {};
8557
- /** @type {KeyframeAnimationOptions} */
8558
- const tweenParams = { iterations, direction, fill, easing, duration, delay, composite };
8559
- const propertyValue = params[name];
8560
- const individualTransformProperty = hasIndividualTransforms ? validTransforms.includes(name) ? name : shortTransforms.get(name) : false;
9563
+ /**
9564
+ * @param {SVGGeometryElement} [$el]
9565
+ * @return {Number}
9566
+ */
9567
+ const getScaleFactor = $el => {
9568
+ let scaleFactor = 1;
9569
+ if ($el && $el.getCTM) {
9570
+ const ctm = $el.getCTM();
9571
+ if (ctm) {
9572
+ const scaleX = sqrt(ctm.a * ctm.a + ctm.b * ctm.b);
9573
+ const scaleY = sqrt(ctm.c * ctm.c + ctm.d * ctm.d);
9574
+ scaleFactor = (scaleX + scaleY) / 2;
9575
+ }
9576
+ }
9577
+ return scaleFactor;
9578
+ };
8561
9579
 
8562
- const styleName = individualTransformProperty ? 'transform' : name;
8563
- if (!inlineStyles[styleName]) {
8564
- inlineStyles[styleName] = elStyle[styleName];
8565
- }
9580
+ /**
9581
+ * Creates a proxy that wraps an SVGGeometryElement and adds drawing functionality.
9582
+ * @param {SVGGeometryElement} $el - The SVG element to transform into a drawable
9583
+ * @param {number} start - Starting position (0-1)
9584
+ * @param {number} end - Ending position (0-1)
9585
+ * @return {DrawableSVGGeometry} - Returns a proxy that preserves the original element's type with additional 'draw' attribute functionality
9586
+ */
9587
+ const createDrawableProxy = ($el, start, end) => {
9588
+ const pathLength = K;
9589
+ const computedStyles = getComputedStyle($el);
9590
+ const strokeLineCap = computedStyles.strokeLinecap;
9591
+ // @ts-ignore
9592
+ const $scalled = computedStyles.vectorEffect === 'non-scaling-stroke' ? $el : null;
9593
+ let currentCap = strokeLineCap;
8566
9594
 
8567
- let parsedPropertyValue;
8568
- if (isObj(propertyValue)) {
8569
- const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue);
8570
- const tweenOptionsEase = setValue(tweenOptions.ease, ease);
8571
- const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase;
8572
- const to = /** @type {WAAPITweenOptions} */(tweenOptions).to;
8573
- const from = /** @type {WAAPITweenOptions} */(tweenOptions).from;
8574
- /** @type {Number} */
8575
- tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, targetsLength)) * timeScale;
8576
- /** @type {Number} */
8577
- tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, targetsLength) * timeScale;
8578
- /** @type {CompositeOperation} */
8579
- tweenParams.composite = /** @type {CompositeOperation} */(setValue(tweenOptions.composition, composite));
8580
- /** @type {String} */
8581
- tweenParams.easing = parseWAAPIEasing(tweenOptionsEase);
8582
- parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, targetsLength);
8583
- if (individualTransformProperty) {
8584
- keyframes[`--${individualTransformProperty}`] = parsedPropertyValue;
8585
- cachedTransforms[individualTransformProperty] = parsedPropertyValue;
8586
- } else {
8587
- keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, targetsLength);
8588
- }
8589
- addWAAPIAnimation(this, $el, name, keyframes, tweenParams);
8590
- if (!isUnd(from)) {
8591
- if (!individualTransformProperty) {
8592
- elStyle[name] = keyframes[name][0];
8593
- } else {
8594
- const key = `--${individualTransformProperty}`;
8595
- elStyle.setProperty(key, keyframes[key][0]);
9595
+ const proxy = new Proxy($el, {
9596
+ get(target, property) {
9597
+ const value = target[property];
9598
+ if (property === proxyTargetSymbol) return target;
9599
+ if (property === 'setAttribute') {
9600
+ return (...args) => {
9601
+ if (args[0] === 'draw') {
9602
+ const value = args[1];
9603
+ const values = value.split(' ');
9604
+ const v1 = +values[0];
9605
+ const v2 = +values[1];
9606
+ // TOTO: Benchmark if performing two slices is more performant than one split
9607
+ // const spaceIndex = value.indexOf(' ');
9608
+ // const v1 = round(+value.slice(0, spaceIndex), precision);
9609
+ // const v2 = round(+value.slice(spaceIndex + 1), precision);
9610
+ const scaleFactor = getScaleFactor($scalled);
9611
+ const os = v1 * -pathLength * scaleFactor;
9612
+ const d1 = (v2 * pathLength * scaleFactor) + os;
9613
+ const d2 = (pathLength * scaleFactor +
9614
+ ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10 * scaleFactor) - d1);
9615
+ if (strokeLineCap !== 'butt') {
9616
+ const newCap = v1 === v2 ? 'butt' : strokeLineCap;
9617
+ if (currentCap !== newCap) {
9618
+ target.style.strokeLinecap = `${newCap}`;
9619
+ currentCap = newCap;
9620
+ }
8596
9621
  }
9622
+ target.setAttribute('stroke-dashoffset', `${os}`);
9623
+ target.setAttribute('stroke-dasharray', `${d1} ${d2}`);
8597
9624
  }
8598
- } else {
8599
- parsedPropertyValue = isArr(propertyValue) ?
8600
- propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, targetsLength)) :
8601
- normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, targetsLength);
8602
- if (individualTransformProperty) {
8603
- keyframes[`--${individualTransformProperty}`] = parsedPropertyValue;
8604
- cachedTransforms[individualTransformProperty] = parsedPropertyValue;
8605
- } else {
8606
- keyframes[name] = parsedPropertyValue;
8607
- }
8608
- addWAAPIAnimation(this, $el, name, keyframes, tweenParams);
8609
- }
8610
- }
8611
- if (hasIndividualTransforms) {
8612
- let transforms = emptyString;
8613
- for (let t in cachedTransforms) {
8614
- transforms += `${transformsFragmentStrings[t]}var(--${t})) `;
8615
- }
8616
- elStyle.transform = transforms;
9625
+ return Reflect.apply(value, target, args);
9626
+ };
8617
9627
  }
8618
- });
8619
9628
 
8620
- if (scroll) {
8621
- /** @type {ScrollObserver} */(this.autoplay).link(this);
9629
+ if (isFnc(value)) {
9630
+ return (...args) => Reflect.apply(value, target, args);
9631
+ } else {
9632
+ return value;
9633
+ }
8622
9634
  }
9635
+ });
9636
+
9637
+ if ($el.getAttribute('pathLength') !== `${pathLength}`) {
9638
+ $el.setAttribute('pathLength', `${pathLength}`);
9639
+ proxy.setAttribute('draw', `${start} ${end}`);
8623
9640
  }
8624
9641
 
8625
- /**
8626
- * @callback forEachCallback
8627
- * @param {globalThis.Animation} animation
8628
- */
9642
+ return /** @type {DrawableSVGGeometry} */(proxy);
9643
+ };
8629
9644
 
8630
- /**
8631
- * @param {forEachCallback|String} callback
8632
- * @return {this}
8633
- */
8634
- forEach(callback) {
8635
- const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback;
8636
- this.animations.forEach(cb);
8637
- return this;
8638
- }
9645
+ /**
9646
+ * Creates drawable proxies for multiple SVG elements.
9647
+ * @param {TargetsParam} selector - CSS selector, SVG element, or array of elements and selectors
9648
+ * @param {number} [start=0] - Starting position (0-1)
9649
+ * @param {number} [end=0] - Ending position (0-1)
9650
+ * @return {Array<DrawableSVGGeometry>} - Array of proxied elements with drawing functionality
9651
+ */
9652
+ const createDrawable = (selector, start = 0, end = 0) => {
9653
+ const els = parseTargets(selector);
9654
+ return els.map($el => createDrawableProxy(
9655
+ /** @type {SVGGeometryElement} */($el),
9656
+ start,
9657
+ end
9658
+ ));
9659
+ };
8639
9660
 
8640
- get speed() {
8641
- return this._speed;
8642
- }
8643
9661
 
8644
- set speed(speed) {
8645
- this._speed = +speed;
8646
- this.forEach(anim => anim.playbackRate = speed);
8647
- }
8648
9662
 
8649
- get currentTime() {
8650
- const controlAnimation = this.controlAnimation;
8651
- const timeScale = globals.timeScale;
8652
- return this.completed ? this.duration : controlAnimation ? +controlAnimation.currentTime * (timeScale === 1 ? 1 : timeScale) : 0;
9663
+ /**
9664
+ * @param {TargetsParam} path2
9665
+ * @param {Number} [precision]
9666
+ * @return {FunctionValue}
9667
+ */
9668
+ const morphTo = (path2, precision = .33) => ($path1) => {
9669
+ const tagName1 = ($path1.tagName || '').toLowerCase();
9670
+ if (!tagName1.match(/^(path|polygon|polyline)$/)) {
9671
+ throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use <path>, <polygon> or <polyline>.`);
9672
+ }
9673
+ const $path2 = /** @type {SVGGeometryElement} */(getPath(path2));
9674
+ if (!$path2) {
9675
+ throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing <path>, <polygon> or <polyline> SVG element.");
9676
+ }
9677
+ const tagName2 = ($path2.tagName || '').toLowerCase();
9678
+ if (!tagName2.match(/^(path|polygon|polyline)$/)) {
9679
+ throw new Error(`Can't morph a <${$path2.tagName}> SVG element. Use <path>, <polygon> or <polyline>.`);
8653
9680
  }
9681
+ const isPath = $path1.tagName === 'path';
9682
+ const separator = isPath ? ' ' : ',';
9683
+ const previousPoints = $path1[morphPointsSymbol];
9684
+ if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints);
8654
9685
 
8655
- set currentTime(time) {
8656
- const t = time * (globals.timeScale === 1 ? 1 : K);
8657
- this.forEach(anim => {
8658
- // Make sure the animation playState is not 'paused' in order to properly trigger an onfinish callback.
8659
- // The "paused" play state supersedes the "finished" play state; if the animation is both paused and finished, the "paused" state is the one that will be reported.
8660
- // https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish_event
8661
- // This is not needed for persisting animations since they never finish.
8662
- if (!this.persist && t >= this.duration) anim.play();
8663
- anim.currentTime = t;
8664
- });
9686
+ let v1 = '', v2 = '';
9687
+
9688
+ if (!precision) {
9689
+ v1 = $path1.getAttribute(isPath ? 'd' : 'points');
9690
+ v2 = $path2.getAttribute(isPath ? 'd' : 'points');
9691
+ } else {
9692
+ const length1 = /** @type {SVGGeometryElement} */($path1).getTotalLength();
9693
+ const length2 = $path2.getTotalLength();
9694
+ const maxPoints = Math.max(Math.ceil(length1 * precision), Math.ceil(length2 * precision));
9695
+ for (let i = 0; i < maxPoints; i++) {
9696
+ const t = i / (maxPoints - 1);
9697
+ const pointOnPath1 = /** @type {SVGGeometryElement} */($path1).getPointAtLength(length1 * t);
9698
+ const pointOnPath2 = $path2.getPointAtLength(length2 * t);
9699
+ const prefix = isPath ? (i === 0 ? 'M' : 'L') : '';
9700
+ v1 += prefix + round$1(pointOnPath1.x, 3) + separator + pointOnPath1.y + ' ';
9701
+ v2 += prefix + round$1(pointOnPath2.x, 3) + separator + pointOnPath2.y + ' ';
9702
+ }
8665
9703
  }
8666
9704
 
8667
- get progress() {
8668
- return this.currentTime / this.duration;
8669
- }
9705
+ $path1[morphPointsSymbol] = v2;
9706
+
9707
+ return [v1, v2];
9708
+ };
9709
+
9710
+ var index$1 = /*#__PURE__*/Object.freeze({
9711
+ __proto__: null,
9712
+ createDrawable: createDrawable,
9713
+ createMotionPath: createMotionPath,
9714
+ morphTo: morphTo
9715
+ });
9716
+
9717
+
9718
+
9719
+ const segmenter = (typeof Intl !== 'undefined') && Intl.Segmenter;
9720
+ const valueRgx = /\{value\}/g;
9721
+ const indexRgx = /\{i\}/g;
9722
+ const whiteSpaceGroupRgx = /(\s+)/;
9723
+ const whiteSpaceRgx = /^\s+$/;
9724
+ const lineType = 'line';
9725
+ const wordType = 'word';
9726
+ const charType = 'char';
9727
+ const dataLine = `data-line`;
9728
+
9729
+ /**
9730
+ * @typedef {Object} Segment
9731
+ * @property {String} segment
9732
+ * @property {Boolean} [isWordLike]
9733
+ */
8670
9734
 
8671
- set progress(progress) {
8672
- this.forEach(anim => anim.currentTime = progress * this.duration || 0);
8673
- }
9735
+ /**
9736
+ * @typedef {Object} Segmenter
9737
+ * @property {function(String): Iterable<Segment>} segment
9738
+ */
8674
9739
 
8675
- resume() {
8676
- if (!this.paused) return this;
8677
- this.paused = false;
8678
- // TODO: Store the current time, and seek back to the last position
8679
- return this.forEach('play');
8680
- }
9740
+ /** @type {Segmenter} */
9741
+ let wordSegmenter = null;
9742
+ /** @type {Segmenter} */
9743
+ let graphemeSegmenter = null;
9744
+ let $splitTemplate = null;
8681
9745
 
8682
- pause() {
8683
- if (this.paused) return this;
8684
- this.paused = true;
8685
- return this.forEach('pause');
8686
- }
9746
+ /**
9747
+ * @param {Segment} seg
9748
+ * @return {Boolean}
9749
+ */
9750
+ const isSegmentWordLike = seg => {
9751
+ return seg.isWordLike ||
9752
+ seg.segment === ' ' || // Consider spaces as words first, then handle them diffrently later
9753
+ isNum(+seg.segment); // Safari doesn't considers numbers as words
9754
+ };
8687
9755
 
8688
- alternate() {
8689
- this.reversed = !this.reversed;
8690
- this.forEach('reverse');
8691
- if (this.paused) this.forEach('pause');
8692
- return this;
8693
- }
9756
+ /**
9757
+ * @param {HTMLElement} $el
9758
+ */
9759
+ const setAriaHidden = $el => $el.setAttribute('aria-hidden', 'true');
8694
9760
 
8695
- play() {
8696
- if (this.reversed) this.alternate();
8697
- return this.resume();
8698
- }
9761
+ /**
9762
+ * @param {DOMTarget} $el
9763
+ * @param {String} type
9764
+ * @return {Array<HTMLElement>}
9765
+ */
9766
+ const getAllTopLevelElements = ($el, type) => [.../** @type {*} */($el.querySelectorAll(`[data-${type}]:not([data-${type}] [data-${type}])`))];
8699
9767
 
8700
- reverse() {
8701
- if (!this.reversed) this.alternate();
8702
- return this.resume();
9768
+ const debugColors = { line: '#00D672', word: '#FF4B4B', char: '#5A87FF' };
9769
+
9770
+ /**
9771
+ * @param {HTMLElement} $el
9772
+ */
9773
+ const filterEmptyElements = $el => {
9774
+ if (!$el.childElementCount && !$el.textContent.trim()) {
9775
+ const $parent = $el.parentElement;
9776
+ $el.remove();
9777
+ if ($parent) filterEmptyElements($parent);
8703
9778
  }
9779
+ };
8704
9780
 
8705
- /**
8706
- * @param {Number} time
8707
- * @param {Boolean} muteCallbacks
8708
- */
8709
- seek(time, muteCallbacks = false) {
8710
- if (muteCallbacks) this.muteCallbacks = true;
8711
- if (time < this.duration) this.completed = false;
8712
- this.currentTime = time;
8713
- this.muteCallbacks = false;
8714
- if (this.paused) this.pause();
8715
- return this;
9781
+ /**
9782
+ * @param {HTMLElement} $el
9783
+ * @param {Number} lineIndex
9784
+ * @param {Set<HTMLElement|Node>} bin
9785
+ * @returns {Set<HTMLElement|Node>}
9786
+ */
9787
+ const filterLineElements = ($el, lineIndex, bin) => {
9788
+ const dataLineAttr = $el.getAttribute(dataLine);
9789
+ if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') {
9790
+ bin.add($el);
9791
+ // Also remove adjacent whitespace-only text nodes
9792
+ const prev = $el.previousSibling;
9793
+ const next = $el.nextSibling;
9794
+ if (prev && prev.nodeType === 3 && whiteSpaceRgx.test(prev.textContent)) {
9795
+ bin.add(prev);
9796
+ }
9797
+ if (next && next.nodeType === 3 && whiteSpaceRgx.test(next.textContent)) {
9798
+ bin.add(next);
9799
+ }
8716
9800
  }
9801
+ let i = $el.childElementCount;
9802
+ while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin);
9803
+ return bin;
9804
+ };
8717
9805
 
8718
- restart() {
8719
- this.completed = false;
8720
- return this.seek(0, true).resume();
9806
+ /**
9807
+ * @param {'line'|'word'|'char'} type
9808
+ * @param {SplitTemplateParams} params
9809
+ * @return {String}
9810
+ */
9811
+ const generateTemplate = (type, params = {}) => {
9812
+ let template = ``;
9813
+ const classString = isStr(params.class) ? ` class="${params.class}"` : '';
9814
+ const cloneType = setValue(params.clone, false);
9815
+ const wrapType = setValue(params.wrap, false);
9816
+ const overflow = wrapType ? wrapType === true ? 'clip' : wrapType : cloneType ? 'clip' : false;
9817
+ if (wrapType) template += `<span${overflow ? ` style="overflow:${overflow};"` : ''}>`;
9818
+ template += `<span${classString}${cloneType ? ` style="position:relative;"` : ''} data-${type}="{i}">`;
9819
+ if (cloneType) {
9820
+ const left = cloneType === 'left' ? '-100%' : cloneType === 'right' ? '100%' : '0';
9821
+ const top = cloneType === 'top' ? '-100%' : cloneType === 'bottom' ? '100%' : '0';
9822
+ template += `<span>{value}</span>`;
9823
+ template += `<span inert style="position:absolute;top:${top};left:${left};white-space:nowrap;">{value}</span>`;
9824
+ } else {
9825
+ template += `{value}`;
8721
9826
  }
9827
+ template += `</span>`;
9828
+ if (wrapType) template += `</span>`;
9829
+ return template;
9830
+ };
8722
9831
 
8723
- commitStyles() {
8724
- return this.forEach('commitStyles');
9832
+ /**
9833
+ * @param {String|SplitFunctionValue} htmlTemplate
9834
+ * @param {Array<HTMLElement>} store
9835
+ * @param {Node|HTMLElement} node
9836
+ * @param {DocumentFragment} $parentFragment
9837
+ * @param {'line'|'word'|'char'} type
9838
+ * @param {Boolean} debug
9839
+ * @param {Number} lineIndex
9840
+ * @param {Number} [wordIndex]
9841
+ * @param {Number} [charIndex]
9842
+ * @return {HTMLElement}
9843
+ */
9844
+ const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, debug, lineIndex, wordIndex, charIndex) => {
9845
+ const isLine = type === lineType;
9846
+ const isChar = type === charType;
9847
+ const className = `_${type}_`;
9848
+ const template = isFnc(htmlTemplate) ? htmlTemplate(node) : htmlTemplate;
9849
+ const displayStyle = isLine ? 'block' : 'inline-block';
9850
+ $splitTemplate.innerHTML = template
9851
+ .replace(valueRgx, `<i class="${className}"></i>`)
9852
+ .replace(indexRgx, `${isChar ? charIndex : isLine ? lineIndex : wordIndex}`);
9853
+ const $content = $splitTemplate.content;
9854
+ const $highestParent = /** @type {HTMLElement} */($content.firstElementChild);
9855
+ const $split = /** @type {HTMLElement} */($content.querySelector(`[data-${type}]`)) || $highestParent;
9856
+ const $replacables = /** @type {NodeListOf<HTMLElement>} */($content.querySelectorAll(`i.${className}`));
9857
+ const replacablesLength = $replacables.length;
9858
+ if (replacablesLength) {
9859
+ $highestParent.style.display = displayStyle;
9860
+ $split.style.display = displayStyle;
9861
+ $split.setAttribute(dataLine, `${lineIndex}`);
9862
+ if (!isLine) {
9863
+ $split.setAttribute('data-word', `${wordIndex}`);
9864
+ if (isChar) $split.setAttribute('data-char', `${charIndex}`);
9865
+ }
9866
+ let i = replacablesLength;
9867
+ while (i--) {
9868
+ const $replace = $replacables[i];
9869
+ const $closestParent = $replace.parentElement;
9870
+ $closestParent.style.display = displayStyle;
9871
+ if (isLine) {
9872
+ $closestParent.innerHTML = /** @type {HTMLElement} */(node).innerHTML;
9873
+ } else {
9874
+ $closestParent.replaceChild(node.cloneNode(true), $replace);
9875
+ }
9876
+ }
9877
+ store.push($split);
9878
+ $parentFragment.appendChild($content);
9879
+ } else {
9880
+ console.warn(`The expression "{value}" is missing from the provided template.`);
8725
9881
  }
9882
+ if (debug) $highestParent.style.outline = `1px dotted ${debugColors[type]}`;
9883
+ return $highestParent;
9884
+ };
8726
9885
 
8727
- complete() {
8728
- return this.seek(this.duration);
9886
+ /**
9887
+ * A class that splits text into words and wraps them in span elements while preserving the original HTML structure.
9888
+ * @class
9889
+ */
9890
+ class TextSplitter {
9891
+ /**
9892
+ * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
9893
+ * @param {TextSplitterParams} [parameters]
9894
+ */
9895
+ constructor(target, parameters = {}) {
9896
+ // Only init segmenters when needed
9897
+ if (!wordSegmenter) wordSegmenter = segmenter ? new segmenter([], { granularity: wordType }) : {
9898
+ segment: (text) => {
9899
+ const segments = [];
9900
+ const words = text.split(whiteSpaceGroupRgx);
9901
+ for (let i = 0, l = words.length; i < l; i++) {
9902
+ const segment = words[i];
9903
+ segments.push({
9904
+ segment,
9905
+ isWordLike: !whiteSpaceRgx.test(segment), // Consider non-whitespace as word-like
9906
+ });
9907
+ }
9908
+ return segments;
9909
+ }
9910
+ };
9911
+ if (!graphemeSegmenter) graphemeSegmenter = segmenter ? new segmenter([], { granularity: 'grapheme' }) : {
9912
+ segment: text => [...text].map(char => ({ segment: char }))
9913
+ };
9914
+ if (!$splitTemplate && isBrowser) $splitTemplate = doc.createElement('template');
9915
+ if (scope.current) scope.current.register(this);
9916
+ const { words, chars, lines, accessible, includeSpaces, debug } = parameters;
9917
+ const $target = /** @type {HTMLElement} */((target = isArr(target) ? target[0] : target) && /** @type {Node} */(target).nodeType ? target : (getNodeList(target) || [])[0]);
9918
+ const lineParams = lines === true ? {} : lines;
9919
+ const wordParams = words === true || isUnd(words) ? {} : words;
9920
+ const charParams = chars === true ? {} : chars;
9921
+ this.debug = setValue(debug, false);
9922
+ this.includeSpaces = setValue(includeSpaces, false);
9923
+ this.accessible = setValue(accessible, true);
9924
+ this.linesOnly = lineParams && (!wordParams && !charParams);
9925
+ /** @type {String|false|SplitFunctionValue} */
9926
+ this.lineTemplate = isObj(lineParams) ? generateTemplate(lineType, /** @type {SplitTemplateParams} */(lineParams)) : lineParams;
9927
+ /** @type {String|false|SplitFunctionValue} */
9928
+ this.wordTemplate = isObj(wordParams) || this.linesOnly ? generateTemplate(wordType, /** @type {SplitTemplateParams} */(wordParams)) : wordParams;
9929
+ /** @type {String|false|SplitFunctionValue} */
9930
+ this.charTemplate = isObj(charParams) ? generateTemplate(charType, /** @type {SplitTemplateParams} */(charParams)) : charParams;
9931
+ this.$target = $target;
9932
+ this.html = $target && $target.innerHTML;
9933
+ this.lines = [];
9934
+ this.words = [];
9935
+ this.chars = [];
9936
+ this.effects = [];
9937
+ this.effectsCleanups = [];
9938
+ this.cache = null;
9939
+ this.ready = false;
9940
+ this.width = 0;
9941
+ this.resizeTimeout = null;
9942
+ const handleSplit = () => this.html && (lineParams || wordParams || charParams) && this.split();
9943
+ // Make sure this is declared before calling handleSplit() in case revert() is called inside an effect callback
9944
+ this.resizeObserver = new ResizeObserver(() => {
9945
+ // Use a setTimeout instead of a Timer for better tree shaking
9946
+ clearTimeout(this.resizeTimeout);
9947
+ this.resizeTimeout = setTimeout(() => {
9948
+ const currentWidth = /** @type {HTMLElement} */($target).offsetWidth;
9949
+ if (currentWidth === this.width) return;
9950
+ this.width = currentWidth;
9951
+ handleSplit();
9952
+ }, 150);
9953
+ });
9954
+ // Only declare the font ready promise when splitting by lines and not alreay split
9955
+ if (this.lineTemplate && !this.ready) {
9956
+ doc.fonts.ready.then(handleSplit);
9957
+ } else {
9958
+ handleSplit();
9959
+ }
9960
+ $target ? this.resizeObserver.observe($target) : console.warn('No Text Splitter target found.');
8729
9961
  }
8730
9962
 
8731
- cancel() {
8732
- this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise
8733
- return this.commitStyles().forEach('cancel');
9963
+ /**
9964
+ * @param {(...args: any[]) => Tickable | (() => void)} effect
9965
+ * @return this
9966
+ */
9967
+ addEffect(effect) {
9968
+ if (!isFnc(effect)) return console.warn('Effect must return a function.');
9969
+ const refreshableEffect = keepTime(effect);
9970
+ this.effects.push(refreshableEffect);
9971
+ if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this);
9972
+ return this;
8734
9973
  }
8735
9974
 
8736
9975
  revert() {
8737
- // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted,
8738
- // This means if you have multiple animations animating different transforms on the same target,
8739
- // reverting one of them will also override the transform property of the other animations.
8740
- // A better approach would be to store the original custom property values is they exist instead of the entire transform value,
8741
- // and update the CSS variables with the orignal value
8742
- this.cancel().targets.forEach(($el, i) => {
8743
- const targetStyle = $el.style;
8744
- const targetInlineStyles = this._inlineStyles[i];
8745
- for (let name in targetInlineStyles) {
8746
- const originalInlinedValue = targetInlineStyles[name];
8747
- if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) {
8748
- targetStyle.removeProperty(toLowerCase(name));
8749
- } else {
8750
- targetStyle[name] = originalInlinedValue;
8751
- }
8752
- }
8753
- // Remove style attribute if empty
8754
- if ($el.getAttribute('style') === emptyString) $el.removeAttribute('style');
8755
- });
9976
+ clearTimeout(this.resizeTimeout);
9977
+ this.lines.length = this.words.length = this.chars.length = 0;
9978
+ this.resizeObserver.disconnect();
9979
+ // Make sure to revert the effects after disconnecting the resizeObserver to avoid triggering it in the process
9980
+ this.effectsCleanups.forEach(cleanup => isFnc(cleanup) ? cleanup(this) : cleanup.revert && cleanup.revert());
9981
+ this.$target.innerHTML = this.html;
8756
9982
  return this;
8757
9983
  }
8758
9984
 
8759
9985
  /**
8760
- * @typedef {this & {then: null}} ResolvedWAAPIAnimation
9986
+ * Recursively processes a node and its children
9987
+ * @param {Node} node
8761
9988
  */
9989
+ splitNode(node) {
9990
+ const wordTemplate = this.wordTemplate;
9991
+ const charTemplate = this.charTemplate;
9992
+ const includeSpaces = this.includeSpaces;
9993
+ const debug = this.debug;
9994
+ const nodeType = node.nodeType;
9995
+ if (nodeType === 3) {
9996
+ const nodeText = node.nodeValue;
9997
+ // If the nodeText is only whitespace, leave it as is
9998
+ if (nodeText.trim()) {
9999
+ const tempWords = [];
10000
+ const words = this.words;
10001
+ const chars = this.chars;
10002
+ const wordSegments = wordSegmenter.segment(nodeText);
10003
+ const $wordsFragment = doc.createDocumentFragment();
10004
+ let prevSeg = null;
10005
+ for (const wordSegment of wordSegments) {
10006
+ const segment = wordSegment.segment;
10007
+ const isWordLike = isSegmentWordLike(wordSegment);
10008
+ // Determine if this segment should be a new word, first segment always becomes a new word
10009
+ if (!prevSeg || (isWordLike && (prevSeg && (isSegmentWordLike(prevSeg))))) {
10010
+ tempWords.push(segment);
10011
+ } else {
10012
+ // Only concatenate if both current and previous are non-word-like and don't contain spaces
10013
+ const lastWordIndex = tempWords.length - 1;
10014
+ const lastWord = tempWords[lastWordIndex];
10015
+ if (!whiteSpaceGroupRgx.test(lastWord) && !whiteSpaceGroupRgx.test(segment)) {
10016
+ tempWords[lastWordIndex] += segment;
10017
+ } else {
10018
+ tempWords.push(segment);
10019
+ }
10020
+ }
10021
+ prevSeg = wordSegment;
10022
+ }
10023
+
10024
+ for (let i = 0, l = tempWords.length; i < l; i++) {
10025
+ const word = tempWords[i];
10026
+ if (!word.trim()) {
10027
+ // Preserve whitespace only if includeSpaces is false and if the current space is not the first node
10028
+ if (i && includeSpaces) continue;
10029
+ $wordsFragment.appendChild(doc.createTextNode(word));
10030
+ } else {
10031
+ const nextWord = tempWords[i + 1];
10032
+ const hasWordFollowingSpace = includeSpaces && nextWord && !nextWord.trim();
10033
+ const wordToProcess = word;
10034
+ const charSegments = charTemplate ? graphemeSegmenter.segment(wordToProcess) : null;
10035
+ const $charsFragment = charTemplate ? doc.createDocumentFragment() : doc.createTextNode(hasWordFollowingSpace ? word + '\xa0' : word);
10036
+ if (charTemplate) {
10037
+ const charSegmentsArray = [...charSegments];
10038
+ for (let j = 0, jl = charSegmentsArray.length; j < jl; j++) {
10039
+ const charSegment = charSegmentsArray[j];
10040
+ const isLastChar = j === jl - 1;
10041
+ // If this is the last character and includeSpaces is true with a following space, append the space
10042
+ const charText = isLastChar && hasWordFollowingSpace ? charSegment.segment + '\xa0' : charSegment.segment;
10043
+ const $charNode = doc.createTextNode(charText);
10044
+ processHTMLTemplate(charTemplate, chars, $charNode, /** @type {DocumentFragment} */($charsFragment), charType, debug, -1, words.length, chars.length);
10045
+ }
10046
+ }
10047
+ if (wordTemplate) {
10048
+ processHTMLTemplate(wordTemplate, words, $charsFragment, $wordsFragment, wordType, debug, -1, words.length, chars.length);
10049
+ // Chars elements must be re-parsed in the split() method if both words and chars are parsed
10050
+ } else if (charTemplate) {
10051
+ $wordsFragment.appendChild($charsFragment);
10052
+ } else {
10053
+ $wordsFragment.appendChild(doc.createTextNode(word));
10054
+ }
10055
+ // Skip the next iteration if we included a space
10056
+ if (hasWordFollowingSpace) i++;
10057
+ }
10058
+ }
10059
+ node.parentNode.replaceChild($wordsFragment, node);
10060
+ }
10061
+ } else if (nodeType === 1) {
10062
+ // Converting to an array is necessary to work around childNodes pottential mutation
10063
+ const childNodes = /** @type {Array<Node>} */([.../** @type {*} */(node.childNodes)]);
10064
+ for (let i = 0, l = childNodes.length; i < l; i++) this.splitNode(childNodes[i]);
10065
+ }
10066
+ }
8762
10067
 
8763
10068
  /**
8764
- * @param {Callback<ResolvedWAAPIAnimation>} [callback]
8765
- * @return Promise<this>
10069
+ * @param {Boolean} clearCache
10070
+ * @return {this}
8766
10071
  */
8767
- then(callback = noop) {
8768
- const then = this.then;
8769
- const onResolve = () => {
8770
- this.then = null;
8771
- callback(/** @type {ResolvedWAAPIAnimation} */(this));
8772
- this.then = then;
8773
- this._resolve = noop;
8774
- };
8775
- return new Promise(r => {
8776
- this._resolve = () => r(onResolve());
8777
- if (this.completed) this._resolve();
8778
- return this;
8779
- });
10072
+ split(clearCache = false) {
10073
+ const $el = this.$target;
10074
+ const isCached = !!this.cache && !clearCache;
10075
+ const lineTemplate = this.lineTemplate;
10076
+ const wordTemplate = this.wordTemplate;
10077
+ const charTemplate = this.charTemplate;
10078
+ const fontsReady = doc.fonts.status !== 'loading';
10079
+ const canSplitLines = lineTemplate && fontsReady;
10080
+ this.ready = !lineTemplate || fontsReady;
10081
+ if (canSplitLines || clearCache) {
10082
+ // No need to revert effects animations here since it's already taken care by the refreshable
10083
+ this.effectsCleanups.forEach(cleanup => isFnc(cleanup) && cleanup(this));
10084
+ }
10085
+ if (!isCached) {
10086
+ if (clearCache) {
10087
+ $el.innerHTML = this.html;
10088
+ this.words.length = this.chars.length = 0;
10089
+ }
10090
+ this.splitNode($el);
10091
+ this.cache = $el.innerHTML;
10092
+ }
10093
+ if (canSplitLines) {
10094
+ if (isCached) $el.innerHTML = this.cache;
10095
+ this.lines.length = 0;
10096
+ if (wordTemplate) this.words = getAllTopLevelElements($el, wordType);
10097
+ }
10098
+ // Always reparse characters after a line reset or if both words and chars are activated
10099
+ if (charTemplate && (canSplitLines || wordTemplate)) {
10100
+ this.chars = getAllTopLevelElements($el, charType);
10101
+ }
10102
+ // Words are used when lines only and prioritized over chars
10103
+ const elementsArray = this.words.length ? this.words : this.chars;
10104
+ let y, linesCount = 0;
10105
+ for (let i = 0, l = elementsArray.length; i < l; i++) {
10106
+ const $el = elementsArray[i];
10107
+ const { top, height } = $el.getBoundingClientRect();
10108
+ if (!isUnd(y) && top - y > height * .5) linesCount++;
10109
+ $el.setAttribute(dataLine, `${linesCount}`);
10110
+ const nested = $el.querySelectorAll(`[${dataLine}]`);
10111
+ let c = nested.length;
10112
+ while (c--) nested[c].setAttribute(dataLine, `${linesCount}`);
10113
+ y = top;
10114
+ }
10115
+ if (canSplitLines) {
10116
+ const linesFragment = doc.createDocumentFragment();
10117
+ const parents = new Set();
10118
+ const clones = [];
10119
+ for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) {
10120
+ const $clone = /** @type {HTMLElement} */($el.cloneNode(true));
10121
+ filterLineElements($clone, lineIndex, new Set()).forEach($el => {
10122
+ const $parent = $el.parentNode;
10123
+ if ($parent) {
10124
+ if ($el.nodeType === 1) parents.add(/** @type {HTMLElement} */($parent));
10125
+ $parent.removeChild($el);
10126
+ }
10127
+ });
10128
+ clones.push($clone);
10129
+ }
10130
+ parents.forEach(filterEmptyElements);
10131
+ for (let cloneIndex = 0, clonesLength = clones.length; cloneIndex < clonesLength; cloneIndex++) {
10132
+ processHTMLTemplate(lineTemplate, this.lines, clones[cloneIndex], linesFragment, lineType, this.debug, cloneIndex);
10133
+ }
10134
+ $el.innerHTML = '';
10135
+ $el.appendChild(linesFragment);
10136
+ if (wordTemplate) this.words = getAllTopLevelElements($el, wordType);
10137
+ if (charTemplate) this.chars = getAllTopLevelElements($el, charType);
10138
+ }
10139
+
10140
+ // Remove the word wrappers and clear the words array if lines split only
10141
+ if (this.linesOnly) {
10142
+ const words = this.words;
10143
+ let w = words.length;
10144
+ while (w--) {
10145
+ const $word = words[w];
10146
+ $word.replaceWith($word.textContent);
10147
+ }
10148
+ words.length = 0;
10149
+ }
10150
+ if (this.accessible && (canSplitLines || !isCached)) {
10151
+ const $accessible = doc.createElement('span');
10152
+ // Make the accessible element visually-hidden (https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html)
10153
+ $accessible.style.cssText = `position:absolute;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);width:1px;height:1px;white-space:nowrap;`;
10154
+ // $accessible.setAttribute('tabindex', '-1');
10155
+ $accessible.innerHTML = this.html;
10156
+ $el.insertBefore($accessible, $el.firstChild);
10157
+ this.lines.forEach(setAriaHidden);
10158
+ this.words.forEach(setAriaHidden);
10159
+ this.chars.forEach(setAriaHidden);
10160
+ }
10161
+ this.width = /** @type {HTMLElement} */($el).offsetWidth;
10162
+ if (canSplitLines || clearCache) {
10163
+ this.effects.forEach((effect, i) => this.effectsCleanups[i] = effect(this));
10164
+ }
10165
+ return this;
10166
+ }
10167
+
10168
+ refresh() {
10169
+ this.split(true);
8780
10170
  }
8781
10171
  }
8782
10172
 
8783
- const waapi = {
8784
10173
  /**
8785
- * @param {DOMTargetsParam} targets
8786
- * @param {WAAPIAnimationParams} params
8787
- * @return {WAAPIAnimation}
10174
+ * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
10175
+ * @param {TextSplitterParams} [parameters]
10176
+ * @return {TextSplitter}
8788
10177
  */
8789
- animate: (targets, params) => new WAAPIAnimation(targets, params),
8790
- convertEase: easingToLinear
10178
+ const splitText = (target, parameters) => new TextSplitter(target, parameters);
10179
+
10180
+ /**
10181
+ * @deprecated text.split() is deprecated, import splitText() directly, or text.splitText()
10182
+ *
10183
+ * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
10184
+ * @param {TextSplitterParams} [parameters]
10185
+ * @return {TextSplitter}
10186
+ */
10187
+ const split = (target, parameters) => {
10188
+ console.warn('text.split() is deprecated, import splitText() directly, or text.splitText()');
10189
+ return new TextSplitter(target, parameters);
8791
10190
  };
8792
10191
 
8793
- export { registerTargets as $, Animatable, Draggable, JSAnimation, Scope, ScrollObserver, Spring, TextSplitter, Timeline, Timer, WAAPIAnimation, animate, clamp, cleanInlineStyles, createAnimatable, createDraggable, createDrawable, createMotionPath, createScope, createSeededRandom, createSpring, createTimeline, createTimer, cubicBezier, damp, degToRad, eases, index$3 as easings, engine, get, irregular, keepTime, lerp, linear, mapRange, morphTo, onScroll, padEnd, padStart, radToDeg, random, randomPick, remove, round, roundPad, scrollContainers, set, shuffle, snap, split, splitText, spring, stagger, steps, index$1 as svg, sync, index as text, index$2 as utils, waapi, wrap };
10192
+ var index = /*#__PURE__*/Object.freeze({
10193
+ __proto__: null,
10194
+ TextSplitter: TextSplitter,
10195
+ split: split,
10196
+ splitText: splitText
10197
+ });
10198
+
10199
+ export { registerTargets as $, Animatable, AutoLayout, Draggable, JSAnimation, Scope, ScrollObserver, Spring, TextSplitter, Timeline, Timer, WAAPIAnimation, animate, clamp, cleanInlineStyles, createAnimatable, createDraggable, createDrawable, createLayout, createMotionPath, createScope, createSeededRandom, createSpring, createTimeline, createTimer, cubicBezier, damp, degToRad, eases, index$3 as easings, engine, get, irregular, keepTime, lerp, linear, mapRange, morphTo, onScroll, padEnd, padStart, radToDeg, random, randomPick, remove, round, roundPad, scrollContainers, set, shuffle, snap, split, splitText, spring, stagger, steps, index$1 as svg, sync, index as text, index$2 as utils, waapi, wrap };