animejs 4.2.2 → 4.3.0-beta.1

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 +2705 -1303
  3. package/dist/bundles/anime.esm.min.js +2 -2
  4. package/dist/bundles/anime.umd.js +2710 -1306
  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 +1 -1
  95. package/dist/modules/svg/motionpath.js +1 -1
  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.2
3
+ * @version v4.3.0-beta.1
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.2', engine: null };
802
+ const devTools = isBrowser && win.AnimeJSDevTools;
803
+
804
+ const globalVersions = { version: '4.3.0-beta.1', 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,1517 +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;
7745
+ }
7746
+
7747
+ /**
7748
+ * @typedef {this & {then: null}} ResolvedWAAPIAnimation
7749
+ */
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
+ });
7606
7768
  }
7769
+ }
7770
+
7771
+ const waapi = {
7772
+ /**
7773
+ * @param {DOMTargetsParam} targets
7774
+ * @param {WAAPIAnimationParams} params
7775
+ * @return {WAAPIAnimation}
7776
+ */
7777
+ animate: (targets, params) => new WAAPIAnimation(targets, params),
7778
+ convertEase: easingToLinear
7607
7779
  };
7608
7780
 
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
- });
7781
+
7782
+
7783
+
7784
+
7785
+
7636
7786
 
7637
7787
 
7638
7788
 
7639
7789
  /**
7640
- * @param {TargetsParam} path
7641
- * @return {SVGGeometryElement|void}
7790
+ * @typedef {DOMTargetSelector|Array<DOMTargetSelector>} LayoutChildrenParam
7642
7791
  */
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;
7648
- };
7649
7792
 
7793
+ /**
7794
+ * @typedef {Record<String, Number|String>} LayoutStateParams
7795
+ */
7650
7796
 
7797
+ /**
7798
+ * @typedef {Object} LayoutAnimationParams
7799
+ * @property {Number|FunctionValue} [delay]
7800
+ * @property {Number|FunctionValue} [duration]
7801
+ * @property {EasingParam} [ease]
7802
+ * @property {LayoutStateParams} [frozen]
7803
+ * @property {LayoutStateParams} [added]
7804
+ * @property {LayoutStateParams} [removed]
7805
+ * @property {Callback<AutoLayout>} [onComplete]
7806
+ */
7651
7807
 
7652
- // Motion path animation
7808
+ /**
7809
+ * @typedef {LayoutAnimationParams & {
7810
+ * children?: LayoutChildrenParam,
7811
+ * properties?: Array<String>,
7812
+ * }} AutoLayoutParams
7813
+ */
7653
7814
 
7654
7815
  /**
7655
- * @param {SVGGeometryElement} $path
7656
- * @param {Number} totalLength
7657
- * @param {Number} progress
7658
- * @param {Number} lookup
7659
- * @param {Boolean} shouldClamp
7660
- * @return {DOMPoint}
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
7661
7827
  */
7662
- const getPathPoint = ($path, totalLength, progress, lookup, shouldClamp) => {
7663
- const point = progress + lookup;
7664
- const pointOnPath = shouldClamp
7665
- ? Math.max(0, Math.min(point, totalLength)) // Clamp between 0 and totalLength
7666
- : (point % totalLength + totalLength) % totalLength; // Wrap around
7667
- return $path.getPointAtLength(pointOnPath);
7668
- };
7669
7828
 
7670
7829
  /**
7671
- * @param {SVGGeometryElement} $path
7672
- * @param {String} pathProperty
7673
- * @param {Number} [offset=0]
7674
- * @return {FunctionValue}
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}
7675
7877
  */
7676
- const getPathProgess = ($path, pathProperty, offset = 0) => {
7677
- return $el => {
7678
- const totalLength = +($path.getTotalLength());
7679
- const inSvg = $el[isSvgSymbol];
7680
- const ctm = $path.getCTM();
7681
- const shouldClamp = offset === 0;
7682
- /** @type {TweenObjectValue} */
7683
- return {
7684
- from: 0,
7685
- to: totalLength,
7686
- /** @type {TweenModifier} */
7687
- modifier: progress => {
7688
- const offsetLength = offset * totalLength;
7689
- const newProgress = progress + offsetLength;
7690
- if (pathProperty === 'a') {
7691
- const p0 = getPathPoint($path, totalLength, newProgress, -1, shouldClamp);
7692
- const p1 = getPathPoint($path, totalLength, newProgress, 1, shouldClamp);
7693
- return atan2(p1.y - p0.y, p1.x - p0.x) * 180 / PI;
7694
- } else {
7695
- const p = getPathPoint($path, totalLength, newProgress, 0, shouldClamp);
7696
- return pathProperty === 'x' ?
7697
- inSvg || !ctm ? p.x : p.x * ctm.a + p.y * ctm.c + ctm.e :
7698
- inSvg || !ctm ? p.y : p.x * ctm.b + p.y * ctm.d + ctm.f
7699
- }
7700
- }
7701
- }
7702
- }
7703
- };
7878
+
7879
+ let layoutId = 0;
7880
+ let nodeId = 0;
7704
7881
 
7705
7882
  /**
7706
- * @param {TargetsParam} path
7707
- * @param {Number} [offset=0]
7883
+ * @param {DOMTarget} root
7884
+ * @param {DOMTarget} $el
7885
+ * @return {Boolean}
7708
7886
  */
7709
- const createMotionPath = (path, offset = 0) => {
7710
- const $path = getPath(path);
7711
- if (!$path) return;
7712
- return {
7713
- translateX: getPathProgess($path, 'x', offset),
7714
- translateY: getPathProgess($path, 'y', offset),
7715
- rotate: getPathProgess($path, 'a', offset),
7716
- }
7887
+ const isElementInRoot = (root, $el) => {
7888
+ if (!root || !$el) return false;
7889
+ return root === $el || root.contains($el);
7717
7890
  };
7718
7891
 
7719
-
7720
-
7721
7892
  /**
7722
- * @param {SVGGeometryElement} [$el]
7723
- * @return {Number}
7893
+ * @param {Node} node
7894
+ * @param {'previousSibling'|'nextSibling'} direction
7895
+ * @return {Boolean}
7724
7896
  */
7725
- const getScaleFactor = $el => {
7726
- let scaleFactor = 1;
7727
- if ($el && $el.getCTM) {
7728
- const ctm = $el.getCTM();
7729
- if (ctm) {
7730
- const scaleX = sqrt(ctm.a * ctm.a + ctm.b * ctm.b);
7731
- const scaleY = sqrt(ctm.c * ctm.c + ctm.d * ctm.d);
7732
- scaleFactor = (scaleX + scaleY) / 2;
7733
- }
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];
7734
7901
  }
7735
- return scaleFactor;
7902
+ return sibling && sibling.nodeType === Node.TEXT_NODE;
7736
7903
  };
7737
7904
 
7738
7905
  /**
7739
- * Creates a proxy that wraps an SVGGeometryElement and adds drawing functionality.
7740
- * @param {SVGGeometryElement} $el - The SVG element to transform into a drawable
7741
- * @param {number} start - Starting position (0-1)
7742
- * @param {number} end - Ending position (0-1)
7743
- * @return {DrawableSVGGeometry} - Returns a proxy that preserves the original element's type with additional 'draw' attribute functionality
7906
+ * @param {DOMTarget} $el
7907
+ * @return {Boolean}
7744
7908
  */
