animejs 4.2.2 → 4.3.0-beta.0

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