animejs 4.0.1 → 4.1.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.
package/types/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * anime.js - ESM
3
- * @version v4.0.1
3
+ * @version v4.1.0
4
4
  * @author Julian Garnier
5
5
  * @license MIT
6
6
  * @copyright (c) 2025 Julian Garnier
@@ -56,10 +56,13 @@ const K = 1e3;
56
56
  const maxFps = 120;
57
57
  // Strings
58
58
  const emptyString = '';
59
- const shortTransforms = new Map();
60
- shortTransforms.set('x', 'translateX');
61
- shortTransforms.set('y', 'translateY');
62
- shortTransforms.set('z', 'translateZ');
59
+ const shortTransforms = /*#__PURE__*/ (() => {
60
+ const map = new Map();
61
+ map.set('x', 'translateX');
62
+ map.set('y', 'translateY');
63
+ map.set('z', 'translateZ');
64
+ return map;
65
+ })();
63
66
  const validTransforms = [
64
67
  'translateX',
65
68
  'translateY',
@@ -79,7 +82,7 @@ const validTransforms = [
79
82
  'matrix',
80
83
  'matrix3d',
81
84
  ];
82
- const transformsFragmentStrings = validTransforms.reduce((a, v) => ({ ...a, [v]: v + '(' }), {});
85
+ const transformsFragmentStrings = /*#__PURE__*/ validTransforms.reduce((a, v) => ({ ...a, [v]: v + '(' }), {});
83
86
  // Functions
84
87
  /** @return {void} */
85
88
  const noop = () => { };
@@ -123,13 +126,15 @@ const defaults = {
123
126
  onComplete: noop,
124
127
  onRender: noop,
125
128
  };
129
+ const scope = {
130
+ /** @type {Scope} */
131
+ current: null,
132
+ /** @type {Document|DOMTarget} */
133
+ root: doc,
134
+ };
126
135
  const globals = {
127
136
  /** @type {DefaultsParams} */
128
137
  defaults,
129
- /** @type {Document|DOMTarget} */
130
- root: doc,
131
- /** @type {Scope} */
132
- scope: null,
133
138
  /** @type {Number} */
134
139
  precision: 4,
135
140
  /** @type {Number} */
@@ -137,7 +142,7 @@ const globals = {
137
142
  /** @type {Number} */
138
143
  tickThreshold: 200,
139
144
  };
140
- const globalVersions = { version: '4.0.1', engine: null };
145
+ const globalVersions = { version: '4.1.0', engine: null };
141
146
  if (isBrowser) {
142
147
  if (!win.AnimeJS)
143
148
  win.AnimeJS = [];
@@ -244,6 +249,28 @@ const snap = (v, increment) => isArr(increment) ? increment.reduce((closest, cv)
244
249
  * @return {Number}
245
250
  */
246
251
  const interpolate = (start, end, progress) => start + (end - start) * progress;
252
+ /**
253
+ * @param {Number} min
254
+ * @param {Number} max
255
+ * @param {Number} [decimalLength]
256
+ * @return {Number}
257
+ */
258
+ const random = (min, max, decimalLength) => { const m = 10 ** (decimalLength || 0); return floor((Math.random() * (max - min + (1 / m)) + min) * m) / m; };
259
+ /**
260
+ * Adapted from https://bost.ocks.org/mike/shuffle/
261
+ * @param {Array} items
262
+ * @return {Array}
263
+ */
264
+ const shuffle = items => {
265
+ let m = items.length, t, i;
266
+ while (m) {
267
+ i = random(0, --m);
268
+ t = items[m];
269
+ items[m] = items[i];
270
+ items[i] = t;
271
+ }
272
+ return items;
273
+ };
247
274
  /**
248
275
  * @param {Number} v
249
276
  * @return {Number}
@@ -253,7 +280,7 @@ const clampInfinity = v => v === Infinity ? maxValue : v === -Infinity ? -1e12 :
253
280
  * @param {Number} v
254
281
  * @return {Number}
255
282
  */
256
- const clampZero = v => v < minValue ? minValue : v;
283
+ const normalizeTime = v => v <= minValue ? minValue : clampInfinity(round(v, 11));
257
284
  // Arrays
258
285
  /**
259
286
  * @template T
@@ -332,6 +359,32 @@ const addChild = (parent, child, sortMethod, prevProp = '_prev', nextProp = '_ne
332
359
  child[prevProp] = prev;
333
360
  child[nextProp] = next;
334
361
  };
362
+ /**
363
+ * @param {(...args: any[]) => Tickable | ((...args: any[]) => void)} constructor
364
+ * @return {(...args: any[]) => Tickable | ((...args: any[]) => void)}
365
+ */
366
+ const createRefreshable = constructor => {
367
+ /** @type {Tickable} */
368
+ let tracked;
369
+ return (...args) => {
370
+ let currentIteration, currentIterationProgress, reversed, alternate;
371
+ if (tracked) {
372
+ currentIteration = tracked.currentIteration;
373
+ currentIterationProgress = tracked.iterationProgress;
374
+ reversed = tracked.reversed;
375
+ alternate = tracked._alternate;
376
+ tracked.revert();
377
+ }
378
+ const cleanup = constructor(...args);
379
+ if (cleanup && !isFnc(cleanup) && cleanup.revert)
380
+ tracked = cleanup;
381
+ if (!isUnd(currentIterationProgress)) {
382
+ /** @type {Tickable} */ (tracked).currentIteration = currentIteration;
383
+ /** @type {Tickable} */ (tracked).iterationProgress = (alternate ? !(currentIteration % 2) ? reversed : !reversed : reversed) ? 1 - currentIterationProgress : currentIterationProgress;
384
+ }
385
+ return cleanup || noop;
386
+ };
387
+ };
335
388
 
336
389
  /*
337
390
  * Base class to control framerate and playback rate.
@@ -801,8 +854,8 @@ const addAdditiveAnimation = lookups => {
801
854
  return animation;
802
855
  };
803
856
 
804
- const engineTickMethod = isBrowser ? requestAnimationFrame : setImmediate;
805
- const engineCancelMethod = isBrowser ? cancelAnimationFrame : clearImmediate;
857
+ const engineTickMethod = /*#__PURE__*/ (() => isBrowser ? requestAnimationFrame : setImmediate)();
858
+ const engineCancelMethod = /*#__PURE__*/ (() => isBrowser ? cancelAnimationFrame : clearImmediate)();
806
859
  class Engine extends Clock {
807
860
  /** @param {Number} [initTime] */
808
861
  constructor(initTime) {
@@ -958,7 +1011,7 @@ const parseInlineTransforms = (target, propName, animationInlineStyles) => {
958
1011
  * @return {NodeList|HTMLCollection}
959
1012
  */
960
1013
  function getNodeList(v) {
961
- const n = isStr(v) ? globals.root.querySelectorAll(v) : v;
1014
+ const n = isStr(v) ? scope.root.querySelectorAll(v) : v;
962
1015
  if (n instanceof NodeList || n instanceof HTMLCollection)
963
1016
  return n;
964
1017
  }
@@ -1112,14 +1165,34 @@ const morphTo = (path2, precision = .33) => ($path1) => {
1112
1165
  return [v1, v2];
1113
1166
  };
1114
1167
  /**
1115
- * @param {SVGGeometryElement} $el
1116
- * @param {Number} start
1117
- * @param {Number} end
1118
- * @return {Proxy}
1168
+ * @param {SVGGeometryElement} [$el]
1169
+ * @return {Number}
1119
1170
  */
1120
- function createDrawableProxy($el, start, end) {
1121
- const strokeLineCap = getComputedStyle($el).strokeLinecap;
1171
+ const getScaleFactor = $el => {
1172
+ let scaleFactor = 1;
1173
+ if ($el && $el.getCTM) {
1174
+ const ctm = $el.getCTM();
1175
+ if (ctm) {
1176
+ const scaleX = sqrt(ctm.a * ctm.a + ctm.b * ctm.b);
1177
+ const scaleY = sqrt(ctm.c * ctm.c + ctm.d * ctm.d);
1178
+ scaleFactor = (scaleX + scaleY) / 2;
1179
+ }
1180
+ }
1181
+ return scaleFactor;
1182
+ };
1183
+ /**
1184
+ * Creates a proxy that wraps an SVGGeometryElement and adds drawing functionality.
1185
+ * @param {SVGGeometryElement} $el - The SVG element to transform into a drawable
1186
+ * @param {number} start - Starting position (0-1)
1187
+ * @param {number} end - Ending position (0-1)
1188
+ * @return {DrawableSVGGeometry} - Returns a proxy that preserves the original element's type with additional 'draw' attribute functionality
1189
+ */
1190
+ const createDrawableProxy = ($el, start, end) => {
1122
1191
  const pathLength = K;
1192
+ const computedStyles = getComputedStyle($el);
1193
+ const strokeLineCap = computedStyles.strokeLinecap;
1194
+ // @ts-ignore
1195
+ const $scalled = computedStyles.vectorEffect === 'non-scaling-stroke' ? $el : null;
1123
1196
  let currentCap = strokeLineCap;
1124
1197
  const proxy = new Proxy($el, {
1125
1198
  get(target, property) {
@@ -1127,7 +1200,6 @@ function createDrawableProxy($el, start, end) {
1127
1200
  if (property === proxyTargetSymbol)
1128
1201
  return target;
1129
1202
  if (property === 'setAttribute') {
1130
- /** @param {any[]} args */
1131
1203
  return (...args) => {
1132
1204
  if (args[0] === 'draw') {
1133
1205
  const value = args[1];
@@ -1138,15 +1210,15 @@ function createDrawableProxy($el, start, end) {
1138
1210
  // const spaceIndex = value.indexOf(' ');
1139
1211
  // const v1 = round(+value.slice(0, spaceIndex), precision);
1140
1212
  // const v2 = round(+value.slice(spaceIndex + 1), precision);
1141
- const os = v1 * -1e3;
1142
- const d1 = (v2 * pathLength) + os;
1143
- // Prevents linecap to smear by offsetting the dasharray length by 0.01% when v2 is not at max
1144
- const d2 = (pathLength + ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10) - d1);
1145
- // Handle cases where the cap is still visible when the line is completly hidden
1213
+ const scaleFactor = getScaleFactor($scalled);
1214
+ const os = v1 * -1e3 * scaleFactor;
1215
+ const d1 = (v2 * pathLength * scaleFactor) + os;
1216
+ const d2 = (pathLength * scaleFactor +
1217
+ ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10 * scaleFactor) - d1);
1146
1218
  if (strokeLineCap !== 'butt') {
1147
1219
  const newCap = v1 === v2 ? 'butt' : strokeLineCap;
1148
1220
  if (currentCap !== newCap) {
1149
- target.setAttribute('stroke-linecap', `${newCap}`);
1221
+ target.style.strokeLinecap = `${newCap}`;
1150
1222
  currentCap = newCap;
1151
1223
  }
1152
1224
  }
@@ -1157,7 +1229,6 @@ function createDrawableProxy($el, start, end) {
1157
1229
  };
1158
1230
  }
1159
1231
  if (isFnc(value)) {
1160
- /** @param {any[]} args */
1161
1232
  return (...args) => Reflect.apply(value, target, args);
1162
1233
  }
1163
1234
  else {
@@ -1169,18 +1240,19 @@ function createDrawableProxy($el, start, end) {
1169
1240
  $el.setAttribute('pathLength', `${pathLength}`);
1170
1241
  proxy.setAttribute('draw', `${start} ${end}`);
1171
1242
  }
1172
- return /** @type {typeof Proxy} */ ( /** @type {unknown} */(proxy));
1173
- }
1243
+ return /** @type {DrawableSVGGeometry} */ (proxy);
1244
+ };
1174
1245
  /**
1175
- * @param {TargetsParam} selector
1176
- * @param {Number} [start=0]
1177
- * @param {Number} [end=0]
1178
- * @return {Array.<Proxy>}
1246
+ * Creates drawable proxies for multiple SVG elements.
1247
+ * @param {TargetsParam} selector - CSS selector, SVG element, or array of elements and selectors
1248
+ * @param {number} [start=0] - Starting position (0-1)
1249
+ * @param {number} [end=0] - Ending position (0-1)
1250
+ * @return {Array<DrawableSVGGeometry>} - Array of proxied elements with drawing functionality
1179
1251
  */
1180
1252
  const createDrawable = (selector, start = 0, end = 0) => {
1181
- const els = /** @type {Array.<Proxy>} */ (( /** @type {unknown} */(parseTargets(selector))));
1182
- els.forEach(($el, i) => els[i] = createDrawableProxy(/** @type {SVGGeometryElement} */ ( /** @type {unknown} */($el)), start, end));
1183
- return els;
1253
+ const els = parseTargets(selector);
1254
+ return els.map($el => createDrawableProxy(
1255
+ /** @type {SVGGeometryElement} */ ($el), start, end));
1184
1256
  };
1185
1257
  // Motion path animation
1186
1258
  /**
@@ -1784,8 +1856,8 @@ class Timer extends Clock {
1784
1856
  constructor(parameters = {}, parent = null, parentPosition = 0) {
1785
1857
  super(0);
1786
1858
  const { id, delay, duration, reversed, alternate, loop, loopDelay, autoplay, frameRate, playbackRate, onComplete, onLoop, onPause, onBegin, onBeforeUpdate, onUpdate, } = parameters;
1787
- if (globals.scope)
1788
- globals.scope.revertibles.push(this);
1859
+ if (scope.current)
1860
+ scope.current.register(this);
1789
1861
  const timerInitTime = parent ? 0 : engine._elapsedTime;
1790
1862
  const timerDefaults = parent ? parent.defaults : globals.defaults;
1791
1863
  const timerDelay = /** @type {Number} */ (isFnc(delay) || isUnd(delay) ? timerDefaults.delay : +delay);
@@ -1905,14 +1977,14 @@ class Timer extends Clock {
1905
1977
  this.currentTime = (this.iterationDuration * this._currentIteration) + time;
1906
1978
  }
1907
1979
  get progress() {
1908
- return clamp(round(this._currentTime / this.duration, 5), 0, 1);
1980
+ return clamp(round(this._currentTime / this.duration, 10), 0, 1);
1909
1981
  }
1910
1982
  /** @param {Number} progress */
1911
1983
  set progress(progress) {
1912
1984
  this.currentTime = this.duration * progress;
1913
1985
  }
1914
1986
  get iterationProgress() {
1915
- return clamp(round(this._iterationTime / this.iterationDuration, 5), 0, 1);
1987
+ return clamp(round(this._iterationTime / this.iterationDuration, 10), 0, 1);
1916
1988
  }
1917
1989
  /** @param {Number} progress */
1918
1990
  set iterationProgress(progress) {
@@ -2093,12 +2165,13 @@ class Timer extends Clock {
2093
2165
  */
2094
2166
  stretch(newDuration) {
2095
2167
  const currentDuration = this.duration;
2096
- if (currentDuration === clampZero(newDuration))
2168
+ const normlizedDuration = normalizeTime(newDuration);
2169
+ if (currentDuration === normlizedDuration)
2097
2170
  return this;
2098
2171
  const timeScale = newDuration / currentDuration;
2099
2172
  const isSetter = newDuration <= minValue;
2100
- this.duration = isSetter ? minValue : clampZero(clampInfinity(round(currentDuration * timeScale, 12)));
2101
- this.iterationDuration = isSetter ? minValue : clampZero(clampInfinity(round(this.iterationDuration * timeScale, 12)));
2173
+ this.duration = isSetter ? minValue : normlizedDuration;
2174
+ this.iterationDuration = isSetter ? minValue : normalizeTime(this.iterationDuration * timeScale);
2102
2175
  this._offset *= timeScale;
2103
2176
  this._delay *= timeScale;
2104
2177
  this._loopDelay *= timeScale;
@@ -3051,14 +3124,14 @@ class JSAnimation extends Timer {
3051
3124
  */
3052
3125
  stretch(newDuration) {
3053
3126
  const currentDuration = this.duration;
3054
- if (currentDuration === clampZero(newDuration))
3127
+ if (currentDuration === normalizeTime(newDuration))
3055
3128
  return this;
3056
3129
  const timeScale = newDuration / currentDuration;
3057
3130
  // NOTE: Find a better way to handle the stretch of an animation after stretch = 0
3058
3131
  forEachChildren(this, (/** @type {Tween} */ tween) => {
3059
3132
  // Rounding is necessary here to minimize floating point errors
3060
- tween._updateDuration = clampZero(round(tween._updateDuration * timeScale, 12));
3061
- tween._changeDuration = clampZero(round(tween._changeDuration * timeScale, 12));
3133
+ tween._updateDuration = normalizeTime(tween._updateDuration * timeScale);
3134
+ tween._changeDuration = normalizeTime(tween._changeDuration * timeScale);
3062
3135
  tween._currentTime *= timeScale;
3063
3136
  tween._startTime *= timeScale;
3064
3137
  tween._absoluteStartTime *= timeScale;
@@ -3070,12 +3143,13 @@ class JSAnimation extends Timer {
3070
3143
  */
3071
3144
  refresh() {
3072
3145
  forEachChildren(this, (/** @type {Tween} */ tween) => {
3073
- const ogValue = getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType);
3074
- decomposeRawValue(ogValue, decomposedOriginalValue);
3075
- tween._fromNumbers = cloneArray(decomposedOriginalValue.d);
3076
- tween._fromNumber = decomposedOriginalValue.n;
3077
- if (tween._func) {
3078
- decomposeRawValue(tween._func(), toTargetObject);
3146
+ const tweenFunc = tween._func;
3147
+ if (tweenFunc) {
3148
+ const ogValue = getOriginalAnimatableValue(tween.target, tween.property, tween._tweenType);
3149
+ decomposeRawValue(ogValue, decomposedOriginalValue);
3150
+ decomposeRawValue(tweenFunc(), toTargetObject);
3151
+ tween._fromNumbers = cloneArray(decomposedOriginalValue.d);
3152
+ tween._fromNumber = decomposedOriginalValue.n;
3079
3153
  tween._toNumbers = cloneArray(toTargetObject.d);
3080
3154
  tween._strings = cloneArray(toTargetObject.s);
3081
3155
  tween._toNumber = toTargetObject.n;
@@ -3154,6 +3228,7 @@ const parseWAAPIEasing = (ease) => {
3154
3228
  if (isFnc(parsed))
3155
3229
  parsedEase = parsed === none ? 'linear' : easingToLinear(parsed);
3156
3230
  }
3231
+ WAAPIEasesLookups[ease] = parsedEase;
3157
3232
  }
3158
3233
  else if (isFnc(ease)) {
3159
3234
  const easing = easingToLinear(ease);
@@ -3163,7 +3238,7 @@ const parseWAAPIEasing = (ease) => {
3163
3238
  else if ( /** @type {Spring} */(ease).ease) {
3164
3239
  parsedEase = easingToLinear(/** @type {Spring} */ (ease).ease);
3165
3240
  }
3166
- return WAAPIEasesLookups[ease] = parsedEase;
3241
+ return parsedEase;
3167
3242
  };
3168
3243
  /**
3169
3244
  * @typedef {String|Number|Array<String>|Array<Number>} WAAPITweenValue
@@ -3222,26 +3297,8 @@ const commonDefaultPXProperties = [
3222
3297
  'borderRadius',
3223
3298
  ...transformsShorthands
3224
3299
  ];
3225
- const validIndividualTransforms = [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))];
3226
- // Setting it to true in case CSS.registerProperty is not supported will automatically skip the registration and fallback to no animation
3227
- let transformsPropertiesRegistered = isBrowser && (isUnd(CSS) || !Object.hasOwnProperty.call(CSS, 'registerProperty'));
3228
- const registerTransformsProperties = () => {
3229
- validTransforms.forEach(t => {
3230
- const isSkew = stringStartsWith(t, 'skew');
3231
- const isScale = stringStartsWith(t, 'scale');
3232
- const isRotate = stringStartsWith(t, 'rotate');
3233
- const isTranslate = stringStartsWith(t, 'translate');
3234
- const isAngle = isRotate || isSkew;
3235
- const syntax = isAngle ? '<angle>' : isScale ? "<number>" : isTranslate ? "<length-percentage>" : "*";
3236
- CSS.registerProperty({
3237
- name: '--' + t,
3238
- syntax,
3239
- inherits: false,
3240
- initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0',
3241
- });
3242
- });
3243
- transformsPropertiesRegistered = true;
3244
- };
3300
+ const validIndividualTransforms = /*#__PURE__*/ (() => [...transformsShorthands, ...validTransforms.filter(t => ['X', 'Y', 'Z'].some(axis => t.endsWith(axis)))])();
3301
+ let transformsPropertiesRegistered = null;
3245
3302
  const WAAPIAnimationsLookups = {
3246
3303
  _head: null,
3247
3304
  _tail: null,
@@ -3354,10 +3411,34 @@ class WAAPIAnimation {
3354
3411
  * @param {WAAPIAnimationParams} params
3355
3412
  */
3356
3413
  constructor(targets, params) {
3357
- if (globals.scope)
3358
- globals.scope.revertibles.push(this);
3359
- if (!transformsPropertiesRegistered)
3360
- registerTransformsProperties();
3414
+ if (scope.current)
3415
+ scope.current.register(this);
3416
+ // Skip the registration and fallback to no animation in case CSS.registerProperty is not supported
3417
+ if (isNil(transformsPropertiesRegistered)) {
3418
+ if (isBrowser && (isUnd(CSS) || !Object.hasOwnProperty.call(CSS, 'registerProperty'))) {
3419
+ transformsPropertiesRegistered = false;
3420
+ }
3421
+ else {
3422
+ validTransforms.forEach(t => {
3423
+ const isSkew = stringStartsWith(t, 'skew');
3424
+ const isScale = stringStartsWith(t, 'scale');
3425
+ const isRotate = stringStartsWith(t, 'rotate');
3426
+ const isTranslate = stringStartsWith(t, 'translate');
3427
+ const isAngle = isRotate || isSkew;
3428
+ const syntax = isAngle ? '<angle>' : isScale ? "<number>" : isTranslate ? "<length-percentage>" : "*";
3429
+ try {
3430
+ CSS.registerProperty({
3431
+ name: '--' + t,
3432
+ syntax,
3433
+ inherits: false,
3434
+ initialValue: isTranslate ? '0px' : isAngle ? '0deg' : isScale ? '1' : '0',
3435
+ });
3436
+ }
3437
+ catch { }
3438
+ });
3439
+ transformsPropertiesRegistered = true;
3440
+ }
3441
+ }
3361
3442
  const parsedTargets = registerTargets(targets);
3362
3443
  const targetsLength = parsedTargets.length;
3363
3444
  if (!targetsLength) {
@@ -3513,7 +3594,14 @@ class WAAPIAnimation {
3513
3594
  /** @param {Number} time */
3514
3595
  set currentTime(time) {
3515
3596
  const t = time * (globals.timeScale === 1 ? 1 : K);
3516
- this.forEach(anim => anim.currentTime = t);
3597
+ this.forEach(anim => {
3598
+ // Make sure the animation playState is not 'paused' in order to properly trigger an onfinish callback.
3599
+ // 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.
3600
+ // https://developer.mozilla.org/en-US/docs/Web/API/Animation/finish_event
3601
+ if (t >= this.duration)
3602
+ anim.play();
3603
+ anim.currentTime = t;
3604
+ });
3517
3605
  }
3518
3606
  get progress() {
3519
3607
  return this.currentTime / this.duration;
@@ -3783,32 +3871,15 @@ const remove = (targets, renderable, propertyName) => {
3783
3871
  return targetsArray;
3784
3872
  };
3785
3873
  /**
3786
- * @param {Number} min
3787
- * @param {Number} max
3788
- * @param {Number} [decimalLength]
3789
- * @return {Number}
3874
+ * @param {(...args: any[]) => Tickable} constructor
3875
+ * @return {(...args: any[]) => Tickable}
3790
3876
  */
3791
- const random = (min, max, decimalLength) => { const m = 10 ** (decimalLength || 0); return floor((Math.random() * (max - min + (1 / m)) + min) * m) / m; };
3877
+ const keepTime = createRefreshable;
3792
3878
  /**
3793
3879
  * @param {String|Array} items
3794
3880
  * @return {any}
3795
3881
  */
3796
3882
  const randomPick = items => items[random(0, items.length - 1)];
3797
- /**
3798
- * Adapted from https://bost.ocks.org/mike/shuffle/
3799
- * @param {Array} items
3800
- * @return {Array}
3801
- */
3802
- const shuffle = items => {
3803
- let m = items.length, t, i;
3804
- while (m) {
3805
- i = random(0, --m);
3806
- t = items[m];
3807
- items[m] = items[i];
3808
- items[i] = t;
3809
- }
3810
- return items;
3811
- };
3812
3883
  /**
3813
3884
  * @param {Number|String} v
3814
3885
  * @param {Number} decimalLength
@@ -3989,6 +4060,7 @@ const utils = {
3989
4060
  shuffle,
3990
4061
  lerp,
3991
4062
  sync,
4063
+ keepTime,
3992
4064
  clamp: /** @type {typeof clamp & ChainedClamp} */ (makeChainable(clamp)),
3993
4065
  round: /** @type {typeof round & ChainedRound} */ (makeChainable(round)),
3994
4066
  snap: /** @type {typeof snap & ChainedSnap} */ (makeChainable(snap)),
@@ -4260,16 +4332,13 @@ class Timeline extends Timer {
4260
4332
  */
4261
4333
  stretch(newDuration) {
4262
4334
  const currentDuration = this.duration;
4263
- if (currentDuration === clampZero(newDuration))
4335
+ if (currentDuration === normalizeTime(newDuration))
4264
4336
  return this;
4265
4337
  const timeScale = newDuration / currentDuration;
4266
4338
  const labels = this.labels;
4267
- forEachChildren(this, (/** @type {JSAnimation} */ child) => {
4268
- child.stretch(child.duration * timeScale);
4269
- });
4270
- for (let labelName in labels) {
4339
+ forEachChildren(this, (/** @type {JSAnimation} */ child) => child.stretch(child.duration * timeScale));
4340
+ for (let labelName in labels)
4271
4341
  labels[labelName] *= timeScale;
4272
- }
4273
4342
  return super.stretch(newDuration);
4274
4343
  }
4275
4344
  /**
@@ -4311,8 +4380,8 @@ class Animatable {
4311
4380
  * @param {AnimatableParams} parameters
4312
4381
  */
4313
4382
  constructor(targets, parameters) {
4314
- if (globals.scope)
4315
- globals.scope.revertibles.push(this);
4383
+ if (scope.current)
4384
+ scope.current.register(this);
4316
4385
  /** @type {AnimationParams} */
4317
4386
  const globalParams = {};
4318
4387
  const properties = {};
@@ -4620,11 +4689,6 @@ class Transforms {
4620
4689
  });
4621
4690
  }
4622
4691
  }
4623
- /**
4624
- * @typedef {Object} DraggableCursorParams
4625
- * @property {String} [onHover]
4626
- * @property {String} [onGrab]
4627
- */
4628
4692
  /**
4629
4693
  * @template {Array<Number>|DOMTargetSelector|String|Number|Boolean|Function|DraggableCursorParams} T
4630
4694
  * @param {T | ((draggable: Draggable) => T)} value
@@ -4633,45 +4697,6 @@ class Transforms {
4633
4697
  */
4634
4698
  const parseDraggableFunctionParameter = (value, draggable) => value && isFnc(value) ? /** @type {Function} */ (value)(draggable) : value;
4635
4699
  let zIndex = 0;
4636
- /**
4637
- * @typedef {Object} DraggableAxisParam
4638
- * @property {String} [mapTo]
4639
- * @property {TweenModifier} [modifier]
4640
- * @property {TweenComposition} [composition]
4641
- * @property {Number|Array<Number>|((draggable: Draggable) => Number|Array<Number>)} [snap]
4642
- */
4643
- /**
4644
- * @typedef {Object} DraggableParams
4645
- * @property {DOMTargetSelector} [trigger]
4646
- * @property {DOMTargetSelector|Array<Number>|((draggable: Draggable) => DOMTargetSelector|Array<Number>)} [container]
4647
- * @property {Boolean|DraggableAxisParam} [x]
4648
- * @property {Boolean|DraggableAxisParam} [y]
4649
- * @property {TweenModifier} [modifier]
4650
- * @property {Number|Array<Number>|((draggable: Draggable) => Number|Array<Number>)} [snap]
4651
- * @property {Number|Array<Number>|((draggable: Draggable) => Number|Array<Number>)} [containerPadding]
4652
- * @property {Number|((draggable: Draggable) => Number)} [containerFriction]
4653
- * @property {Number|((draggable: Draggable) => Number)} [releaseContainerFriction]
4654
- * @property {Number|((draggable: Draggable) => Number)} [dragSpeed]
4655
- * @property {Number|((draggable: Draggable) => Number)} [scrollSpeed]
4656
- * @property {Number|((draggable: Draggable) => Number)} [scrollThreshold]
4657
- * @property {Number|((draggable: Draggable) => Number)} [minVelocity]
4658
- * @property {Number|((draggable: Draggable) => Number)} [maxVelocity]
4659
- * @property {Number|((draggable: Draggable) => Number)} [velocityMultiplier]
4660
- * @property {Number} [releaseMass]
4661
- * @property {Number} [releaseStiffness]
4662
- * @property {Number} [releaseDamping]
4663
- * @property {Boolean} [releaseDamping]
4664
- * @property {EasingParam} [releaseEase]
4665
- * @property {Boolean|DraggableCursorParams|((draggable: Draggable) => Boolean|DraggableCursorParams)} [cursor]
4666
- * @property {Callback<Draggable>} [onGrab]
4667
- * @property {Callback<Draggable>} [onDrag]
4668
- * @property {Callback<Draggable>} [onRelease]
4669
- * @property {Callback<Draggable>} [onUpdate]
4670
- * @property {Callback<Draggable>} [onSettle]
4671
- * @property {Callback<Draggable>} [onSnap]
4672
- * @property {Callback<Draggable>} [onResize]
4673
- * @property {Callback<Draggable>} [onAfterResize]
4674
- */
4675
4700
  class Draggable {
4676
4701
  /**
4677
4702
  * @param {TargetsParam} target
@@ -4680,8 +4705,8 @@ class Draggable {
4680
4705
  constructor(target, parameters = {}) {
4681
4706
  if (!target)
4682
4707
  return;
4683
- if (globals.scope)
4684
- globals.scope.revertibles.push(this);
4708
+ if (scope.current)
4709
+ scope.current.register(this);
4685
4710
  const paramX = parameters.x;
4686
4711
  const paramY = parameters.y;
4687
4712
  const trigger = parameters.trigger;
@@ -4885,7 +4910,7 @@ class Draggable {
4885
4910
  this.canScroll = false;
4886
4911
  this.enabled = false;
4887
4912
  this.initialized = false;
4888
- this.activeProp = this.disabled[0] ? yProp : xProp;
4913
+ this.activeProp = this.disabled[1] ? xProp : yProp;
4889
4914
  this.animate.animations[this.activeProp].onRender = () => {
4890
4915
  const hasUpdated = this.updated;
4891
4916
  const hasMoved = this.grabbed && hasUpdated;
@@ -5665,6 +5690,7 @@ class Draggable {
5665
5690
  this.overshootXTicker.revert();
5666
5691
  this.overshootYTicker.revert();
5667
5692
  this.resizeTicker.revert();
5693
+ this.animate.revert();
5668
5694
  return this;
5669
5695
  }
5670
5696
  /**
@@ -5710,39 +5736,11 @@ class Draggable {
5710
5736
  const createDraggable = (target, parameters) => new Draggable(target, parameters);
5711
5737
 
5712
5738
 
5713
- /**
5714
- * @typedef {Object} ReactRef
5715
- * @property {HTMLElement|SVGElement|null} [current]
5716
- */
5717
- /**
5718
- * @typedef {Object} AngularRef
5719
- * @property {HTMLElement|SVGElement} [nativeElement]
5720
- */
5721
- /**
5722
- * @typedef {Object} ScopeParams
5723
- * @property {DOMTargetSelector|ReactRef|AngularRef} [root]
5724
- * @property {DefaultsParams} [defaults]
5725
- * @property {Record<String, String>} [mediaQueries]
5726
- */
5727
- /**
5728
- * @callback ScopeCleanup
5729
- * @param {Scope} [scope]
5730
- */
5731
- /**
5732
- * @callback ScopeConstructor
5733
- * @param {Scope} [scope]
5734
- * @return {ScopeCleanup|void}
5735
- */
5736
- /**
5737
- * @callback ScopeMethod
5738
- * @param {...*} args
5739
- * @return {ScopeCleanup|void}
5740
- */
5741
5739
  class Scope {
5742
5740
  /** @param {ScopeParams} [parameters] */
5743
5741
  constructor(parameters = {}) {
5744
- if (globals.scope)
5745
- globals.scope.revertibles.push(this);
5742
+ if (scope.current)
5743
+ scope.current.register(this);
5746
5744
  const rootParam = parameters.root;
5747
5745
  /** @type {Document|DOMTarget} */
5748
5746
  let root = doc;
@@ -5759,13 +5757,23 @@ class Scope {
5759
5757
  this.defaults = scopeDefaults ? mergeObjects(scopeDefaults, globalDefault) : globalDefault;
5760
5758
  /** @type {Document|DOMTarget} */
5761
5759
  this.root = root;
5762
- /** @type {Array<ScopeConstructor>} */
5760
+ /** @type {Array<ScopeConstructorCallback>} */
5763
5761
  this.constructors = [];
5764
- /** @type {Array<Function>} */
5762
+ /** @type {Array<ScopeCleanupCallback>} */
5765
5763
  this.revertConstructors = [];
5766
5764
  /** @type {Array<Revertible>} */
5767
5765
  this.revertibles = [];
5768
- /** @type {Record<String, Function>} */
5766
+ /** @type {Array<ScopeConstructorCallback | ((scope: this) => Tickable)>} */
5767
+ this.constructorsOnce = [];
5768
+ /** @type {Array<ScopeCleanupCallback>} */
5769
+ this.revertConstructorsOnce = [];
5770
+ /** @type {Array<Revertible>} */
5771
+ this.revertiblesOnce = [];
5772
+ /** @type {Boolean} */
5773
+ this.once = false;
5774
+ /** @type {Number} */
5775
+ this.onceIndex = 0;
5776
+ /** @type {Record<String, ScopeMethod>} */
5769
5777
  this.methods = {};
5770
5778
  /** @type {Record<String, Boolean>} */
5771
5779
  this.matches = {};
@@ -5782,26 +5790,30 @@ class Scope {
5782
5790
  }
5783
5791
  }
5784
5792
  /**
5785
- * @callback ScoppedCallback
5786
- * @param {this} scope
5787
- * @return {any}
5788
- *
5789
- * @param {ScoppedCallback} cb
5790
- * @return {this}
5793
+ * @param {Revertible} revertible
5794
+ */
5795
+ register(revertible) {
5796
+ const store = this.once ? this.revertiblesOnce : this.revertibles;
5797
+ store.push(revertible);
5798
+ }
5799
+ /**
5800
+ * @template T
5801
+ * @param {ScopedCallback<T>} cb
5802
+ * @return {T}
5791
5803
  */
5792
5804
  execute(cb) {
5793
- let activeScope = globals.scope;
5794
- let activeRoot = globals.root;
5805
+ let activeScope = scope.current;
5806
+ let activeRoot = scope.root;
5795
5807
  let activeDefaults = globals.defaults;
5796
- globals.scope = this;
5797
- globals.root = this.root;
5808
+ scope.current = this;
5809
+ scope.root = this.root;
5798
5810
  globals.defaults = this.defaults;
5799
5811
  const mqs = this.mediaQueryLists;
5800
5812
  for (let mq in mqs)
5801
5813
  this.matches[mq] = mqs[mq].matches;
5802
5814
  const returned = cb(this);
5803
- globals.scope = activeScope;
5804
- globals.root = activeRoot;
5815
+ scope.current = activeScope;
5816
+ scope.root = activeRoot;
5805
5817
  globals.defaults = activeDefaults;
5806
5818
  return returned;
5807
5819
  }
@@ -5809,6 +5821,7 @@ class Scope {
5809
5821
  * @return {this}
5810
5822
  */
5811
5823
  refresh() {
5824
+ this.onceIndex = 0;
5812
5825
  this.execute(() => {
5813
5826
  let i = this.revertibles.length;
5814
5827
  let y = this.revertConstructors.length;
@@ -5818,9 +5831,9 @@ class Scope {
5818
5831
  this.revertConstructors[y](this);
5819
5832
  this.revertibles.length = 0;
5820
5833
  this.revertConstructors.length = 0;
5821
- this.constructors.forEach(constructor => {
5834
+ this.constructors.forEach((/** @type {ScopeConstructorCallback} */ constructor) => {
5822
5835
  const revertConstructor = constructor(this);
5823
- if (revertConstructor) {
5836
+ if (isFnc(revertConstructor)) {
5824
5837
  this.revertConstructors.push(revertConstructor);
5825
5838
  }
5826
5839
  });
@@ -5828,28 +5841,26 @@ class Scope {
5828
5841
  return this;
5829
5842
  }
5830
5843
  /**
5831
- * @callback contructorCallback
5832
- * @param {this} self
5833
- *
5834
5844
  * @overload
5835
5845
  * @param {String} a1
5836
5846
  * @param {ScopeMethod} a2
5837
5847
  * @return {this}
5838
5848
  *
5839
5849
  * @overload
5840
- * @param {contructorCallback} a1
5850
+ * @param {ScopeConstructorCallback} a1
5841
5851
  * @return {this}
5842
5852
  *
5843
- * @param {String|contructorCallback} a1
5853
+ * @param {String|ScopeConstructorCallback} a1
5844
5854
  * @param {ScopeMethod} [a2]
5845
5855
  */
5846
5856
  add(a1, a2) {
5857
+ this.once = false;
5847
5858
  if (isFnc(a1)) {
5848
- const constructor = /** @type {contructorCallback} */ (a1);
5859
+ const constructor = /** @type {ScopeConstructorCallback} */ (a1);
5849
5860
  this.constructors.push(constructor);
5850
5861
  this.execute(() => {
5851
5862
  const revertConstructor = constructor(this);
5852
- if (revertConstructor) {
5863
+ if (isFnc(revertConstructor)) {
5853
5864
  this.revertConstructors.push(revertConstructor);
5854
5865
  }
5855
5866
  });
@@ -5859,6 +5870,46 @@ class Scope {
5859
5870
  }
5860
5871
  return this;
5861
5872
  }
5873
+ /**
5874
+ * @param {ScopeConstructorCallback} scopeConstructorCallback
5875
+ * @return {this}
5876
+ */
5877
+ addOnce(scopeConstructorCallback) {
5878
+ this.once = true;
5879
+ if (isFnc(scopeConstructorCallback)) {
5880
+ const currentIndex = this.onceIndex++;
5881
+ const tracked = this.constructorsOnce[currentIndex];
5882
+ if (tracked)
5883
+ return this;
5884
+ const constructor = /** @type {ScopeConstructorCallback} */ (scopeConstructorCallback);
5885
+ this.constructorsOnce[currentIndex] = constructor;
5886
+ this.execute(() => {
5887
+ const revertConstructor = constructor(this);
5888
+ if (isFnc(revertConstructor)) {
5889
+ this.revertConstructorsOnce.push(revertConstructor);
5890
+ }
5891
+ });
5892
+ }
5893
+ return this;
5894
+ }
5895
+ /**
5896
+ * @param {(scope: this) => Tickable} cb
5897
+ * @return {Tickable}
5898
+ */
5899
+ keepTime(cb) {
5900
+ this.once = true;
5901
+ const currentIndex = this.onceIndex++;
5902
+ const tracked = /** @type {(scope: this) => Tickable} */ (this.constructorsOnce[currentIndex]);
5903
+ if (isFnc(tracked))
5904
+ return tracked(this);
5905
+ const constructor = /** @type {(scope: this) => Tickable} */ (createRefreshable(cb));
5906
+ this.constructorsOnce[currentIndex] = constructor;
5907
+ let trackedTickable;
5908
+ this.execute(() => {
5909
+ trackedTickable = constructor(this);
5910
+ });
5911
+ return trackedTickable;
5912
+ }
5862
5913
  /**
5863
5914
  * @param {Event} e
5864
5915
  */
@@ -5872,18 +5923,30 @@ class Scope {
5872
5923
  revert() {
5873
5924
  const revertibles = this.revertibles;
5874
5925
  const revertConstructors = this.revertConstructors;
5926
+ const revertiblesOnce = this.revertiblesOnce;
5927
+ const revertConstructorsOnce = this.revertConstructorsOnce;
5875
5928
  const mqs = this.mediaQueryLists;
5876
5929
  let i = revertibles.length;
5877
- let y = revertConstructors.length;
5930
+ let j = revertConstructors.length;
5931
+ let k = revertiblesOnce.length;
5932
+ let l = revertConstructorsOnce.length;
5878
5933
  while (i--)
5879
5934
  revertibles[i].revert();
5880
- while (y--)
5881
- revertConstructors[y](this);
5935
+ while (j--)
5936
+ revertConstructors[j](this);
5937
+ while (k--)
5938
+ revertiblesOnce[k].revert();
5939
+ while (l--)
5940
+ revertConstructorsOnce[l](this);
5882
5941
  for (let mq in mqs)
5883
5942
  mqs[mq].removeEventListener('change', this);
5884
5943
  revertibles.length = 0;
5885
5944
  revertConstructors.length = 0;
5886
5945
  this.constructors.length = 0;
5946
+ revertiblesOnce.length = 0;
5947
+ revertConstructorsOnce.length = 0;
5948
+ this.constructorsOnce.length = 0;
5949
+ this.onceIndex = 0;
5887
5950
  this.matches = {};
5888
5951
  this.methods = {};
5889
5952
  this.mediaQueryLists = {};
@@ -6200,7 +6263,7 @@ const getAnimationDomTarget = linked => {
6200
6263
  return $linkedTarget;
6201
6264
  };
6202
6265
  let scrollerIndex = 0;
6203
- const debugColors = ['#FF4B4B', '#FF971B', '#FFC730', '#F9F640', '#7AFF5A', '#18FF74', '#17E09B', '#3CFFEC', '#05DBE9', '#33B3F1', '#638CF9', '#C563FE', '#FF4FCF', '#F93F8A'];
6266
+ const debugColors$1 = ['#FF4B4B', '#FF971B', '#FFC730', '#F9F640', '#7AFF5A', '#18FF74', '#17E09B', '#3CFFEC', '#05DBE9', '#33B3F1', '#638CF9', '#C563FE', '#FF4FCF', '#F93F8A'];
6204
6267
  /**
6205
6268
  * @typedef {Object} ScrollThresholdParam
6206
6269
  * @property {ScrollThresholdValue} [target]
@@ -6241,8 +6304,8 @@ class ScrollObserver {
6241
6304
  * @param {ScrollObserverParams} parameters
6242
6305
  */
6243
6306
  constructor(parameters = {}) {
6244
- if (globals.scope)
6245
- globals.scope.revertibles.push(this);
6307
+ if (scope.current)
6308
+ scope.current.register(this);
6246
6309
  const syncMode = setValue(parameters.sync, 'play pause');
6247
6310
  const ease = syncMode ? parseEasings(/** @type {EasingParam} */ (syncMode)) : null;
6248
6311
  const isLinear = syncMode && (syncMode === 'linear' || syncMode === none);
@@ -6432,7 +6495,7 @@ class ScrollObserver {
6432
6495
  const $debug = doc.createElement('div');
6433
6496
  const $thresholds = doc.createElement('div');
6434
6497
  const $triggers = doc.createElement('div');
6435
- const color = debugColors[this.index % debugColors.length];
6498
+ const color = debugColors$1[this.index % debugColors$1.length];
6436
6499
  const useWin = container.useWin;
6437
6500
  const containerWidth = useWin ? container.winWidth : container.width;
6438
6501
  const containerHeight = useWin ? container.winHeight : container.height;
@@ -6785,27 +6848,474 @@ class ScrollObserver {
6785
6848
  const onScroll = (parameters = {}) => new ScrollObserver(parameters);
6786
6849
 
6787
6850
 
6851
+ const segmenter = !isUnd(Intl) && Intl.Segmenter;
6852
+ const valueRgx = /\{value\}/g;
6853
+ const indexRgx = /\{i\}/g;
6854
+ const whiteSpaceGroupRgx = /(\s+)/;
6855
+ const whiteSpaceRgx = /^\s+$/;
6856
+ const lineType = 'line';
6857
+ const wordType = 'word';
6858
+ const charType = 'char';
6859
+ const dataLine = `data-line`;
6860
+ /**
6861
+ * @typedef {Object} Segment
6862
+ * @property {String} segment
6863
+ * @property {Boolean} [isWordLike]
6864
+ */
6865
+ /**
6866
+ * @typedef {Object} Segmenter
6867
+ * @property {function(String): Iterable<Segment>} segment
6868
+ */
6869
+ /** @type {Segmenter} */
6870
+ let wordSegmenter = null;
6871
+ /** @type {Segmenter} */
6872
+ let graphemeSegmenter = null;
6873
+ let $splitTemplate = null;
6874
+ /**
6875
+ * @param {Segment} seg
6876
+ * @return {Boolean}
6877
+ */
6878
+ const isSegmentWordLike = seg => {
6879
+ return seg.isWordLike ||
6880
+ seg.segment === ' ' || // Consider spaces as words first, then handle them diffrently later
6881
+ isNum(+seg.segment); // Safari doesn't considers numbers as words
6882
+ };
6788
6883
  /**
6789
- * @typedef {Object} StaggerParameters
6790
- * @property {Number|String} [start]
6791
- * @property {Number|'first'|'center'|'last'} [from]
6792
- * @property {Boolean} [reversed]
6793
- * @property {Array.<Number>} [grid]
6794
- * @property {('x'|'y')} [axis]
6795
- * @property {EasingParam} [ease]
6796
- * @property {TweenModifier} [modifier]
6884
+ * @param {HTMLElement} $el
6797
6885
  */
6886
+ const setAriaHidden = $el => $el.setAttribute('aria-hidden', 'true');
6798
6887
  /**
6799
- * @callback StaggerFunction
6800
- * @param {Target} [target]
6801
- * @param {Number} [index]
6802
- * @param {Number} [length]
6803
- * @param {Timeline} [tl]
6804
- * @return {Number|String}
6888
+ * @param {DOMTarget} $el
6889
+ * @param {String} type
6890
+ * @return {Array<HTMLElement>}
6891
+ */
6892
+ const getAllTopLevelElements = ($el, type) => [... /** @type {*} */($el.querySelectorAll(`[data-${type}]:not([data-${type}] [data-${type}])`))];
6893
+ const debugColors = { line: '#00D672', word: '#FF4B4B', char: '#5A87FF' };
6894
+ /**
6895
+ * @param {HTMLElement} $el
6896
+ */
6897
+ const filterEmptyElements = $el => {
6898
+ if (!$el.childElementCount && !$el.textContent.trim()) {
6899
+ const $parent = $el.parentElement;
6900
+ $el.remove();
6901
+ if ($parent)
6902
+ filterEmptyElements($parent);
6903
+ }
6904
+ };
6905
+ /**
6906
+ * @param {HTMLElement} $el
6907
+ * @param {Number} lineIndex
6908
+ * @param {Set<HTMLElement>} bin
6909
+ * @returns {Set<HTMLElement>}
6910
+ */
6911
+ const filterLineElements = ($el, lineIndex, bin) => {
6912
+ const dataLineAttr = $el.getAttribute(dataLine);
6913
+ if (dataLineAttr !== null && +dataLineAttr !== lineIndex || $el.tagName === 'BR')
6914
+ bin.add($el);
6915
+ let i = $el.childElementCount;
6916
+ while (i--)
6917
+ filterLineElements(/** @type {HTMLElement} */ ($el.children[i]), lineIndex, bin);
6918
+ return bin;
6919
+ };
6920
+ /**
6921
+ * @param {'line'|'word'|'char'} type
6922
+ * @param {splitTemplateParams} params
6923
+ * @return {String}
6924
+ */
6925
+ const generateTemplate = (type, params = {}) => {
6926
+ let template = ``;
6927
+ const classString = isStr(params.class) ? ` class="${params.class}"` : '';
6928
+ const cloneType = setValue(params.clone, false);
6929
+ const wrapType = setValue(params.wrap, false);
6930
+ const overflow = wrapType ? wrapType === true ? 'clip' : wrapType : cloneType ? 'clip' : false;
6931
+ if (wrapType)
6932
+ template += `<span${overflow ? ` style="overflow:${overflow};"` : ''}>`;
6933
+ template += `<span${classString}${cloneType ? ` style="position:relative;"` : ''} data-${type}="{i}">`;
6934
+ if (cloneType) {
6935
+ const left = cloneType === 'left' ? '-100%' : cloneType === 'right' ? '100%' : '0';
6936
+ const top = cloneType === 'top' ? '-100%' : cloneType === 'bottom' ? '100%' : '0';
6937
+ template += `<span>{value}</span>`;
6938
+ template += `<span inert style="position:absolute;top:${top};left:${left};white-space:nowrap;">{value}</span>`;
6939
+ }
6940
+ else {
6941
+ template += `{value}`;
6942
+ }
6943
+ template += `</span>`;
6944
+ if (wrapType)
6945
+ template += `</span>`;
6946
+ return template;
6947
+ };
6948
+ /**
6949
+ * @param {String|SplitFunctionValue} htmlTemplate
6950
+ * @param {Array<HTMLElement>} store
6951
+ * @param {Node|HTMLElement} node
6952
+ * @param {DocumentFragment} $parentFragment
6953
+ * @param {'line'|'word'|'char'} type
6954
+ * @param {Boolean} debug
6955
+ * @param {Number} lineIndex
6956
+ * @param {Number} [wordIndex]
6957
+ * @param {Number} [charIndex]
6958
+ * @return {HTMLElement}
6959
+ */
6960
+ const processHTMLTemplate = (htmlTemplate, store, node, $parentFragment, type, debug, lineIndex, wordIndex, charIndex) => {
6961
+ const isLine = type === lineType;
6962
+ const isChar = type === charType;
6963
+ const className = `_${type}_`;
6964
+ const template = isFnc(htmlTemplate) ? htmlTemplate(node) : htmlTemplate;
6965
+ const displayStyle = isLine ? 'block' : 'inline-block';
6966
+ $splitTemplate.innerHTML = template
6967
+ .replace(valueRgx, `<i class="${className}"></i>`)
6968
+ .replace(indexRgx, `${isChar ? charIndex : isLine ? lineIndex : wordIndex}`);
6969
+ const $content = $splitTemplate.content;
6970
+ const $highestParent = /** @type {HTMLElement} */ ($content.firstElementChild);
6971
+ const $split = /** @type {HTMLElement} */ ($content.querySelector(`[data-${type}]`)) || $highestParent;
6972
+ const $replacables = /** @type {NodeListOf<HTMLElement>} */ ($content.querySelectorAll(`i.${className}`));
6973
+ const replacablesLength = $replacables.length;
6974
+ if (replacablesLength) {
6975
+ $highestParent.style.display = displayStyle;
6976
+ $split.style.display = displayStyle;
6977
+ $split.setAttribute(dataLine, `${lineIndex}`);
6978
+ if (!isLine) {
6979
+ $split.setAttribute('data-word', `${wordIndex}`);
6980
+ if (isChar)
6981
+ $split.setAttribute('data-char', `${charIndex}`);
6982
+ }
6983
+ let i = replacablesLength;
6984
+ while (i--) {
6985
+ const $replace = $replacables[i];
6986
+ const $closestParent = $replace.parentElement;
6987
+ $closestParent.style.display = displayStyle;
6988
+ if (isLine) {
6989
+ $closestParent.innerHTML = /** @type {HTMLElement} */ (node).innerHTML;
6990
+ }
6991
+ else {
6992
+ $closestParent.replaceChild(node.cloneNode(true), $replace);
6993
+ }
6994
+ }
6995
+ store.push($split);
6996
+ $parentFragment.appendChild($content);
6997
+ }
6998
+ else {
6999
+ console.warn(`The expression "{value}" is missing from the provided template.`);
7000
+ }
7001
+ if (debug)
7002
+ $highestParent.style.outline = `1px dotted ${debugColors[type]}`;
7003
+ return $highestParent;
7004
+ };
7005
+ /**
7006
+ * A class that splits text into words and wraps them in span elements while preserving the original HTML structure.
7007
+ * @class
7008
+ */
7009
+ class TextSplitter {
7010
+ /**
7011
+ * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
7012
+ * @param {TextSplitterParams} [parameters]
7013
+ */
7014
+ constructor(target, parameters = {}) {
7015
+ // Only init segmenters when needed
7016
+ if (!wordSegmenter)
7017
+ wordSegmenter = segmenter ? new segmenter([], { granularity: wordType }) : {
7018
+ segment: (text) => {
7019
+ const segments = [];
7020
+ const words = text.split(whiteSpaceGroupRgx);
7021
+ for (let i = 0, l = words.length; i < l; i++) {
7022
+ const segment = words[i];
7023
+ segments.push({
7024
+ segment,
7025
+ isWordLike: !whiteSpaceRgx.test(segment), // Consider non-whitespace as word-like
7026
+ });
7027
+ }
7028
+ return segments;
7029
+ }
7030
+ };
7031
+ if (!graphemeSegmenter)
7032
+ graphemeSegmenter = segmenter ? new segmenter([], { granularity: 'grapheme' }) : {
7033
+ segment: text => [...text].map(char => ({ segment: char }))
7034
+ };
7035
+ if (!$splitTemplate && isBrowser)
7036
+ $splitTemplate = doc.createElement('template');
7037
+ if (scope.current)
7038
+ scope.current.register(this);
7039
+ const { words, chars, lines, accessible, includeSpaces, debug } = parameters;
7040
+ const $target = /** @type {HTMLElement} */ ((target = isArr(target) ? target[0] : target) && /** @type {Node} */ (target).nodeType ? target : (getNodeList(target) || [])[0]);
7041
+ const lineParams = lines === true ? {} : lines;
7042
+ const wordParams = words === true || isUnd(words) ? {} : words;
7043
+ const charParams = chars === true ? {} : chars;
7044
+ this.debug = setValue(debug, false);
7045
+ this.includeSpaces = setValue(includeSpaces, false);
7046
+ this.accessible = setValue(accessible, true);
7047
+ this.linesOnly = lineParams && (!wordParams && !charParams);
7048
+ /** @type {String|false|SplitFunctionValue} */
7049
+ this.lineTemplate = isObj(lineParams) ? generateTemplate(lineType, /** @type {splitTemplateParams} */ (lineParams)) : lineParams;
7050
+ /** @type {String|false|SplitFunctionValue} */
7051
+ this.wordTemplate = isObj(wordParams) || this.linesOnly ? generateTemplate(wordType, /** @type {splitTemplateParams} */ (wordParams)) : wordParams;
7052
+ /** @type {String|false|SplitFunctionValue} */
7053
+ this.charTemplate = isObj(charParams) ? generateTemplate(charType, /** @type {splitTemplateParams} */ (charParams)) : charParams;
7054
+ this.$target = $target;
7055
+ this.html = $target && $target.innerHTML;
7056
+ this.lines = [];
7057
+ this.words = [];
7058
+ this.chars = [];
7059
+ this.effects = [];
7060
+ this.effectsCleanups = [];
7061
+ this.cache = null;
7062
+ this.ready = false;
7063
+ this.width = 0;
7064
+ this.resizeTimeout = null;
7065
+ const handleSplit = () => this.html && (lineParams || wordParams || charParams) && this.split();
7066
+ // Make sure this is declared before calling handleSplit() in case revert() is called inside an effect callback
7067
+ this.resizeObserver = new ResizeObserver(() => {
7068
+ // Use a setTimeout instead of a Timer for better tree shaking
7069
+ clearTimeout(this.resizeTimeout);
7070
+ this.resizeTimeout = setTimeout(() => {
7071
+ const currentWidth = /** @type {HTMLElement} */ ($target).offsetWidth;
7072
+ if (currentWidth === this.width)
7073
+ return;
7074
+ this.width = currentWidth;
7075
+ handleSplit();
7076
+ }, 150);
7077
+ });
7078
+ // Only declare the font ready promise when splitting by lines and not alreay split
7079
+ if (this.lineTemplate && !this.ready) {
7080
+ doc.fonts.ready.then(handleSplit);
7081
+ }
7082
+ else {
7083
+ handleSplit();
7084
+ }
7085
+ $target ? this.resizeObserver.observe($target) : console.warn('No Text Splitter target found.');
7086
+ }
7087
+ /**
7088
+ * @param {(...args: any[]) => Tickable | (() => void)} effect
7089
+ * @return this
7090
+ */
7091
+ addEffect(effect) {
7092
+ if (!isFnc(effect))
7093
+ return console.warn('Effect must return a function.');
7094
+ const refreshableEffect = createRefreshable(effect);
7095
+ this.effects.push(refreshableEffect);
7096
+ if (this.ready)
7097
+ this.effectsCleanups[this.effects.length - 1] = refreshableEffect(this);
7098
+ return this;
7099
+ }
7100
+ revert() {
7101
+ clearTimeout(this.resizeTimeout);
7102
+ this.lines.length = this.words.length = this.chars.length = 0;
7103
+ this.resizeObserver.disconnect();
7104
+ // Make sure to revert the effects after disconnecting the resizeObserver to avoid triggering it in the process
7105
+ this.effectsCleanups.forEach(cleanup => isFnc(cleanup) ? cleanup(this) : cleanup.revert && cleanup.revert());
7106
+ this.$target.innerHTML = this.html;
7107
+ return this;
7108
+ }
7109
+ /**
7110
+ * Recursively processes a node and its children
7111
+ * @param {Node} node
7112
+ */
7113
+ splitNode(node) {
7114
+ const wordTemplate = this.wordTemplate;
7115
+ const charTemplate = this.charTemplate;
7116
+ const includeSpaces = this.includeSpaces;
7117
+ const debug = this.debug;
7118
+ const nodeType = node.nodeType;
7119
+ if (nodeType === 3) {
7120
+ const nodeText = node.nodeValue;
7121
+ // If the nodeText is only whitespace, leave it as is
7122
+ if (nodeText.trim()) {
7123
+ const tempWords = [];
7124
+ const words = this.words;
7125
+ const chars = this.chars;
7126
+ const wordSegments = wordSegmenter.segment(nodeText);
7127
+ const $wordsFragment = doc.createDocumentFragment();
7128
+ let prevSeg = null;
7129
+ for (const wordSegment of wordSegments) {
7130
+ const segment = wordSegment.segment;
7131
+ const isWordLike = isSegmentWordLike(wordSegment);
7132
+ // Determine if this segment should be a new word, first segment always becomes a new word
7133
+ if (!prevSeg || (isWordLike && (prevSeg && (isSegmentWordLike(prevSeg))))) {
7134
+ tempWords.push(segment);
7135
+ }
7136
+ else {
7137
+ // Only concatenate if both current and previous are non-word-like and don't contain spaces
7138
+ const lastWordIndex = tempWords.length - 1;
7139
+ const lastWord = tempWords[lastWordIndex];
7140
+ if (!lastWord.includes(' ') && !segment.includes(' ')) {
7141
+ tempWords[lastWordIndex] += segment;
7142
+ }
7143
+ else {
7144
+ tempWords.push(segment);
7145
+ }
7146
+ }
7147
+ prevSeg = wordSegment;
7148
+ }
7149
+ for (let i = 0, l = tempWords.length; i < l; i++) {
7150
+ const word = tempWords[i];
7151
+ if (!word.trim()) {
7152
+ // Preserve whitespace only if includeSpaces is false and if the current space is not the first node
7153
+ if (i && includeSpaces)
7154
+ continue;
7155
+ $wordsFragment.appendChild(doc.createTextNode(word));
7156
+ }
7157
+ else {
7158
+ const nextWord = tempWords[i + 1];
7159
+ const hasWordFollowingSpace = includeSpaces && nextWord && !nextWord.trim();
7160
+ const wordToProcess = word;
7161
+ const charSegments = charTemplate ? graphemeSegmenter.segment(wordToProcess) : null;
7162
+ const $charsFragment = charTemplate ? doc.createDocumentFragment() : doc.createTextNode(hasWordFollowingSpace ? word + '\xa0' : word);
7163
+ if (charTemplate) {
7164
+ const charSegmentsArray = [...charSegments];
7165
+ for (let j = 0, jl = charSegmentsArray.length; j < jl; j++) {
7166
+ const charSegment = charSegmentsArray[j];
7167
+ const isLastChar = j === jl - 1;
7168
+ // If this is the last character and includeSpaces is true with a following space, append the space
7169
+ const charText = isLastChar && hasWordFollowingSpace ? charSegment.segment + '\xa0' : charSegment.segment;
7170
+ const $charNode = doc.createTextNode(charText);
7171
+ processHTMLTemplate(charTemplate, chars, $charNode, /** @type {DocumentFragment} */ ($charsFragment), charType, debug, -1, words.length, chars.length);
7172
+ }
7173
+ }
7174
+ if (wordTemplate) {
7175
+ processHTMLTemplate(wordTemplate, words, $charsFragment, $wordsFragment, wordType, debug, -1, words.length, chars.length);
7176
+ // Chars elements must be re-parsed in the split() method if both words and chars are parsed
7177
+ }
7178
+ else if (charTemplate) {
7179
+ $wordsFragment.appendChild($charsFragment);
7180
+ }
7181
+ else {
7182
+ $wordsFragment.appendChild(doc.createTextNode(word));
7183
+ }
7184
+ // Skip the next iteration if we included a space
7185
+ if (hasWordFollowingSpace)
7186
+ i++;
7187
+ }
7188
+ }
7189
+ node.parentNode.replaceChild($wordsFragment, node);
7190
+ }
7191
+ }
7192
+ else if (nodeType === 1) {
7193
+ // Converting to an array is necessary to work around childNodes pottential mutation
7194
+ const childNodes = /** @type {Array<Node>} */ ([... /** @type {*} */(node.childNodes)]);
7195
+ for (let i = 0, l = childNodes.length; i < l; i++)
7196
+ this.splitNode(childNodes[i]);
7197
+ }
7198
+ }
7199
+ /**
7200
+ * @param {Boolean} clearCache
7201
+ * @return {this}
7202
+ */
7203
+ split(clearCache = false) {
7204
+ const $el = this.$target;
7205
+ const isCached = !!this.cache && !clearCache;
7206
+ const lineTemplate = this.lineTemplate;
7207
+ const wordTemplate = this.wordTemplate;
7208
+ const charTemplate = this.charTemplate;
7209
+ const fontsReady = doc.fonts.status !== 'loading';
7210
+ const canSplitLines = lineTemplate && fontsReady;
7211
+ this.ready = !lineTemplate || fontsReady;
7212
+ if (!isCached) {
7213
+ if (clearCache) {
7214
+ $el.innerHTML = this.html;
7215
+ this.words.length = this.chars.length = 0;
7216
+ }
7217
+ this.splitNode($el);
7218
+ this.cache = $el.innerHTML;
7219
+ }
7220
+ // Always reset the html when splitting by lines
7221
+ if (canSplitLines) {
7222
+ // No need to revert effects animations here since it's already taken care by the refreshable
7223
+ this.effectsCleanups.forEach(cleanup => isFnc(cleanup) && cleanup(this));
7224
+ if (isCached)
7225
+ $el.innerHTML = this.cache;
7226
+ this.lines.length = 0;
7227
+ if (wordTemplate)
7228
+ this.words = getAllTopLevelElements($el, wordType);
7229
+ }
7230
+ // Always reparse characters after a line reset or if both words and chars are activated
7231
+ if (charTemplate && (canSplitLines || wordTemplate)) {
7232
+ this.chars = getAllTopLevelElements($el, charType);
7233
+ }
7234
+ // Words are used when lines only and prioritized over chars
7235
+ const elementsArray = this.words.length ? this.words : this.chars;
7236
+ let y, linesCount = 0;
7237
+ for (let i = 0, l = elementsArray.length; i < l; i++) {
7238
+ const $el = elementsArray[i];
7239
+ const { top, height } = $el.getBoundingClientRect();
7240
+ if (y && top - y > height * .5)
7241
+ linesCount++;
7242
+ $el.setAttribute(dataLine, `${linesCount}`);
7243
+ const nested = $el.querySelectorAll(`[${dataLine}]`);
7244
+ let c = nested.length;
7245
+ while (c--)
7246
+ nested[c].setAttribute(dataLine, `${linesCount}`);
7247
+ y = top;
7248
+ }
7249
+ if (canSplitLines) {
7250
+ const linesFragment = doc.createDocumentFragment();
7251
+ const parents = new Set();
7252
+ const clones = [];
7253
+ for (let lineIndex = 0; lineIndex < linesCount + 1; lineIndex++) {
7254
+ const $clone = /** @type {HTMLElement} */ ($el.cloneNode(true));
7255
+ filterLineElements($clone, lineIndex, new Set()).forEach($el => {
7256
+ const $parent = $el.parentElement;
7257
+ if ($parent)
7258
+ parents.add($parent);
7259
+ $el.remove();
7260
+ });
7261
+ clones.push($clone);
7262
+ }
7263
+ parents.forEach(filterEmptyElements);
7264
+ for (let cloneIndex = 0, clonesLength = clones.length; cloneIndex < clonesLength; cloneIndex++) {
7265
+ processHTMLTemplate(lineTemplate, this.lines, clones[cloneIndex], linesFragment, lineType, this.debug, cloneIndex);
7266
+ }
7267
+ $el.innerHTML = '';
7268
+ $el.appendChild(linesFragment);
7269
+ if (wordTemplate)
7270
+ this.words = getAllTopLevelElements($el, wordType);
7271
+ if (charTemplate)
7272
+ this.chars = getAllTopLevelElements($el, charType);
7273
+ }
7274
+ // Remove the word wrappers and clear the words array if lines split only
7275
+ if (this.linesOnly) {
7276
+ const words = this.words;
7277
+ let w = words.length;
7278
+ while (w--) {
7279
+ const $word = words[w];
7280
+ $word.replaceWith($word.textContent);
7281
+ }
7282
+ words.length = 0;
7283
+ }
7284
+ if (canSplitLines || clearCache) {
7285
+ this.effects.forEach((effect, i) => this.effectsCleanups[i] = effect(this));
7286
+ }
7287
+ if (this.accessible && (canSplitLines || !isCached)) {
7288
+ const $accessible = doc.createElement('span');
7289
+ // Make the accessible element visually-hidden (https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html)
7290
+ $accessible.style.cssText = `position:absolute;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);width:1px;height:1px;white-space:nowrap;`;
7291
+ // $accessible.setAttribute('tabindex', '-1');
7292
+ $accessible.innerHTML = this.html;
7293
+ $el.insertBefore($accessible, $el.firstChild);
7294
+ this.lines.forEach(setAriaHidden);
7295
+ this.words.forEach(setAriaHidden);
7296
+ this.chars.forEach(setAriaHidden);
7297
+ }
7298
+ this.width = /** @type {HTMLElement} */ ($el).offsetWidth;
7299
+ return this;
7300
+ }
7301
+ refresh() {
7302
+ this.split(true);
7303
+ }
7304
+ }
7305
+ /**
7306
+ * @param {HTMLElement|NodeList|String|Array<HTMLElement>} target
7307
+ * @param {TextSplitterParams} [parameters]
7308
+ * @return {TextSplitter}
6805
7309
  */
7310
+ const split = (target, parameters) => new TextSplitter(target, parameters);
7311
+ const text = {
7312
+ split,
7313
+ };
7314
+
7315
+
6806
7316
  /**
6807
7317
  * @param {Number|String|[Number|String,Number|String]} val
6808
- * @param {StaggerParameters} params
7318
+ * @param {StaggerParams} params
6809
7319
  * @return {StaggerFunction}
6810
7320
  */
6811
7321
  const stagger = (val, params = {}) => {
@@ -6819,22 +7329,29 @@ const stagger = (val, params = {}) => {
6819
7329
  const staggerEase = hasSpring ? /** @type {Spring} */ (ease).ease : hasEasing ? parseEasings(ease) : null;
6820
7330
  const grid = params.grid;
6821
7331
  const axis = params.axis;
7332
+ const customTotal = params.total;
6822
7333
  const fromFirst = isUnd(from) || from === 0 || from === 'first';
6823
7334
  const fromCenter = from === 'center';
6824
7335
  const fromLast = from === 'last';
7336
+ const fromRandom = from === 'random';
6825
7337
  const isRange = isArr(val);
7338
+ const useProp = params.use;
6826
7339
  const val1 = isRange ? parseNumber(val[0]) : parseNumber(val);
6827
7340
  const val2 = isRange ? parseNumber(val[1]) : 0;
6828
7341
  const unitMatch = unitsExecRgx.exec((isRange ? val[1] : val) + emptyString);
6829
7342
  const start = params.start || 0 + (isRange ? val1 : 0);
6830
7343
  let fromIndex = fromFirst ? 0 : isNum(from) ? from : 0;
6831
- return (_, i, t, tl) => {
7344
+ return (target, i, t, tl) => {
7345
+ const [registeredTarget] = registerTargets(target);
7346
+ const total = isUnd(customTotal) ? t : customTotal;
7347
+ const customIndex = !isUnd(useProp) ? isFnc(useProp) ? useProp(registeredTarget, i, total) : getOriginalAnimatableValue(registeredTarget, useProp) : false;
7348
+ const staggerIndex = isNum(customIndex) || isStr(customIndex) && isNum(+customIndex) ? +customIndex : i;
6832
7349
  if (fromCenter)
6833
- fromIndex = (t - 1) / 2;
7350
+ fromIndex = (total - 1) / 2;
6834
7351
  if (fromLast)
6835
- fromIndex = t - 1;
7352
+ fromIndex = total - 1;
6836
7353
  if (!values.length) {
6837
- for (let index = 0; index < t; index++) {
7354
+ for (let index = 0; index < total; index++) {
6838
7355
  if (!grid) {
6839
7356
  values.push(abs(fromIndex - index));
6840
7357
  }
@@ -6858,11 +7375,13 @@ const stagger = (val, params = {}) => {
6858
7375
  values = values.map(val => staggerEase(val / maxValue) * maxValue);
6859
7376
  if (reversed)
6860
7377
  values = values.map(val => axis ? (val < 0) ? val * -1 : -val : abs(maxValue - val));
7378
+ if (fromRandom)
7379
+ values = shuffle(values);
6861
7380
  }
6862
7381
  const spacing = isRange ? (val2 - val1) / maxValue : val1;
6863
7382
  const offset = tl ? parseTimelinePosition(tl, isUnd(params.start) ? tl.iterationDuration : start) : /** @type {Number} */ (start);
6864
7383
  /** @type {String|Number} */
6865
- let output = offset + ((spacing * round(values[i], 2)) || 0);
7384
+ let output = offset + ((spacing * round(values[staggerIndex], 2)) || 0);
6866
7385
  if (params.modifier)
6867
7386
  output = params.modifier(output);
6868
7387
  if (unitMatch)
@@ -6871,4 +7390,4 @@ const stagger = (val, params = {}) => {
6871
7390
  };
6872
7391
  };
6873
7392
 
6874
- export { Animatable, Draggable, JSAnimation, Scope, ScrollObserver, Spring, Timeline, Timer, WAAPIAnimation, animate, createAnimatable, createDraggable, createScope, createSpring, createTimeline, createTimer, eases, engine, onScroll, scrollContainers, stagger, svg, utils, waapi };
7393
+ export { Animatable, Draggable, JSAnimation, Scope, ScrollObserver, Spring, TextSplitter, Timeline, Timer, WAAPIAnimation, animate, createAnimatable, createDraggable, createScope, createSpring, createTimeline, createTimer, eases, engine, onScroll, scrollContainers, stagger, svg, text, utils, waapi };