7745
- const createDrawableProxy = ($el, start, end) => {
7746
- const pathLength = K;
7747
- const computedStyles = getComputedStyle($el);
7748
- const strokeLineCap = computedStyles.strokeLinecap;
7749
- // @ts-ignore
7750
- const $scalled = computedStyles.vectorEffect === 'non-scaling-stroke' ? $el : null;
7751
- let currentCap = strokeLineCap;
7752
-
7753
- const proxy = new Proxy($el, {
7754
- get(target, property) {
7755
- const value = target[property];
7756
- if (property === proxyTargetSymbol) return target;
7757
- if (property === 'setAttribute') {
7758
- return (...args) => {
7759
- if (args[0] === 'draw') {
7760
- const value = args[1];
7761
- const values = value.split(' ');
7762
- const v1 = +values[0];
7763
- const v2 = +values[1];
7764
- // TOTO: Benchmark if performing two slices is more performant than one split
7765
- // const spaceIndex = value.indexOf(' ');
7766
- // const v1 = round(+value.slice(0, spaceIndex), precision);
7767
- // const v2 = round(+value.slice(spaceIndex + 1), precision);
7768
- const scaleFactor = getScaleFactor($scalled);
7769
- const os = v1 * -pathLength * scaleFactor;
7770
- const d1 = (v2 * pathLength * scaleFactor) + os;
7771
- const d2 = (pathLength * scaleFactor +
7772
- ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10 * scaleFactor) - d1);
7773
- if (strokeLineCap !== 'butt') {
7774
- const newCap = v1 === v2 ? 'butt' : strokeLineCap;
7775
- if (currentCap !== newCap) {
7776
- target.style.strokeLinecap = `${newCap}`;
7777
- currentCap = newCap;
7778
- }
7779
- }
7780
- target.setAttribute('stroke-dashoffset', `${os}`);
7781
- target.setAttribute('stroke-dasharray', `${d1} ${d2}`);
7782
- }
7783
- return Reflect.apply(value, target, args);
7784
- };
7785
- }
7909
+ const isElementSurroundedByText = $el => hasTextSibling($el, 'previousSibling') || hasTextSibling($el, 'nextSibling');
7786
7910
 
7787
- if (isFnc(value)) {
7788
- return (...args) => Reflect.apply(value, target, args);
7789
- } else {
7790
- return value;
7791
- }
7792
- }
7793
- });
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
+ };
7794
7922
 
7795
- if ($el.getAttribute('pathLength') !== `${pathLength}`) {
7796
- $el.setAttribute('pathLength', `${pathLength}`);
7797
- proxy.setAttribute('draw', `${start} ${end}`);
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;
7932
+ } else {
7933
+ style.removeProperty('transition');
7798
7934
  }
7799
-
7800
- return /** @type {DrawableSVGGeometry} */(proxy);
7801
7935
  };
7802
7936
 
7803
7937
  /**
7804
- * Creates drawable proxies for multiple SVG elements.
7805
- * @param {TargetsParam} selector - CSS selector, SVG element, or array of elements and selectors
7806
- * @param {number} [start=0] - Starting position (0-1)
7807
- * @param {number} [end=0] - Ending position (0-1)
7808
- * @return {Array<DrawableSVGGeometry>} - Array of proxied elements with drawing functionality
7938
+ * @param {LayoutNode} node
7809
7939
  */
7810
- const createDrawable = (selector, start = 0, end = 0) => {
7811
- const els = parseTargets(selector);
7812
- return els.map($el => createDrawableProxy(
7813
- /** @type {SVGGeometryElement} */($el),
7814
- start,
7815
- end
7816
- ));
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));
7817
7946
  };
7818
7947
 
7948
+ /**
7949
+ * @param {Map<DOMTarget, String|null>} store
7950
+ */
7951
+ const restoreLayoutTransition = store => {
7952
+ store.forEach((value, $el) => restoreElementTransition($el, value));
7953
+ store.clear();
7954
+ };
7819
7955
 
7956
+ const hiddenComputedStyle = /** @type {CSSStyleDeclaration} */({
7957
+ display: 'none',
7958
+ visibility: 'hidden',
7959
+ opacity: '0',
7960
+ transform: 'none',
7961
+ position: 'static',
7962
+ });
7820
7963
 
7821
7964
  /**
7822
- * @param {TargetsParam} path2
7823
- * @param {Number} [precision]
7824
- * @return {FunctionValue}
7965
+ * @param {LayoutNode|null} node
7825
7966
  */
7826
- const morphTo = (path2, precision = .33) => ($path1) => {
7827
- const tagName1 = ($path1.tagName || '').toLowerCase();
7828
- if (!tagName1.match(/^(path|polygon|polyline)$/)) {
7829
- throw new Error(`Can't morph a <${$path1.tagName}> SVG element. Use <path>, <polygon> or <polyline>.`);
7830
- }
7831
- const $path2 = /** @type {SVGGeometryElement} */(getPath(path2));
7832
- if (!$path2) {
7833
- throw new Error("Can't morph to an invalid target. 'path2' must resolve to an existing <path>, <polygon> or <polyline> SVG element.");
7834
- }
7835
- const tagName2 = ($path2.tagName || '').toLowerCase();
7836
- if (!tagName2.match(/^(path|polygon|polyline)$/)) {
7837
- throw new Error(`Can't morph a <${$path2.tagName}> SVG element. Use <path>, <polygon> or <polyline>.`);
7838
- }
7839
- const isPath = $path1.tagName === 'path';
7840
- const separator = isPath ? ' ' : ',';
7841
- const previousPoints = $path1[morphPointsSymbol];
7842
- if (previousPoints) $path1.setAttribute(isPath ? 'd' : 'points', previousPoints);
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
+ };
7843
7979
 
7844
- let v1 = '', v2 = '';
7980
+ /**
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
+ };
7845
8043
 
7846
- if (!precision) {
7847
- v1 = $path1.getAttribute(isPath ? 'd' : 'points');
7848
- v2 = $path2.getAttribute(isPath ? 'd' : 'points');
7849
- } else {
7850
- const length1 = /** @type {SVGGeometryElement} */($path1).getTotalLength();
7851
- const length2 = $path2.getTotalLength();
7852
- const maxPoints = Math.max(Math.ceil(length1 * precision), Math.ceil(length2 * precision));
7853
- for (let i = 0; i < maxPoints; i++) {
7854
- const t = i / (maxPoints - 1);
7855
- const pointOnPath1 = /** @type {SVGGeometryElement} */($path1).getPointAtLength(length1 * t);
7856
- const pointOnPath2 = $path2.getPointAtLength(length2 * t);
7857
- const prefix = isPath ? (i === 0 ? 'M' : 'L') : '';
7858
- v1 += prefix + round$1(pointOnPath1.x, 3) + separator + pointOnPath1.y + ' ';
7859
- v2 += prefix + round$1(pointOnPath2.x, 3) + separator + pointOnPath2.y + ' ';
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';
7860
8087
  }
7861
8088
  }
7862
8089
 
7863
- $path1[morphPointsSymbol] = v2;
7864
-
7865
- return [v1, v2];
7866
- };
7867
-
7868
- var index$1 = /*#__PURE__*/Object.freeze({
7869
- __proto__: null,
7870
- createDrawable: createDrawable,
7871
- createMotionPath: createMotionPath,
7872
- morphTo: morphTo
7873
- });
7874
-
8090
+ let left = 0;
8091
+ let top = 0;
8092
+ let width = 0;
8093
+ let height = 0;
7875
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
+ }
7876
8102
 
7877
- const segmenter = (typeof Intl !== 'undefined') && Intl.Segmenter;
7878
- const valueRgx = /\{value\}/g;
7879
- const indexRgx = /\{i\}/g;
7880
- const whiteSpaceGroupRgx = /(\s+)/;
7881
- const whiteSpaceRgx = /^\s+$/;
7882
- const lineType = 'line';
7883
- const wordType = 'word';
7884
- const charType = 'char';
7885
- const dataLine = `data-line`;
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
+ }
7886
8107
 
7887
- /**
7888
- * @typedef {Object} Segment
7889
- * @property {String} segment
7890
- * @property {Boolean} [isWordLike]
7891
- */
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
+ };
7892
8151
 
7893
8152
  /**
7894
- * @typedef {Object} Segmenter
7895
- * @property {function(String): Iterable<Segment>} segment
8153
+ * @param {LayoutNode} node
8154
+ * @param {LayoutStateParams} [props]
7896
8155
  */
7897
-
7898
- /** @type {Segmenter} */
7899
- let wordSegmenter = null;
7900
- /** @type {Segmenter} */
7901
- let graphemeSegmenter = null;
7902
- let $splitTemplate = null;
8156
+ const updateNodeProperties = (node, props) => {
8157
+ if (!props) return;
8158
+ for (let name in props) {
8159
+ node.properties[name] = props[name];
8160
+ }
8161
+ };
7903
8162
 
7904
8163
  /**
7905
- * @param {Segment} seg
7906
- * @return {Boolean}
8164
+ * @param {LayoutNode} node
7907
8165
  */
7908
- const isSegmentWordLike = seg => {
7909
- return seg.isWordLike ||
7910
- seg.segment === ' ' || // Consider spaces as words first, then handle them diffrently later
7911
- isNum(+seg.segment); // Safari doesn't considers numbers as words
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
+ });
7912
8173
  };
7913
8174
 
7914
8175
  /**
7915
- * @param {HTMLElement} $el
8176
+ * @param {LayoutNode} node
7916
8177
  */
7917
- const setAriaHidden = $el => $el.setAttribute('aria-hidden', 'true');
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
+ };
7918
8192
 
7919
8193
  /**
7920
- * @param {DOMTarget} $el
7921
- * @param {String} type
7922
- * @return {Array<HTMLElement>}
8194
+ * @param {LayoutNode} node
7923
8195
  */
7924
- const getAllTopLevelElements = ($el, type) => [.../** @type {*} */($el.querySelectorAll(`[data-${type}]:not([data-${type}] [data-${type}])`))];
7925
-
7926
- const debugColors = { line: '#00D672', word: '#FF4B4B', char: '#5A87FF' };
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;
8222
+ }
8223
+ };
7927
8224
 
7928
8225
  /**
7929
- * @param {HTMLElement} $el
8226
+ * @param {LayoutNode} node
7930
8227
  */
7931
- const filterEmptyElements = $el => {
7932
- if (!$el.childElementCount && !$el.textContent.trim()) {
7933
- const $parent = $el.parentElement;
7934
- $el.remove();
7935
- if ($parent) filterEmptyElements($parent);
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);
7936
8239
  }
7937
8240
  };
7938
8241
 
7939
8242
  /**
7940
- * @param {HTMLElement} $el
7941
- * @param {Number} lineIndex
7942
- * @param {Set<HTMLElement>} bin
7943
- * @returns {Set<HTMLElement>}
7944
- */
7945
- const filterLineElements = ($el, lineIndex, bin) => {
7946
- const dataLineAttr = $el.getAttribute(dataLine);
7947
- if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR') bin.add($el);
7948
- let i = $el.childElementCount;
7949
- while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin);
7950
- return bin;
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;
7951
8263
  };
7952
8264
 
7953
- /**
7954
- * @param {'line'|'word'|'char'} type
7955
- * @param {SplitTemplateParams} params
7956
- * @return {String}
7957
- */
7958
- const generateTemplate = (type, params = {}) => {
7959
- let template = ``;
7960
- const classString = isStr(params.class) ? ` class="${params.class}"` : '';
7961
- const cloneType = setValue(params.clone, false);
7962
- const wrapType = setValue(params.wrap, false);
7963
- const overflow = wrapType ? wrapType === true ? 'clip' : wrapType : cloneType ? 'clip' : false;
7964
- if (wrapType) template += `<span${overflow ? ` style="overflow:${overflow};"` : ''}>`;
7965
- template += `<span${classString}${cloneType ? ` style="position:relative;"` : ''} data-${type}="{i}">`;
7966
- if (cloneType) {
7967
- const left = cloneType === 'left' ? '-100%' : cloneType === 'right' ? '100%' : '0';
7968
- const top = cloneType === 'top' ? '-100%' : cloneType === 'bottom' ? '100%' : '0';
7969
- template += `<span>{value}</span>`;
7970
- template += `<span inert style="position:absolute;top:${top};left:${left};white-space:nowrap;">{value}</span>`;
7971
- } else {
7972
- template += `{value}`;
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;
7973
8282
  }
7974
- template += `</span>`;
7975
- if (wrapType) template += `</span>`;
7976
- return template;
7977
- };
7978
8283
 
7979
- /**
7980
- * @param {String|SplitFunctionValue} htmlTemplate
7981
- * @param {Array<HTMLElement>} store
7982
- * @param {Node|HTMLElement} node
7983
- * @param {DocumentFragment} $parentFragment
7984
- * @param {'line'|'word'|'char'} type
7985
- * @param {Boolean} debug
7986
- * @param {Number} lineIndex
7987
- * @param {Number} [wordIndex]
7988
- * @param {Number} [charIndex]
7989
- * @return {HTMLElement}
7990
- */
7991
- const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, debug, lineIndex, wordIndex, charIndex) => {
7992
- const isLine = type === lineType;
7993
- const isChar = type === charType;
7994
- const className = `_${type}_`;
7995
- const template = isFnc(htmlTemplate) ? htmlTemplate(node) : htmlTemplate;
7996
- const displayStyle = isLine ? 'block' : 'inline-block';
7997
- $splitTemplate.innerHTML = template
7998
- .replace(valueRgx, `<i class="${className}"></i>`)
7999
- .replace(indexRgx, `${isChar ? charIndex : isLine ? lineIndex : wordIndex}`);
8000
- const $content = $splitTemplate.content;
8001
- const $highestParent = /** @type {HTMLElement} */($content.firstElementChild);
8002
- const $split = /** @type {HTMLElement} */($content.querySelector(`[data-${type}]`)) || $highestParent;
8003
- const $replacables = /** @type {NodeListOf<HTMLElement>} */($content.querySelectorAll(`i.${className}`));
8004
- const replacablesLength = $replacables.length;
8005
- if (replacablesLength) {
8006
- $highestParent.style.display = displayStyle;
8007
- $split.style.display = displayStyle;
8008
- $split.setAttribute(dataLine, `${lineIndex}`);
8009
- if (!isLine) {
8010
- $split.setAttribute('data-word', `${wordIndex}`);
8011
- if (isChar) $split.setAttribute('data-char', `${charIndex}`);
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;
8012
8307
  }
8013
- let i = replacablesLength;
8014
- while (i--) {
8015
- const $replace = $replacables[i];
8016
- const $closestParent = $replace.parentElement;
8017
- $closestParent.style.display = displayStyle;
8018
- if (isLine) {
8019
- $closestParent.innerHTML = /** @type {HTMLElement} */(node).innerHTML;
8020
- } else {
8021
- $closestParent.replaceChild(node.cloneNode(true), $replace);
8022
- }
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;
8023
8320
  }
8024
- store.push($split);
8025
- $parentFragment.appendChild($content);
8026
- } else {
8027
- console.warn(`The expression "{value}" is missing from the provided template.`);
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);
8028
8328
  }
8029
- if (debug) $highestParent.style.outline = `1px dotted ${debugColors[type]}`;
8030
- return $highestParent;
8031
- };
8032
8329
 
8033
- /**
8034
- * A class that splits text into words and wraps them in span elements while preserving the original HTML structure.
8035
- * @class
8036
- */
8037
- class TextSplitter {
8038
8330
  /**
8039
- * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
8040
- * @param {TextSplitterParams} [parameters]
8331
+ * @param {LayoutNode|null} rootNode
8332
+ * @param {LayoutNodeIterator} cb
8041
8333
  */
8042
- constructor(target, parameters = {}) {
8043
- // Only init segmenters when needed
8044
- if (!wordSegmenter) wordSegmenter = segmenter ? new segmenter([], { granularity: wordType }) : {
8045
- segment: (text) => {
8046
- const segments = [];
8047
- const words = text.split(whiteSpaceGroupRgx);
8048
- for (let i = 0, l = words.length; i < l; i++) {
8049
- const segment = words[i];
8050
- segments.push({
8051
- segment,
8052
- isWordLike: !whiteSpaceRgx.test(segment), // Consider non-whitespace as word-like
8053
- });
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;
8343
+ } else {
8344
+ while (node && !node._next) {
8345
+ node = node.parentNode;
8054
8346
  }
8055
- return segments;
8347
+ if (node) node = node._next;
8056
8348
  }
8057
- };
8058
- if (!graphemeSegmenter) graphemeSegmenter = segmenter ? new segmenter([], { granularity: 'grapheme' }) : {
8059
- segment: text => [...text].map(char => ({ segment: char }))
8060
- };
8061
- if (!$splitTemplate && isBrowser) $splitTemplate = doc.createElement('template');
8062
- if (scope.current) scope.current.register(this);
8063
- const { words, chars, lines, accessible, includeSpaces, debug } = parameters;
8064
- const $target = /** @type {HTMLElement} */((target = isArr(target) ? target[0] : target) && /** @type {Node} */(target).nodeType ? target : (getNodeList(target) || [])[0]);
8065
- const lineParams = lines === true ? {} : lines;
8066
- const wordParams = words === true || isUnd(words) ? {} : words;
8067
- const charParams = chars === true ? {} : chars;
8068
- this.debug = setValue(debug, false);
8069
- this.includeSpaces = setValue(includeSpaces, false);
8070
- this.accessible = setValue(accessible, true);
8071
- this.linesOnly = lineParams && (!wordParams && !charParams);
8072
- /** @type {String|false|SplitFunctionValue} */
8073
- this.lineTemplate = isObj(lineParams) ? generateTemplate(lineType, /** @type {SplitTemplateParams} */(lineParams)) : lineParams;
8074
- /** @type {String|false|SplitFunctionValue} */
8075
- this.wordTemplate = isObj(wordParams) || this.linesOnly ? generateTemplate(wordType, /** @type {SplitTemplateParams} */(wordParams)) : wordParams;
8076
- /** @type {String|false|SplitFunctionValue} */
8077
- this.charTemplate = isObj(charParams) ? generateTemplate(charType, /** @type {SplitTemplateParams} */(charParams)) : charParams;
8078
- this.$target = $target;
8079
- this.html = $target && $target.innerHTML;
8080
- this.lines = [];
8081
- this.words = [];
8082
- this.chars = [];
8083
- this.effects = [];
8084
- this.effectsCleanups = [];
8085
- this.cache = null;
8086
- this.ready = false;
8087
- this.width = 0;
8088
- this.resizeTimeout = null;
8089
- const handleSplit = () => this.html && (lineParams || wordParams || charParams) && this.split();
8090
- // Make sure this is declared before calling handleSplit() in case revert() is called inside an effect callback
8091
- this.resizeObserver = new ResizeObserver(() => {
8092
- // Use a setTimeout instead of a Timer for better tree shaking
8093
- clearTimeout(this.resizeTimeout);
8094
- this.resizeTimeout = setTimeout(() => {
8095
- const currentWidth = /** @type {HTMLElement} */($target).offsetWidth;
8096
- if (currentWidth === this.width) return;
8097
- this.width = currentWidth;
8098
- handleSplit();
8099
- }, 150);
8100
- });
8101
- // Only declare the font ready promise when splitting by lines and not alreay split
8102
- if (this.lineTemplate && !this.ready) {
8103
- doc.fonts.ready.then(handleSplit);
8104
- } else {
8105
- handleSplit();
8106
8349
  }
8107
- $target ? this.resizeObserver.observe($target) : console.warn('No Text Splitter target found.');
8108
8350
  }
8109
8351
 
8110
8352
  /**
8111
- * @param {(...args: any[]) => Tickable | (() => void)} effect
8112
- * @return this
8353
+ * @param {LayoutNodeIterator} cb
8113
8354
  */
8114
- addEffect(effect) {
8115
- if (!isFnc(effect)) return console.warn('Effect must return a function.');
8116
- const refreshableEffect = keepTime(effect);
8117
- this.effects.push(refreshableEffect);
8118
- if (this.ready) this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this);
8119
- return this;
8355
+ forEachRootNode(cb) {
8356
+ this.forEach(this.rootNode, cb);
8120
8357
  }
8121
8358
 
8122
- revert() {
8123
- clearTimeout(this.resizeTimeout);
8124
- this.lines.length = this.words.length = this.chars.length = 0;
8125
- this.resizeObserver.disconnect();
8126
- // Make sure to revert the effects after disconnecting the resizeObserver to avoid triggering it in the process
8127
- this.effectsCleanups.forEach(cleanup => isFnc(cleanup) ? cleanup(this) : cleanup.revert && cleanup.revert());
8128
- this.$target.innerHTML = this.html;
8129
- return this;
8359
+ /**
8360
+ * @param {LayoutNodeIterator} cb
8361
+ */
8362
+ forEachNode(cb) {
8363
+ for (const rootNode of this.rootNodes) {
8364
+ this.forEach(rootNode, cb);
8365
+ }
8130
8366
  }
8131
8367
 
8132
8368
  /**
8133
- * Recursively processes a node and its children
8134
- * @param {Node} node
8369
+ * @param {DOMTarget} $el
8370
+ * @param {LayoutNode|null} parentNode
8371
+ * @return {LayoutNode|null}
8135
8372
  */
8136
- splitNode(node) {
8137
- const wordTemplate = this.wordTemplate;
8138
- const charTemplate = this.charTemplate;
8139
- const includeSpaces = this.includeSpaces;
8140
- const debug = this.debug;
8141
- const nodeType = node.nodeType;
8142
- if (nodeType === 3) {
8143
- const nodeText = node.nodeValue;
8144
- // If the nodeText is only whitespace, leave it as is
8145
- if (nodeText.trim()) {
8146
- const tempWords = [];
8147
- const words = this.words;
8148
- const chars = this.chars;
8149
- const wordSegments = wordSegmenter.segment(nodeText);
8150
- const $wordsFragment = doc.createDocumentFragment();
8151
- let prevSeg = null;
8152
- for (const wordSegment of wordSegments) {
8153
- const segment = wordSegment.segment;
8154
- const isWordLike = isSegmentWordLike(wordSegment);
8155
- // Determine if this segment should be a new word, first segment always becomes a new word
8156
- if (!prevSeg || (isWordLike && (prevSeg && (isSegmentWordLike(prevSeg))))) {
8157
- tempWords.push(segment);
8158
- } else {
8159
- // Only concatenate if both current and previous are non-word-like and don't contain spaces
8160
- const lastWordIndex = tempWords.length - 1;
8161
- const lastWord = tempWords[lastWordIndex];
8162
- if (!lastWord.includes(' ') && !segment.includes(' ')) {
8163
- tempWords[lastWordIndex] += segment;
8164
- } else {
8165
- tempWords.push(segment);
8166
- }
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;
8167
8417
  }
8168
- prevSeg = wordSegment;
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;
8169
8429
  }
8430
+ } else {
8431
+ node = createNode($current, $parent, this, node);
8432
+ }
8170
8433
 
8171
- for (let i = 0, l = tempWords.length; i < l; i++) {
8172
- const word = tempWords[i];
8173
- if (!word.trim()) {
8174
- // Preserve whitespace only if includeSpaces is false and if the current space is not the first node
8175
- if (i && includeSpaces) continue;
8176
- $wordsFragment.appendChild(doc.createTextNode(word));
8177
- } else {
8178
- const nextWord = tempWords[i + 1];
8179
- const hasWordFollowingSpace = includeSpaces && nextWord && !nextWord.trim();
8180
- const wordToProcess = word;
8181
- const charSegments = charTemplate ? graphemeSegmenter.segment(wordToProcess) : null;
8182
- const $charsFragment = charTemplate ? doc.createDocumentFragment() : doc.createTextNode(hasWordFollowingSpace ? word + '\xa0' : word);
8183
- if (charTemplate) {
8184
- const charSegmentsArray = [...charSegments];
8185
- for (let j = 0, jl = charSegmentsArray.length; j < jl; j++) {
8186
- const charSegment = charSegmentsArray[j];
8187
- const isLastChar = j === jl - 1;
8188
- // If this is the last character and includeSpaces is true with a following space, append the space
8189
- const charText = isLastChar && hasWordFollowingSpace ? charSegment.segment + '\xa0' : charSegment.segment;
8190
- const $charNode = doc.createTextNode(charText);
8191
- processHTMLTemplate(charTemplate, chars, $charNode, /** @type {DocumentFragment} */($charsFragment), charType, debug, -1, words.length, chars.length);
8192
- }
8193
- }
8194
- if (wordTemplate) {
8195
- processHTMLTemplate(wordTemplate, words, $charsFragment, $wordsFragment, wordType, debug, -1, words.length, chars.length);
8196
- // Chars elements must be re-parsed in the split() method if both words and chars are parsed
8197
- } else if (charTemplate) {
8198
- $wordsFragment.appendChild($charsFragment);
8199
- } else {
8200
- $wordsFragment.appendChild(doc.createTextNode(word));
8201
- }
8202
- // Skip the next iteration if we included a space
8203
- if (hasWordFollowingSpace) i++;
8204
- }
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;
8205
8459
  }
8206
- node.parentNode.replaceChild($wordsFragment, node);
8460
+ } else {
8461
+ this.rootNodes.add(node);
8207
8462
  }
8208
- } else if (nodeType === 1) {
8209
- // Converting to an array is necessary to work around childNodes pottential mutation
8210
- const childNodes = /** @type {Array<Node>} */([.../** @type {*} */(node.childNodes)]);
8211
- for (let i = 0, l = childNodes.length; i < l; i++) this.splitNode(childNodes[i]);
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;
8212
8473
  }
8474
+
8475
+ return firstNode;
8476
+ }
8477
+
8478
+ /**
8479
+ * @param {DOMTarget} $el
8480
+ * @param {Set<DOMTarget>} candidates
8481
+ * @return {LayoutNode|null}
8482
+ */
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);
8213
8498
  }
8214
8499
 
8215
8500
  /**
8216
- * @param {Boolean} clearCache
8217
8501
  * @return {this}
8218
8502
  */
8219
- split(clearCache = false) {
8220
- const $el = this.$target;
8221
- const isCached = !!this.cache && !clearCache;
8222
- const lineTemplate = this.lineTemplate;
8223
- const wordTemplate = this.wordTemplate;
8224
- const charTemplate = this.charTemplate;
8225
- const fontsReady = doc.fonts.status !== 'loading';
8226
- const canSplitLines = lineTemplate && fontsReady;
8227
- this.ready = !lineTemplate || fontsReady;
8228
- if (canSplitLines || clearCache) {
8229
- // No need to revert effects animations here since it's already taken care by the refreshable
8230
- this.effectsCleanups.forEach(cleanup => isFnc(cleanup) && cleanup(this));
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;
8231
8512
  }
8232
- if (!isCached) {
8233
- if (clearCache) {
8234
- $el.innerHTML = this.html;
8235
- this.words.length = this.chars.length = 0;
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);
8236
8547
  }
8237
- this.splitNode($el);
8238
- this.cache = $el.innerHTML;
8239
- }
8240
- if (canSplitLines) {
8241
- if (isCached) $el.innerHTML = this.cache;
8242
- this.lines.length = 0;
8243
- if (wordTemplate) this.words = getAllTopLevelElements($el, wordType);
8244
8548
  }
8245
- // Always reparse characters after a line reset or if both words and chars are activated
8246
- if (charTemplate && (canSplitLines || wordTemplate)) {
8247
- this.chars = getAllTopLevelElements($el, charType);
8549
+
8550
+ for (let i = 0, l = orderedDetachedElements.length; i < l; i++) {
8551
+ this.ensureDetachedNode(orderedDetachedElements[i], detachedElementsLookup);
8248
8552
  }
8249
- // Words are used when lines only and prioritized over chars
8250
- const elementsArray = this.words.length ? this.words : this.chars;
8251
- let y, linesCount = 0;
8252
- for (let i = 0, l = elementsArray.length; i < l; i++) {
8253
- const $el = elementsArray[i];
8254
- const { top, height } = $el.getBoundingClientRect();
8255
- if (y && top - y > height * .5) linesCount++;
8256
- $el.setAttribute(dataLine, `${linesCount}`);
8257
- const nested = $el.querySelectorAll(`[${dataLine}]`);
8258
- let c = nested.length;
8259
- while (c--) nested[c].setAttribute(dataLine, `${linesCount}`);
8260
- y = top;
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
+ }
8261
8565
  }
8262
- if (canSplitLines) {
8263
- const linesFragment = doc.createDocumentFragment();
8264
- const parents = new Set();
8265
- const clones = [];
8266
- for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) {
8267
- const $clone = /** @type {HTMLElement} */($el.cloneNode(true));
8268
- filterLineElements($clone, lineIndex, new Set()).forEach($el => {
8269
- const $parent = $el.parentElement;
8270
- if ($parent) parents.add($parent);
8271
- $el.remove();
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
+
8578
+ return this;
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|FunctionValue} */
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, 'inOut(3.5)');
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
+ }
8667
+
8668
+ /**
8669
+ * @return {this}
8670
+ */
8671
+ revert() {
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));
8685
+ return this;
8686
+ }
8687
+
8688
+ /**
8689
+ * @return {this}
8690
+ */
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;
8864
+ }
8865
+ }
8866
+ }
8867
+
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);
8272
8905
  });
8273
- clones.push($clone);
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';
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`;
8969
+ }
8274
8970
  }
8275
- parents.forEach(filterEmptyElements);
8276
- for (let cloneIndex = 0, clonesLength = clones.length; cloneIndex < clonesLength; cloneIndex++) {
8277
- processHTMLTemplate(lineTemplate, this.lines, clones[cloneIndex], linesFragment, lineType, this.debug, cloneIndex);
8971
+
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
+ });
8278
8978
  }
8279
- $el.innerHTML = '';
8280
- $el.appendChild(linesFragment);
8281
- if (wordTemplate) this.words = getAllTopLevelElements($el, wordType);
8282
- if (charTemplate) this.chars = getAllTopLevelElements($el, charType);
8283
- }
8284
- // Remove the word wrappers and clear the words array if lines split only
8285
- if (this.linesOnly) {
8286
- const words = this.words;
8287
- let w = words.length;
8288
- while (w--) {
8289
- const $word = words[w];
8290
- $word.replaceWith($word.textContent);
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
+ }
8291
9019
  }
8292
- words.length = 0;
9020
+
8293
9021
  }
8294
- if (this.accessible && (canSplitLines || !isCached)) {
8295
- const $accessible = doc.createElement('span');
8296
- // Make the accessible element visually-hidden (https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html)
8297
- $accessible.style.cssText = `position:absolute;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);width:1px;height:1px;white-space:nowrap;`;
8298
- // $accessible.setAttribute('tabindex', '-1');
8299
- $accessible.innerHTML = this.html;
8300
- $el.insertBefore($accessible, $el.firstChild);
8301
- this.lines.forEach(setAriaHidden);
8302
- this.words.forEach(setAriaHidden);
8303
- this.chars.forEach(setAriaHidden);
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
+ }
9043
+ });
9044
+ }
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);
9067
+ }
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);
9080
+ }
9081
+
8304
9082
  }
8305
- this.width = /** @type {HTMLElement} */($el).offsetWidth;
8306
- if (canSplitLines || clearCache) {
8307
- 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);
8308
9101
  }
8309
- return this;
9102
+
9103
+ return this.timeline.init();
8310
9104
  }
8311
9105
 
8312
- refresh() {
8313
- 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;
8314
9116
  }
8315
9117
  }
8316
9118
 
8317
9119
  /**
8318
- * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
8319
- * @param {TextSplitterParams} [parameters]
8320
- * @return {TextSplitter}
9120
+ * @param {DOMTargetSelector} root
9121
+ * @param {AutoLayoutParams} [params]
9122
+ * @return {AutoLayout}
8321
9123
  */
8322
- 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 = {};
8323
9131
 
8324
9132
  /**
8325
- * @deprecated text.split() is deprecated, import splitText() directly, or text.splitText()
9133
+ * @callback UtilityFunction
9134
+ * @param {...*} args
9135
+ * @return {Number|String}
8326
9136
  *
8327
- * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
8328
- * @param {TextSplitterParams} [parameters]
8329
- * @return {TextSplitter}
8330
- */
8331
- const split = (target, parameters) => {
8332
- console.warn('text.split() is deprecated, import splitText() directly, or text.splitText()');
8333
- return new TextSplitter(target, parameters);
9137
+ * @param {UtilityFunction} fn
9138
+ * @param {Number} [last=0]
9139
+ * @return {function(...(Number|String)): function(Number|String): (Number|String)}
9140
+ */
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
+ }
8334
9158
  };
8335
9159
 
8336
- var index = /*#__PURE__*/Object.freeze({
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
+ }
9449
+ };
9450
+
9451
+ var index$2 = /*#__PURE__*/Object.freeze({
8337
9452
  __proto__: null,
8338
- TextSplitter: TextSplitter,
8339
- split: split,
8340
- 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
8341
9477
  });
8342
9478
 
8343
9479
 
8344
9480
 
8345
-
8346
-
8347
-
8348
-
8349
- /**
8350
- * Converts an easing function into a valid CSS linear() timing function string
8351
- * @param {EasingFunction} fn
8352
- * @param {number} [samples=100]
8353
- * @returns {string} CSS linear() timing function
8354
- */
8355
- const easingToLinear = (fn, samples = 100) => {
8356
- const points = [];
8357
- for (let i = 0; i <= samples; i++) points.push(round$1(fn(i / samples), 4));
8358
- return `linear(${points.join(', ')})`;
8359
- };
8360
-
8361
- const WAAPIEasesLookups = {};
8362
-
8363
9481
  /**
8364
- * @param {EasingParam} ease
8365
- * @return {String}
9482
+ * @param {TargetsParam} path
9483
+ * @return {SVGGeometryElement|void}
8366
9484
  */
8367
- const parseWAAPIEasing = (ease) => {
8368
- let parsedEase = WAAPIEasesLookups[ease];
8369
- if (parsedEase) return parsedEase;
8370
- parsedEase = 'linear';
8371
- if (isStr(ease)) {
8372
- if (
8373
- stringStartsWith(ease, 'linear') ||
8374
- stringStartsWith(ease, 'cubic-') ||
8375
- stringStartsWith(ease, 'steps') ||
8376
- stringStartsWith(ease, 'ease')
8377
- ) {
8378
- parsedEase = ease;
8379
- } else if (stringStartsWith(ease, 'cubicB')) {
8380
- parsedEase = toLowerCase(ease);
8381
- } else {
8382
- const parsed = parseEaseString(ease);
8383
- if (isFnc(parsed)) parsedEase = parsed === none ? 'linear' : easingToLinear(parsed);
8384
- }
8385
- // Only cache string based easing name, otherwise function arguments get lost
8386
- WAAPIEasesLookups[ease] = parsedEase;
8387
- } else if (isFnc(ease)) {
8388
- const easing = easingToLinear(ease);
8389
- if (easing) parsedEase = easing;
8390
- } else if (/** @type {Spring} */(ease).ease) {
8391
- parsedEase = easingToLinear(/** @type {Spring} */(ease).ease);
8392
- }
8393
- 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;
8394
9490
  };
8395
9491
 
8396
- const transformsShorthands = ['x', 'y', 'z'];
8397
- const commonDefaultPXProperties = [
8398
- 'perspective',
8399
- 'width',
8400
- 'height',
8401
- 'margin',
8402
- 'padding',
8403
- 'top',
8404
- 'right',
8405
- 'bottom',
8406
- 'left',
8407
- 'borderWidth',
8408
- 'fontSize',
8409
- 'borderRadius',
8410
- ...transformsShorthands
8411
- ];
8412
9492
 
8413
- const validIndividualTransforms = /*#__PURE__*/ (() => [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))])();
8414
9493
 
8415
- let transformsPropertiesRegistered = null;
9494
+ // Motion path animation
8416
9495
 
8417
9496
  /**
8418
- * @param {String} propName
8419
- * @param {WAAPIKeyframeValue} value
8420
- * @param {DOMTarget} $el
8421
- * @param {Number} i
8422
- * @param {Number} targetsLength
8423
- * @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}
8424
9503
  */
8425
- const normalizeTweenValue = (propName, value, $el, i, targetsLength) => {
8426
- // Do not try to compute strings with getFunctionValue otherwise it will convert CSS variables
8427
- let v = isStr(value) ? value : getFunctionValue(/** @type {any} */(value), $el, i, targetsLength);
8428
- if (!isNum(v)) return v;
8429
- if (commonDefaultPXProperties.includes(propName) || stringStartsWith(propName, 'translate')) return `${v}px`;
8430
- if (stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew')) return `${v}deg`;
8431
- 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);
8432
9510
  };
8433
9511
 
8434
9512
  /**
8435
- * @param {DOMTarget} $el
8436
- * @param {String} propName
8437
- * @param {WAAPIKeyframeValue} from
8438
- * @param {WAAPIKeyframeValue} to
8439
- * @param {Number} i
8440
- * @param {Number} targetsLength
8441
- * @return {WAAPITweenValue}
9513
+ * @param {SVGGeometryElement} $path
9514
+ * @param {String} pathProperty
9515
+ * @param {Number} [offset=0]
9516
+ * @return {FunctionValue}
8442
9517
  */
8443
- const parseIndividualTweenValue = ($el, propName, from, to, i, targetsLength) => {
8444
- /** @type {WAAPITweenValue} */
8445
- let tweenValue = '0';
8446
- const computedTo = !isUnd(to) ? normalizeTweenValue(propName, to, $el, i, targetsLength) : getComputedStyle($el)[propName];
8447
- if (!isUnd(from)) {
8448
- const computedFrom = normalizeTweenValue(propName, from, $el, i, targetsLength);
8449
- tweenValue = [computedFrom, computedTo];
8450
- } else {
8451
- 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
+ }
8452
9544
  }
8453
- return tweenValue;
8454
9545
  };
8455
9546
 
8456
- class WAAPIAnimation {
8457
9547
  /**
8458
- * @param {DOMTargetsParam} targets
8459
- * @param {WAAPIAnimationParams} params
9548
+ * @param {TargetsParam} path
9549
+ * @param {Number} [offset=0]
8460
9550
  */
8461
- constructor(targets, params) {
8462
-
8463
- if (scope.current) scope.current.register(this);
8464
-
8465
- // Skip the registration and fallback to no animation in case CSS.registerProperty is not supported
8466
- if (isNil(transformsPropertiesRegistered)) {
8467
- if (isBrowser && (isUnd(CSS) || !Object.hasOwnProperty.call(CSS, 'registerProperty'))) {
8468
- transformsPropertiesRegistered = false;
8469
- } else {
8470
- validTransforms.forEach(t => {
8471
- const isSkew = stringStartsWith(t, 'skew');
8472
- const isScale = stringStartsWith(t, 'scale');
8473
- const isRotate = stringStartsWith(t, 'rotate');
8474
- const isTranslate = stringStartsWith(t, 'translate');
8475
- const isAngle = isRotate || isSkew;
8476
- const syntax = isAngle ? '<angle>' : isScale ? "<number>" : isTranslate ? "<length-percentage>" : "*";
8477
- try {
8478
- CSS.registerProperty({
8479
- name: '--' + t,
8480
- syntax,
8481
- inherits: false,
8482
- initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0',
8483
- });
8484
- } catch {} });
8485
- transformsPropertiesRegistered = true;
8486
- }
8487
- }
8488
-
8489
- const parsedTargets = registerTargets(targets);
8490
- const targetsLength = parsedTargets.length;
8491
-
8492
- if (!targetsLength) {
8493
- console.warn(`No target found. Make sure the element you're trying to animate is accessible before creating your animation.`);
8494
- }
8495
-
8496
- const ease = setValue(params.ease, parseWAAPIEasing(globals.defaults.ease));
8497
- const spring = /** @type {Spring} */(ease).ease && ease;
8498
- const autoplay = setValue(params.autoplay, globals.defaults.autoplay);
8499
- const scroll = autoplay && /** @type {ScrollObserver} */(autoplay).link ? autoplay : false;
8500
- const alternate = params.alternate && /** @type {Boolean} */(params.alternate) === true;
8501
- const reversed = params.reversed && /** @type {Boolean} */(params.reversed) === true;
8502
- const loop = setValue(params.loop, globals.defaults.loop);
8503
- const iterations = /** @type {Number} */((loop === true || loop === Infinity) ? Infinity : isNum(loop) ? loop + 1 : 1);
8504
- /** @type {PlaybackDirection} */
8505
- const direction = alternate ? reversed ? 'alternate-reverse' : 'alternate' : reversed ? 'reverse' : 'normal';
8506
- /** @type {FillMode} */
8507
- const fill = 'both'; // We use 'both' here because the animation can be reversed during playback
8508
- /** @type {String} */
8509
- const easing = parseWAAPIEasing(ease);
8510
- const timeScale = (globals.timeScale === 1 ? 1 : K);
8511
-
8512
- /** @type {DOMTargetsArray}] */
8513
- this.targets = parsedTargets;
8514
- /** @type {Array<globalThis.Animation>}] */
8515
- this.animations = [];
8516
- /** @type {globalThis.Animation}] */
8517
- this.controlAnimation = null;
8518
- /** @type {Callback<this>} */
8519
- this.onComplete = params.onComplete || /** @type {Callback<WAAPIAnimation>} */(/** @type {unknown} */(globals.defaults.onComplete));
8520
- /** @type {Number} */
8521
- this.duration = 0;
8522
- /** @type {Boolean} */
8523
- this.muteCallbacks = false;
8524
- /** @type {Boolean} */
8525
- this.completed = false;
8526
- /** @type {Boolean} */
8527
- this.paused = !autoplay || scroll !== false;
8528
- /** @type {Boolean} */
8529
- this.reversed = reversed;
8530
- /** @type {Boolean} */
8531
- this.persist = setValue(params.persist, globals.defaults.persist);
8532
- /** @type {Boolean|ScrollObserver} */
8533
- this.autoplay = autoplay;
8534
- /** @type {Number} */
8535
- this._speed = setValue(params.playbackRate, globals.defaults.playbackRate);
8536
- /** @type {Function} */
8537
- this._resolve = noop; // Used by .then()
8538
- /** @type {Number} */
8539
- this._completed = 0;
8540
- /** @type {Array.<Object>} */
8541
- this._inlineStyles = [];
8542
-
8543
- 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
+ };
8544
9560
 
8545
- const cachedTransforms = $el[transformsSymbol];
8546
- const hasIndividualTransforms = validIndividualTransforms.some(t => params.hasOwnProperty(t));
8547
- const elStyle = $el.style;
8548
- const inlineStyles = this._inlineStyles[i] = {};
8549
9561
 
8550
- /** @type {Number} */
8551
- const duration = (spring ? /** @type {Spring} */(spring).settlingDuration : getFunctionValue(setValue(params.duration, globals.defaults.duration), $el, i, targetsLength)) * timeScale;
8552
- /** @type {Number} */
8553
- const delay = getFunctionValue(setValue(params.delay, globals.defaults.delay), $el, i, targetsLength) * timeScale;
8554
- /** @type {CompositeOperation} */
8555
- const composite = /** @type {CompositeOperation} */(setValue(params.composition, 'replace'));
8556
9562
 
8557
- for (let name in params) {
8558
- if (!isKey(name)) continue;
8559
- /** @type {PropertyIndexedKeyframes} */
8560
- const keyframes = {};
8561
- /** @type {KeyframeAnimationOptions} */
8562
- const tweenParams = { iterations, direction, fill, easing, duration, delay, composite };
8563
- const propertyValue = params[name];
8564
- 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
+ };
8565
9579
 
8566
- const styleName = individualTransformProperty ? 'transform' : name;
8567
- if (!inlineStyles[styleName]) {
8568
- inlineStyles[styleName] = elStyle[styleName];
8569
- }
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;
8570
9594
 
8571
- let parsedPropertyValue;
8572
- if (isObj(propertyValue)) {
8573
- const tweenOptions = /** @type {WAAPITweenOptions} */(propertyValue);
8574
- const tweenOptionsEase = setValue(tweenOptions.ease, ease);
8575
- const tweenOptionsSpring = /** @type {Spring} */(tweenOptionsEase).ease && tweenOptionsEase;
8576
- const to = /** @type {WAAPITweenOptions} */(tweenOptions).to;
8577
- const from = /** @type {WAAPITweenOptions} */(tweenOptions).from;
8578
- /** @type {Number} */
8579
- tweenParams.duration = (tweenOptionsSpring ? /** @type {Spring} */(tweenOptionsSpring).settlingDuration : getFunctionValue(setValue(tweenOptions.duration, duration), $el, i, targetsLength)) * timeScale;
8580
- /** @type {Number} */
8581
- tweenParams.delay = getFunctionValue(setValue(tweenOptions.delay, delay), $el, i, targetsLength) * timeScale;
8582
- /** @type {CompositeOperation} */
8583
- tweenParams.composite = /** @type {CompositeOperation} */(setValue(tweenOptions.composition, composite));
8584
- /** @type {String} */
8585
- tweenParams.easing = parseWAAPIEasing(tweenOptionsEase);
8586
- parsedPropertyValue = parseIndividualTweenValue($el, name, from, to, i, targetsLength);
8587
- if (individualTransformProperty) {
8588
- keyframes[`--${individualTransformProperty}`] = parsedPropertyValue;
8589
- cachedTransforms[individualTransformProperty] = parsedPropertyValue;
8590
- } else {
8591
- keyframes[name] = parseIndividualTweenValue($el, name, from, to, i, targetsLength);
8592
- }
8593
- addWAAPIAnimation(this, $el, name, keyframes, tweenParams);
8594
- if (!isUnd(from)) {
8595
- if (!individualTransformProperty) {
8596
- elStyle[name] = keyframes[name][0];
8597
- } else {
8598
- const key = `--${individualTransformProperty}`;
8599
- 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
+ }
8600
9621
  }
9622
+ target.setAttribute('stroke-dashoffset', `${os}`);
9623
+ target.setAttribute('stroke-dasharray', `${d1} ${d2}`);
8601
9624
  }
8602
- } else {
8603
- parsedPropertyValue = isArr(propertyValue) ?
8604
- propertyValue.map((/** @type {any} */v) => normalizeTweenValue(name, v, $el, i, targetsLength)) :
8605
- normalizeTweenValue(name, /** @type {any} */(propertyValue), $el, i, targetsLength);
8606
- if (individualTransformProperty) {
8607
- keyframes[`--${individualTransformProperty}`] = parsedPropertyValue;
8608
- cachedTransforms[individualTransformProperty] = parsedPropertyValue;
8609
- } else {
8610
- keyframes[name] = parsedPropertyValue;
8611
- }
8612
- addWAAPIAnimation(this, $el, name, keyframes, tweenParams);
8613
- }
8614
- }
8615
- if (hasIndividualTransforms) {
8616
- let transforms = emptyString;
8617
- for (let t in cachedTransforms) {
8618
- transforms += `${transformsFragmentStrings[t]}var(--${t})) `;
8619
- }
8620
- elStyle.transform = transforms;
9625
+ return Reflect.apply(value, target, args);
9626
+ };
8621
9627
  }
8622
- });
8623
9628
 
8624
- if (scroll) {
8625
- /** @type {ScrollObserver} */(this.autoplay).link(this);
9629
+ if (isFnc(value)) {
9630
+ return (...args) => Reflect.apply(value, target, args);
9631
+ } else {
9632
+ return value;
9633
+ }
8626
9634
  }
9635
+ });
9636
+
9637
+ if ($el.getAttribute('pathLength') !== `${pathLength}`) {
9638
+ $el.setAttribute('pathLength', `${pathLength}`);
9639
+ proxy.setAttribute('draw', `${start} ${end}`);
8627
9640
  }
8628
9641
 
8629
- /**
8630
- * @callback forEachCallback
8631
- * @param {globalThis.Animation} animation
8632
- */
9642
+ return /** @type {DrawableSVGGeometry} */(proxy);
9643
+ };
8633
9644
 
8634
- /**
8635
- * @param {forEachCallback|String} callback
8636
- * @return {this}
8637
- */
8638
- forEach(callback) {
8639
- const cb = isStr(callback) ? (/** @type {globalThis.Animation} */a) => a[callback]() : callback;
8640
- this.animations.forEach(cb);
8641
- return this;
8642
- }
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
+ };
8643
9660
 
8644
- get speed() {
8645
- return this._speed;
8646
- }
8647
9661
 
8648
- set speed(speed) {
8649
- this._speed = +speed;
8650
- this.forEach(anim => anim.playbackRate = speed);
8651
- }
8652
9662
 
8653
- get currentTime() {
8654
- const controlAnimation = this.controlAnimation;
8655
- const timeScale = globals.timeScale;
8656
- 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>.`);
8657
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);
8658
9685
 
8659
- set currentTime(time) {
8660
- const t = time * (globals.timeScale === 1 ? 1 : K);
8661
- this.forEach(anim => {
8662
- // Make sure the animation playState is not 'paused' in order to properly trigger an onfinish callback.
8663
- // 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.
8664
- // https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish_event
8665
- // This is not needed for persisting animations since they never finish.
8666
- if (!this.persist && t >= this.duration) anim.play();
8667
- anim.currentTime = t;
8668
- });
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
+ }
8669
9703
  }
8670
9704
 
8671
- get progress() {
8672
- return this.currentTime / this.duration;
8673
- }
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
+ */
8674
9734
 
8675
- set progress(progress) {
8676
- this.forEach(anim => anim.currentTime = progress * this.duration || 0);
8677
- }
9735
+ /**
9736
+ * @typedef {Object} Segmenter
9737
+ * @property {function(String): Iterable<Segment>} segment
9738
+ */
8678
9739
 
8679
- resume() {
8680
- if (!this.paused) return this;
8681
- this.paused = false;
8682
- // TODO: Store the current time, and seek back to the last position
8683
- return this.forEach('play');
8684
- }
9740
+ /** @type {Segmenter} */
9741
+ let wordSegmenter = null;
9742
+ /** @type {Segmenter} */
9743
+ let graphemeSegmenter = null;
9744
+ let $splitTemplate = null;
8685
9745
 
8686
- pause() {
8687
- if (this.paused) return this;
8688
- this.paused = true;
8689
- return this.forEach('pause');
8690
- }
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
+ };
8691
9755
 
8692
- alternate() {
8693
- this.reversed = !this.reversed;
8694
- this.forEach('reverse');
8695
- if (this.paused) this.forEach('pause');
8696
- return this;
8697
- }
9756
+ /**
9757
+ * @param {HTMLElement} $el
9758
+ */
9759
+ const setAriaHidden = $el => $el.setAttribute('aria-hidden', 'true');
8698
9760
 
8699
- play() {
8700
- if (this.reversed) this.alternate();
8701
- return this.resume();
8702
- }
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}])`))];
8703
9767
 
8704
- reverse() {
8705
- if (!this.reversed) this.alternate();
8706
- 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);
8707
9778
  }
9779
+ };
8708
9780
 
8709
- /**
8710
- * @param {Number} time
8711
- * @param {Boolean} muteCallbacks
8712
- */
8713
- seek(time, muteCallbacks = false) {
8714
- if (muteCallbacks) this.muteCallbacks = true;
8715
- if (time < this.duration) this.completed = false;
8716
- this.currentTime = time;
8717
- this.muteCallbacks = false;
8718
- if (this.paused) this.pause();
8719
- 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
+ }
8720
9800
  }
9801
+ let i = $el.childElementCount;
9802
+ while (i--) filterLineElements(/** @type {HTMLElement} */($el.children[i]), lineIndex, bin);
9803
+ return bin;
9804
+ };
8721
9805
 
8722
- restart() {
8723
- this.completed = false;
8724
- 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}`;
8725
9826
  }
9827
+ template += `</span>`;
9828
+ if (wrapType) template += `</span>`;
9829
+ return template;
9830
+ };
8726
9831
 
8727
- commitStyles() {
8728
- 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.`);
8729
9881
  }
9882
+ if (debug) $highestParent.style.outline = `1px dotted ${debugColors[type]}`;
9883
+ return $highestParent;
9884
+ };
8730
9885
 
8731
- complete() {
8732
- 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.');
8733
9961
  }
8734
9962
 
8735
- cancel() {
8736
- this.muteCallbacks = true; // This prevents triggering the onComplete callback and resolving the Promise
8737
- 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;
8738
9973
  }
8739
9974
 
8740
9975
  revert() {
8741
- // NOTE: We need a better way to revert the transforms, since right now the entire transform property value is reverted,
8742
- // This means if you have multiple animations animating different transforms on the same target,
8743
- // reverting one of them will also override the transform property of the other animations.
8744
- // A better approach would be to store the original custom property values is they exist instead of the entire transform value,
8745
- // and update the CSS variables with the orignal value
8746
- this.cancel().targets.forEach(($el, i) => {
8747
- const targetStyle = $el.style;
8748
- const targetInlineStyles = this._inlineStyles[i];
8749
- for (let name in targetInlineStyles) {
8750
- const originalInlinedValue = targetInlineStyles[name];
8751
- if (isUnd(originalInlinedValue) || originalInlinedValue === emptyString) {
8752
- targetStyle.removeProperty(toLowerCase(name));
8753
- } else {
8754
- targetStyle[name] = originalInlinedValue;
8755
- }
8756
- }
8757
- // Remove style attribute if empty
8758
- if ($el.getAttribute('style') === emptyString) $el.removeAttribute('style');
8759
- });
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;
8760
9982
  return this;
8761
9983
  }
8762
9984
 
8763
9985
  /**
8764
- * @typedef {this & {then: null}} ResolvedWAAPIAnimation
9986
+ * Recursively processes a node and its children
9987
+ * @param {Node} node
8765
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
+ }
8766
10067
 
8767
10068
  /**
8768
- * @param {Callback<ResolvedWAAPIAnimation>} [callback]
8769
- * @return Promise<this>
10069
+ * @param {Boolean} clearCache
10070
+ * @return {this}
8770
10071
  */
8771
- then(callback = noop) {
8772
- const then = this.then;
8773
- const onResolve = () => {
8774
- this.then = null;
8775
- callback(/** @type {ResolvedWAAPIAnimation} */(this));
8776
- this.then = then;
8777
- this._resolve = noop;
8778
- };
8779
- return new Promise(r => {
8780
- this._resolve = () => r(onResolve());
8781
- if (this.completed) this._resolve();
8782
- return this;
8783
- });
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);
8784
10170
  }
8785
10171
  }
8786
10172
 
8787
- const waapi = {
8788
10173
  /**
8789
- * @param {DOMTargetsParam} targets
8790
- * @param {WAAPIAnimationParams} params
8791
- * @return {WAAPIAnimation}
10174
+ * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
10175
+ * @param {TextSplitterParams} [parameters]
10176
+ * @return {TextSplitter}
8792
10177
  */
8793
- animate: (targets, params) => new WAAPIAnimation(targets, params),
8794
- 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);
8795
10190
  };
8796
10191
 
8797
- 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 };