@usal/svelte
Version: 
Ultimate Scroll Animation Library - Lightweight, powerful, wonderfully simple ✨ | Svelte Package
4 lines • 81.9 kB
Source Map (JSON)
{
  "version": 3,
  "sources": ["../../src/integrations/svelte.ts", "../../src/usal.js"],
  "sourcesContent": ["import USALLib from '../usal.js';\n\nexport const usal = (node, value = 'fade') => {\n  node.setAttribute('data-usal', value);\n\n  return {\n    update(newValue) {\n      node.setAttribute('data-usal', newValue);\n    },\n    destroy() {},\n  };\n};\n\nexport const useUSAL = () => ({\n  config: (config) => {\n    if (config === undefined) {\n      return USALLib.config();\n    }\n    USALLib.config(config);\n  },\n  destroy: () => USALLib.destroy(),\n  restart: () => USALLib.restart(),\n});\n\nexport default USALLib;\n", "// noinspection JSBitwiseOperatorUsage\n\nconst USAL = (() => {\n  // Return existing instance\n  if (typeof window !== 'undefined' && window.USAL) {\n    return window.USAL;\n  }\n\n  // SSR safety\n  if (typeof window === 'undefined') {\n    return {\n      config: function () {\n        return arguments.length === 0 ? {} : this;\n      },\n      destroy: async () => {},\n      restart: async function () {\n        return this;\n      },\n      initialized: () => false,\n      version: '{%%VERSION%%}',\n    };\n  }\n\n  // ============================================================================\n  // Configuration & State\n  // ============================================================================\n\n  const defaultConfig = {\n    defaults: {\n      animation: 'fade',\n      direction: 'u',\n      duration: 1000,\n      delay: 0,\n      threshold: 10,\n      splitDelay: 30,\n      forwards: false,\n      easing: 'ease-out',\n      blur: false,\n      loop: 'mirror',\n    },\n    observersDelay: 50,\n    once: false,\n  };\n\n  const instance = {\n    destroying: null,\n    restarting: null,\n    initialized: false,\n    observers: () => {},\n    elements: new Map(),\n    config: { ...defaultConfig },\n  };\n\n  // ============================================================================\n  // Constants\n  // ============================================================================\n\n  const SHADOW_CAPABLE_SELECTOR =\n    '*:not(:is(area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr,textarea,select,option,optgroup,script,style,title,iframe,object,video,audio,canvas,map,svg,math))';\n\n  const DATA_USAL_ATTRIBUTE = 'data-usal';\n  const DATA_USAL_ID = `${DATA_USAL_ATTRIBUTE}-id`;\n  const DATA_USAL_SELECTOR = `[${DATA_USAL_ATTRIBUTE}]`;\n\n  const CONFIG_ANIMATION = 0;\n  const CONFIG_DIRECTION = 1;\n  const CONFIG_DURATION = 2;\n  const CONFIG_DELAY = 3;\n  const CONFIG_THRESHOLD = 4;\n  const CONFIG_EASING = 5;\n  const CONFIG_BLUR = 6;\n  const CONFIG_ONCE = 7;\n  const CONFIG_SPLIT = 8;\n  const CONFIG_COUNT = 9;\n  const CONFIG_TEXT = 10;\n  const CONFIG_LOOP = 11;\n  const CONFIG_FORWARDS = 12;\n  const CONFIG_TUNING = 13;\n  const CONFIG_LINE = 14;\n  const CONFIG_STAGGER = 15;\n\n  const DIRECTION_UP = 1;\n  const DIRECTION_DOWN = 2;\n  const DIRECTION_LEFT = 4;\n  const DIRECTION_RIGHT = 8;\n\n  const STYLE_OPACITY = 'opacity';\n  const STYLE_TRANSFORM = 'transform';\n  const STYLE_FILTER = 'filter';\n  const STYLE_PERSPECTIVE = 'perspective';\n  const STYLE_DISPLAY = 'display';\n  const STYLE_FONT_WEIGHT = 'fontWeight';\n  const CSS_NONE = 'none';\n  const CSS_INLINE_BLOCK = 'inline-block';\n\n  const INTERSECTION_THRESHOLDS = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];\n  // Animation Types\n  // \u26A0\uFE0F WARNING: Order MUST match array below! \u26A0\uFE0F\n  const ANIMATION_FADE = 0;\n  const ANIMATION_ZOOMIN = 1;\n  const ANIMATION_ZOOMOUT = 2;\n  const ANIMATION_FLIP = 3;\n  const ANIMATION_SLIDE = 4;\n  // \u26A0\uFE0F WARNING: Order MUST match constants above! \u26A0\uFE0F\n  const ANIMATION_TYPES = ['fade', 'zoomin', 'zoomout', 'flip', 'slide'];\n\n  // ============================================================================\n  // Utilities\n  // ============================================================================\n\n  const genTmpId = () => `__usal${Date.now()}_${Math.random().toString(36).slice(2)}`;\n\n  function calculateVisibilityRatio(element) {\n    const rect = element.getBoundingClientRect();\n    const windowHeight = window.innerHeight;\n    const windowWidth = window.innerWidth;\n\n    if (\n      rect.bottom <= 0 ||\n      rect.top >= windowHeight ||\n      rect.right <= 0 ||\n      rect.left >= windowWidth\n    ) {\n      return 0;\n    }\n\n    const visibleHeight = Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0);\n    const visibleWidth = Math.min(rect.right, windowWidth) - Math.max(rect.left, 0);\n    return (visibleHeight / rect.height) * (visibleWidth / rect.width);\n  }\n\n  function captureComputedStyle(element) {\n    const computedStyle = window.getComputedStyle(element);\n    return {\n      [STYLE_OPACITY]: computedStyle[STYLE_OPACITY] || '1',\n      [STYLE_TRANSFORM]: computedStyle[STYLE_TRANSFORM] || CSS_NONE,\n      [STYLE_FILTER]: computedStyle[STYLE_FILTER] || CSS_NONE,\n      [STYLE_PERSPECTIVE]: computedStyle[STYLE_PERSPECTIVE] || CSS_NONE,\n      [STYLE_FONT_WEIGHT]: computedStyle[STYLE_FONT_WEIGHT] || '400',\n    };\n  }\n\n  // ============================================================================\n  // Style Management\n  // ============================================================================\n\n  function applyStyles(element, styles, clean = false) {\n    if (!element) return;\n\n    // eslint-disable-next-line no-unused-vars\n    const { offset, composite, easing, ...cleanedStyles } = styles;\n\n    element.animate([cleanedStyles], {\n      duration: 0,\n      fill: 'forwards',\n      iterations: 1,\n      id: genTmpId(),\n    });\n\n    if (instance.destroying == null && !clean) {\n      element.__usalFragment = 1;\n    } else {\n      delete element.__usalFragment;\n      delete element.__usalOGStyle;\n      delete element.__usalID;\n    }\n  }\n\n  const cancelAllAnimations = (data, element, originalStyle) =>\n    new Promise((resolve) => {\n      requestAnimationFrame(() => {\n        if (!element) {\n          resolve();\n          return;\n        }\n        element\n          .getAnimations()\n          .filter((animation) => animation.id && animation.id.startsWith('__usal'))\n          .forEach((animation) => {\n            animation.cancel();\n            animation.effect = null;\n            animation.timeline = null;\n            if (originalStyle) {\n              if (data?.config?.[CONFIG_SPLIT] && !data.config[CONFIG_SPLIT].includes('item'))\n                originalStyle = {\n                  ...originalStyle,\n                  [STYLE_DISPLAY]: CSS_INLINE_BLOCK,\n                };\n              applyStyles(element, originalStyle);\n            }\n          });\n        resolve();\n      });\n    });\n\n  function resetStyle(data) {\n    if (data.config[CONFIG_LOOP]) return;\n    const originalStyle = data.element.__usalOGStyle;\n    if (data.countData) {\n      const span = data.countData.span;\n      applyStyles(span, {\n        [STYLE_DISPLAY]: 'inline',\n      });\n    } else if (data.config[CONFIG_SPLIT]) {\n      if (data.targets) {\n        data.targets().forEach(([target]) => {\n          applyStyles(\n            target,\n            createKeyframes(\n              data.splitConfig || data.config,\n              target.__usalOGStyle || originalStyle\n            )[0]\n          );\n        });\n      }\n    } else {\n      applyStyles(data.element, createKeyframes(data.config, originalStyle)[0]);\n    }\n    data.stop = false;\n  }\n  // ============================================================================\n  // Configuration Parsing\n  // ============================================================================\n\n  function extractAndSetConfig(prefix, config, configKey, classString) {\n    const pattern = new RegExp(`${prefix}-\\\\[[^\\\\]]+\\\\]`);\n    const match = classString.match(pattern);\n    if (match) {\n      config[configKey] = match[0].slice(prefix.length + 2, -1);\n      return classString.replace(match[0], '');\n    }\n    return classString;\n  }\n\n  const extractAnimation = (firstPart, fallback = null) => {\n    const animationIndex = ANIMATION_TYPES.indexOf(firstPart);\n    return animationIndex !== -1 ? animationIndex : fallback;\n  };\n\n  const extractDirection = (secondPart, fallback = null) => {\n    if (!secondPart) return fallback;\n\n    let direction = 0;\n    for (const char of secondPart) {\n      switch (char) {\n        case 'u':\n          direction |= DIRECTION_UP;\n          break;\n        case 'd':\n          direction |= DIRECTION_DOWN;\n          break;\n        case 'l':\n          direction |= DIRECTION_LEFT;\n          break;\n        case 'r':\n          direction |= DIRECTION_RIGHT;\n          break;\n      }\n    }\n    return direction > 0 ? direction : fallback;\n  };\n\n  function genEmptyConfig() {\n    const config = new Array(16).fill(null);\n    config[CONFIG_TUNING] = [];\n    config[CONFIG_STAGGER] = 'index';\n    return config;\n  }\n\n  function parseClasses(classString) {\n    const config = genEmptyConfig();\n\n    classString = extractAndSetConfig('count', config, CONFIG_COUNT, classString);\n    classString = extractAndSetConfig('easing', config, CONFIG_EASING, classString);\n    classString = extractAndSetConfig('line', config, CONFIG_LINE, classString);\n\n    const tokens = classString.split(/\\s+/).filter(Boolean);\n\n    for (const token of tokens) {\n      const parts = token.split('-');\n      const firstPart = parts[0];\n\n      if (config[CONFIG_ANIMATION] === null) {\n        config[CONFIG_ANIMATION] = extractAnimation(firstPart);\n        if (config[CONFIG_ANIMATION] !== null) {\n          config[CONFIG_DIRECTION] = extractDirection(parts[1]);\n          config[CONFIG_TUNING] = parts\n            .slice(1 + (config[CONFIG_DIRECTION] ? 1 : 0))\n            .filter((item) => !isNaN(item) && item !== '')\n            .map((item) => +item);\n          continue;\n        }\n      }\n\n      switch (token) {\n        case 'once':\n          config[CONFIG_ONCE] = true;\n          break;\n        case 'forwards':\n          config[CONFIG_FORWARDS] = true;\n          break;\n        case 'linear':\n        case 'ease':\n        case 'ease-in':\n        case 'ease-out':\n        case 'ease-in-out':\n        case 'step-start':\n        case 'step-end':\n          config[CONFIG_EASING] = token;\n          break;\n        default:\n          switch (firstPart) {\n            case 'split':\n              if (parts[1])\n                config[CONFIG_SPLIT] = (config[CONFIG_SPLIT] ?? '') + ' ' + token.slice(6);\n              break;\n            case 'blur':\n              if (parts[1]) config[CONFIG_BLUR] = +parts[1];\n              else config[CONFIG_BLUR] = true;\n              break;\n            case 'loop':\n              if (parts[1] === 'mirror' || parts[1] === 'jump') {\n                config[CONFIG_LOOP] = parts[1];\n              } else config[CONFIG_LOOP] = true;\n              break;\n            case 'text':\n              if (parts[1] === 'shimmer' || parts[1] === 'fluid') config[CONFIG_TEXT] = parts[1];\n              break;\n            case 'duration':\n              if (parts[1]) config[CONFIG_DURATION] = +parts[1];\n              break;\n            case 'delay':\n              if (parts[1]) config[CONFIG_DELAY] = +parts[1];\n              if (parts[2]) config[CONFIG_STAGGER] = parts[2];\n              break;\n            case 'threshold':\n              if (parts[1]) config[CONFIG_THRESHOLD] = +parts[1];\n              break;\n          }\n      }\n    }\n\n    return config;\n  }\n\n  // ============================================================================\n  // Animation Keyframes\n  // ============================================================================\n\n  function parseTimeline(content, originalStyle, inlineBlock = false) {\n    const clean = content.replace(/\\s/g, '').toLowerCase();\n\n    function buildTransform(type, axis, value, unit) {\n      const axisStr =\n        axis && ['x', 'y', 'z'].includes(axis) ? axis.toUpperCase() : type === 'rotate' ? 'Z' : '';\n      return `${type}${axisStr}(${value}${unit})`;\n    }\n\n    function parseTransforms(str) {\n      const regex = /(\\w|\\w\\w)([+-]\\d+(?:\\.\\d+)?)/g;\n\n      let transforms = '';\n      let opacity = null;\n      let blur = null;\n      let perspective = null;\n      let glow = null;\n      let fontWeight = null;\n\n      let match;\n      while ((match = regex.exec(str)) !== null) {\n        const [, prop, value] = match;\n        const num = parseFloat(value);\n        const first = prop[0];\n        const second = prop[1];\n\n        switch (first) {\n          case 't':\n            transforms += ' ' + buildTransform('translate', second, num, '%');\n            break;\n          case 'r':\n            transforms += ' ' + buildTransform('rotate', second, num, 'deg');\n            break;\n          case 's':\n            transforms += ' ' + buildTransform('scale', second, num, '');\n            break;\n          case 'o':\n            opacity = Math.max(0, Math.min(100, num)) / 100;\n            break;\n          case 'b':\n            blur = `blur(${Math.max(0, num)}rem)`;\n            break;\n          case 'g':\n            glow = `brightness(${Math.max(0, num) / 100})`;\n            break;\n          case 'w':\n            fontWeight = Math.max(0, num).toString();\n            break;\n          case 'p':\n            perspective = `${num}rem`;\n            break;\n        }\n      }\n\n      const result = {};\n      if (transforms) result[STYLE_TRANSFORM] = transforms.trim();\n      if (opacity !== null) result[STYLE_OPACITY] = opacity;\n      if (blur || glow) {\n        result[STYLE_FILTER] = [blur, glow].filter(Boolean).join(' ');\n      }\n      if (fontWeight) result[STYLE_FONT_WEIGHT] = fontWeight;\n      if (perspective) result[STYLE_PERSPECTIVE] = perspective;\n      return result;\n    }\n\n    const keyframes = new Map();\n    clean.split('|').forEach((frame, index) => {\n      const percentMatch = frame.match(/^(\\d+)/);\n      const percent =\n        index === 0\n          ? 0\n          : percentMatch\n            ? Math.max(0, Math.min(100, parseInt(percentMatch[1])))\n            : 100;\n      keyframes.set(percent, parseTransforms(frame.replace(/^\\d+/, '')));\n    });\n\n    if (Object.keys(keyframes.get(0)).length === 0) {\n      keyframes.set(0, originalStyle);\n    }\n    if (keyframes.size === 1) {\n      keyframes.set(100, originalStyle);\n    } else {\n      const allKeys = [...keyframes.keys()];\n      if (keyframes.size >= 3) {\n        const minKey = Math.min(...allKeys);\n        keyframes.set(0, keyframes.get(minKey));\n      }\n      const maxKey = Math.max(...allKeys);\n      keyframes.set(100, keyframes.get(maxKey));\n    }\n\n    const sorted = Array.from(keyframes.entries())\n      .filter(([_, frame]) => Object.keys(frame).length > 0)\n      .sort((a, b) => a[0] - b[0]);\n\n    const compressed = sorted.map(([percent, frame]) => ({\n      offset: (5 + percent * 0.9) / 100,\n      ...frame,\n      ...(inlineBlock && { display: 'inline-block' }),\n    }));\n\n    const first = { ...sorted[0][1], ...(inlineBlock && { display: 'inline-block' }) };\n    const last = {\n      ...sorted[sorted.length - 1][1],\n      ...(inlineBlock && { display: 'inline-block' }),\n    };\n\n    return [{ offset: 0, ...first }, ...compressed, { offset: 1, ...last }];\n  }\n\n  function createKeyframes(config, originalStyle) {\n    if (!originalStyle) return;\n    const isSplitText = config[CONFIG_SPLIT] && !config[CONFIG_SPLIT]?.includes('item');\n\n    if (config[CONFIG_TEXT] === 'shimmer') config[CONFIG_LINE] = 'o+50g+100|50o+100g+130|o+50g+100';\n    else if (config[CONFIG_TEXT] === 'fluid') config[CONFIG_LINE] = 'w+100|50w+900|w+100';\n\n    if (config[CONFIG_LINE]) return parseTimeline(config[CONFIG_LINE], originalStyle, isSplitText);\n\n    const animationType =\n      config[CONFIG_ANIMATION] ??\n      extractAnimation(instance.config.defaults.animation, ANIMATION_FADE);\n    const direction =\n      config[CONFIG_DIRECTION] ?? extractDirection(instance.config.defaults.direction, 1);\n    const blur = config[CONFIG_BLUR] ?? instance.config.defaults.blur;\n\n    const tuning = config[CONFIG_TUNING];\n\n    let firstTuning = tuning?.at(0);\n    const lastTuning = tuning?.at(-1);\n    let secondTuning = tuning?.at(1);\n\n    let fromTimeline = 'o+0';\n    if (animationType === ANIMATION_SLIDE)\n      fromTimeline = `o+${parseFloat(originalStyle[STYLE_OPACITY]) * 100}`;\n\n    const defaultDelta = isSplitText ? 50 : 15;\n    const intensity = (lastTuning ?? defaultDelta) / 100;\n\n    if (animationType === ANIMATION_ZOOMIN || animationType === ANIMATION_ZOOMOUT) {\n      // Zoom\n      fromTimeline += `s+${1 + (animationType === ANIMATION_ZOOMIN ? -intensity : intensity)}`;\n      firstTuning = null;\n      secondTuning = tuning?.length === 2 ? null : secondTuning;\n    } else if (animationType === ANIMATION_FLIP) {\n      // Flip\n      const angle = firstTuning ?? 90;\n      if (direction & (DIRECTION_UP | DIRECTION_DOWN)) {\n        const rotX = direction & DIRECTION_UP ? angle : -angle;\n        fromTimeline += `rx${rotX > 0 ? '+' : ''}${rotX}`;\n      }\n      if (direction & (DIRECTION_LEFT | DIRECTION_RIGHT)) {\n        const rotY = direction & DIRECTION_LEFT ? -angle : angle;\n        fromTimeline += `ry${rotY > 0 ? '+' : ''}${rotY}`;\n      }\n      if (!(direction & (DIRECTION_UP | DIRECTION_DOWN | DIRECTION_LEFT | DIRECTION_RIGHT))) {\n        fromTimeline += `ry+${angle}`;\n      }\n      const perspectiveValue = tuning?.length === 2 ? lastTuning : 25;\n      fromTimeline += `p+${perspectiveValue ?? 25}`;\n    }\n\n    if (animationType !== ANIMATION_FLIP && direction) {\n      if (direction & DIRECTION_RIGHT) {\n        fromTimeline += `tx-${firstTuning ?? defaultDelta}`;\n      } else if (direction & DIRECTION_LEFT) {\n        fromTimeline += `tx+${firstTuning ?? defaultDelta}`;\n      }\n\n      if (direction & DIRECTION_DOWN) {\n        fromTimeline += `ty-${secondTuning ?? firstTuning ?? defaultDelta}`;\n      } else if (direction & DIRECTION_UP) {\n        fromTimeline += `ty+${secondTuning ?? firstTuning ?? defaultDelta}`;\n      }\n    }\n\n    if (blur) {\n      const blurValue =\n        blur === true\n          ? 0.625\n          : typeof blur === 'number' && !isNaN(blur)\n            ? Math.max(0, blur)\n            : 0.625;\n      fromTimeline += `b+${blurValue}`;\n    }\n\n    return parseTimeline(fromTimeline, originalStyle, isSplitText);\n  }\n\n  // ============================================================================\n  // Split Animation Setup\n  // ============================================================================\n  function getStaggerFunction(targets, strategy = 'index') {\n    const targetsData = targets.map((target) => {\n      const rect = target.getBoundingClientRect();\n      return {\n        target,\n        x: rect.left + rect.width / 2,\n        y: rect.top + rect.height / 2,\n      };\n    });\n\n    const bounds = targetsData.reduce(\n      (acc, item) => ({\n        minX: Math.min(acc.minX, item.x),\n        maxX: Math.max(acc.maxX, item.x),\n        minY: Math.min(acc.minY, item.y),\n        maxY: Math.max(acc.maxY, item.y),\n      }),\n      { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity }\n    );\n\n    const centerX = (bounds.minX + bounds.maxX) / 2;\n    const centerY = (bounds.minY + bounds.maxY) / 2;\n\n    const metrics = targetsData.map((item, index) => {\n      let value;\n      switch (strategy) {\n        case 'linear':\n          value = Math.hypot(item.x, item.y);\n          break;\n        case 'center':\n          value = Math.hypot(item.x - centerX, item.y - centerY);\n          break;\n        case 'edges':\n          value = Math.min(\n            Math.abs(item.x - bounds.minX),\n            Math.abs(item.x - bounds.maxX),\n            Math.abs(item.y - bounds.minY),\n            Math.abs(item.y - bounds.maxY)\n          );\n          break;\n        case 'random':\n          value = Math.random();\n          break;\n        default: // index\n          value = index;\n      }\n      return value;\n    });\n\n    const min = Math.min(...metrics);\n    const max = Math.max(...metrics);\n    const range = max - min || 1;\n\n    return (splitDelay = 50) =>\n      targetsData.map((item, index) => {\n        const normalizedValue = (metrics[index] - min) / range;\n\n        let delay;\n        if (strategy === 'index') {\n          delay = index * splitDelay;\n        } else {\n          delay = normalizedValue * (targets.length - 1) * splitDelay;\n        }\n\n        return [item.target, delay];\n      });\n  }\n\n  function setupSplit(element, splitBy, strategy, resolve) {\n    const targets = [];\n\n    // Split by child elements\n    if (splitBy === 'item') {\n      Array.from(element.children).forEach((child) => {\n        child.__usalOGStyle = captureComputedStyle(child);\n        targets.push(child);\n      });\n      return [getStaggerFunction(targets, strategy), null];\n    }\n\n    // Split by text\n    function createSpan(content) {\n      const span = document.createElement('span');\n      span.textContent = content;\n      return span;\n    }\n\n    function processTextContent(text) {\n      if (!text?.trim()) return text ? document.createTextNode(text) : null;\n\n      const wrapper = document.createElement('span');\n      const words = text.split(/(\\s+)/);\n\n      words.forEach((word) => {\n        if (!word) return;\n\n        if (/\\s/.test(word)) {\n          wrapper.appendChild(document.createTextNode(word));\n          return;\n        }\n\n        // Split Word\n        if (splitBy === 'word') {\n          const span = createSpan(word);\n          applyStyles(span, { [STYLE_DISPLAY]: CSS_INLINE_BLOCK });\n          wrapper.appendChild(span);\n          targets.push(span);\n          return;\n        }\n\n        // Split letter\n        const container = document.createElement('span');\n        applyStyles(container, { [STYLE_DISPLAY]: CSS_INLINE_BLOCK, whiteSpace: 'nowrap' });\n\n        let chars;\n        if (typeof Intl !== 'undefined' && Intl.Segmenter) {\n          const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n          chars = Array.from(segmenter.segment(word), (s) => s.segment);\n        } else {\n          chars = word.match(\n            /\\p{RI}\\p{RI}|(?:\\p{Emoji}(?:\\u200D\\p{Emoji})*)|(?:\\P{M}\\p{M}*)|./gsu\n          ) || [word];\n        }\n\n        chars.forEach((char) => {\n          const span = createSpan(char);\n          container.appendChild(span);\n          targets.push(span);\n        });\n\n        wrapper.appendChild(container);\n      });\n\n      return wrapper;\n    }\n\n    const textNodes = [];\n    let countTextNodes = 0;\n    let wrappers = null;\n    const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);\n\n    while (walker.nextNode()) {\n      if (walker.currentNode.textContent.trim()) {\n        textNodes.push(walker.currentNode);\n        countTextNodes++;\n      }\n    }\n\n    if (textNodes.length) wrappers = [];\n    textNodes.forEach((textNode) => {\n      if (!textNode.parentNode || !textNode.isConnected) {\n        countTextNodes--;\n        if (countTextNodes === 0) resolve();\n        return;\n      }\n\n      const processed = processTextContent(textNode.textContent);\n      wrappers.push(processed);\n      requestAnimationFrame(() => {\n        requestAnimationFrame(() => {\n          try {\n            if (textNode.parentNode) {\n              textNode.parentNode.replaceChild(processed, textNode);\n            }\n          } finally {\n            countTextNodes--;\n            if (countTextNodes === 0) resolve();\n          }\n        });\n      });\n    });\n\n    return [getStaggerFunction(targets, strategy), wrappers];\n  }\n\n  // ============================================================================\n  // Count Animation Setup\n  // ============================================================================\n\n  function setupCount(element, config, data, resolve) {\n    const original = config[CONFIG_COUNT].trim();\n    const clean = original.replace(/[^\\d\\s,.]/g, '');\n\n    const separators = [',', '.', ' '].filter((s) => clean.includes(s));\n    const sepPositions = separators\n      .map((s) => ({ s, p: clean.lastIndexOf(s) }))\n      .sort((a, b) => b.p - a.p);\n\n    let value,\n      decimals = 0,\n      thousandSep = '',\n      decimalSep = '';\n\n    if (separators.length === 0) {\n      value = parseFloat(clean);\n    } else if (separators.length === 1) {\n      const sep = separators[0];\n      const afterSep = clean.substring(clean.lastIndexOf(sep) + 1);\n\n      if (afterSep.length <= 3 && afterSep.length > 0 && sep !== ' ') {\n        decimalSep = sep;\n        decimals = afterSep.length;\n        value = parseFloat(clean.replace(sep, '.'));\n      } else {\n        thousandSep = sep;\n        value = parseFloat(clean.replace(new RegExp(`\\\\${thousandSep}`, 'g'), ''));\n      }\n    } else {\n      decimalSep = sepPositions[0].s;\n      thousandSep = sepPositions[1].s;\n      const processed = clean\n        .replace(new RegExp(`\\\\${thousandSep}`, 'g'), '')\n        .replace(decimalSep, '.');\n      value = parseFloat(processed);\n      decimals = clean.substring(sepPositions[0].p + 1).replace(/\\D/g, '').length;\n    }\n\n    let span = null;\n    let wrapper = null;\n\n    function findAndReplace(node) {\n      if (span) return;\n\n      if (node.nodeType === Node.TEXT_NODE) {\n        const text = node.textContent;\n        const index = text.indexOf(config[CONFIG_COUNT]);\n\n        if (index !== -1) {\n          const before = text.substring(0, index);\n          const after = text.substring(index + config[CONFIG_COUNT].length);\n\n          wrapper = document.createElement('span');\n\n          if (before) wrapper.appendChild(document.createTextNode(before));\n\n          span = document.createElement('span');\n          span.textContent = original;\n          wrapper.appendChild(span);\n\n          if (after) wrapper.appendChild(document.createTextNode(after));\n\n          data.textWrappers = [wrapper];\n          requestAnimationFrame(() => {\n            requestAnimationFrame(() => {\n              try {\n                if (node.parentNode) {\n                  node.parentNode.replaceChild(wrapper, node);\n                }\n              } finally {\n                resolve();\n              }\n            });\n          });\n        }\n      } else if (node.nodeType === Node.ELEMENT_NODE) {\n        Array.from(node.childNodes).forEach(findAndReplace);\n      }\n    }\n\n    findAndReplace(element);\n\n    if (!span) {\n      resolve();\n      return false;\n    }\n\n    data.countData = { value, decimals, span, thousandSep, decimalSep };\n    return true;\n  }\n\n  // ============================================================================\n  // Count Animation\n  // ============================================================================\n\n  function animateCount(countData, options) {\n    const { duration, easing } = options;\n    const { value, decimals, span, thousandSep, decimalSep } = countData;\n\n    let currentTime = 0;\n    let playState = 'idle';\n    let playbackRate = 1;\n\n    function getEasingFunction(easingType) {\n      switch (easingType) {\n        case 'linear':\n          return (t) => t;\n\n        case 'ease':\n          return (t) => {\n            if (t === 0) return 0;\n            if (t === 1) return 1;\n            return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;\n          };\n\n        case 'ease-in':\n          return (t) => t * t * t;\n\n        case 'ease-out':\n          return (t) => 1 - Math.pow(1 - t, 3);\n\n        case 'ease-in-out':\n          return (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);\n\n        default:\n          return (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);\n      }\n    }\n\n    const easingFunction = getEasingFunction(easing);\n\n    function formatNumber(val) {\n      const parts = (decimals > 0 ? val.toFixed(decimals) : Math.floor(val).toString()).split('.');\n      if (thousandSep && parts[0].length > 3) {\n        const reversed = parts[0].split('').reverse();\n        parts[0] = reversed.reduce(\n          (acc, digit, i) => (i > 0 && i % 3 === 0 ? digit + thousandSep + acc : digit + acc),\n          ''\n        );\n      }\n      return parts.length > 1 && decimalSep ? parts[0] + decimalSep + parts[1] : parts[0];\n    }\n\n    function updateValue(time) {\n      const progress = Math.max(0, Math.min(1, time / duration));\n\n      if (progress <= 0.06) {\n        span.textContent = formatNumber(0);\n      } else if (progress >= 0.94) {\n        span.textContent = formatNumber(value);\n      } else {\n        const adjustedProgress = (progress - 0.05) / 0.9;\n        const easedProgress = easingFunction(adjustedProgress);\n        const currentValue = value * easedProgress;\n        span.textContent = formatNumber(currentValue);\n      }\n\n      if ((progress >= 1 && playbackRate > 0) || (progress <= 0 && playbackRate < 0)) {\n        playState = 'finished';\n      }\n    }\n\n    return {\n      tick(elapsed, loop = false) {\n        if ((loop && playState !== 'paused') || playState === 'running') {\n          playState = 'running';\n\n          currentTime = currentTime + (playbackRate > 0 ? elapsed : -elapsed);\n          currentTime = Math.max(0, Math.min(duration, currentTime));\n\n          updateValue(currentTime);\n        }\n      },\n\n      play() {\n        if (playState === 'finished' || playState === 'running') return;\n        playState = 'running';\n      },\n\n      pause() {\n        if (playState === 'running') {\n          playState = 'paused';\n        }\n      },\n\n      cancel() {\n        currentTime = duration * 0.95;\n        updateValue(currentTime);\n        playState = 'finished';\n      },\n\n      persist() {},\n\n      get playState() {\n        return playState;\n      },\n\n      get currentTime() {\n        return currentTime;\n      },\n\n      set currentTime(time) {\n        currentTime = Math.max(0, Math.min(duration, time));\n        updateValue(currentTime);\n      },\n\n      get playbackRate() {\n        return playbackRate;\n      },\n\n      set playbackRate(rate) {\n        playbackRate = rate;\n      },\n    };\n  }\n  // ============================================================================\n  // Animation Controller\n  // ============================================================================\n\n  class AnimationController {\n    reset() {\n      this.rafId = null;\n      this.lastTickTime = null;\n      this.virtualTime = 0;\n      this.animations = new Map();\n    }\n\n    constructor(data) {\n      this.data = data;\n      this.reset();\n    }\n\n    timeToSayGoodbye() {\n      if (this.animations.size !== 0) return false;\n      cancelAnimationFrame(this.rafId);\n      this.reset();\n      this.data.resolve();\n      return true;\n    }\n\n    cleanupAnimation(toCleanup) {\n      toCleanup.forEach(([animation, info]) => {\n        const clean = () => {\n          this.animations.delete(animation);\n          this.timeToSayGoodbye();\n        };\n        if (this.data.countData) {\n          animation.cancel();\n          clean();\n        } else\n          cancelAllAnimations(this.data, info.element, info.originalStyle).then(() => {\n            clean();\n          });\n      });\n    }\n\n    prepare(elapsed) {\n      const toCleanup = [];\n      const toAnimate = [];\n\n      for (const [animation, info] of this.animations) {\n        if (this.data.stop) {\n          toCleanup.push([animation, info]);\n          continue;\n        }\n\n        animation.tick?.(elapsed, info.loop);\n\n        const progress = animation.currentTime / info.duration;\n\n        if (!info.loop && (progress >= 0.95 || animation.playState === 'finished')) {\n          toCleanup.push([animation, info]);\n          continue;\n        } else if (\n          !info.waiting &&\n          !info.pendingPlay &&\n          ((progress >= 0.95 && info.playbackRate > 0) ||\n            (progress <= 0.05 && info.playbackRate < 0))\n        ) {\n          if (info.playbackRate < 0) animation.currentTime = info.duration * 0.03;\n          else animation.currentTime = info.duration * 0.97;\n          animation.pause();\n          info.waiting = true;\n        }\n\n        if (info.pendingPlay && this.virtualTime >= info.playAt) {\n          info.pendingPlay = false;\n          animation.play();\n        }\n\n        toAnimate.push(info);\n      }\n\n      return { toCleanup, toAnimate };\n    }\n\n    animate(toAnimate) {\n      if (toAnimate.length > 0 && toAnimate.every((info) => info.waiting)) {\n        const isJump = toAnimate[0].loop === 'jump';\n        const newPlaybackRate = isJump ? 1 : -toAnimate[0].playbackRate;\n\n        const delays = toAnimate.map((info) => info.staggerDelay);\n        const maxDelay = Math.max(...delays);\n\n        toAnimate.forEach((next) => {\n          if (isJump) next.animation.currentTime = next.duration * 0.03;\n          next.animation.playbackRate = newPlaybackRate;\n          next.playbackRate = newPlaybackRate;\n          next.waiting = false;\n\n          let delay = next.staggerDelay;\n          if (newPlaybackRate < 0) {\n            delay = maxDelay - next.staggerDelay;\n          }\n\n          if (!next.hasStarted && next.initialDelay > 0) {\n            next.hasStarted = true;\n            delay += next.initialDelay;\n          }\n\n          if (delay === 0) {\n            next.animation.play();\n          } else {\n            next.playAt = this.virtualTime + delay;\n            next.pendingPlay = true;\n          }\n        });\n      }\n    }\n\n    tick() {\n      const now = performance.now();\n      const elapsed = Math.min(now - (this.lastTickTime ?? now), 16.67);\n      this.virtualTime += elapsed;\n      this.lastTickTime = now;\n\n      const { toCleanup, toAnimate } = this.prepare(elapsed);\n\n      this.cleanupAnimation(toCleanup);\n\n      this.animate(toAnimate);\n\n      if (!this.timeToSayGoodbye()) {\n        this.rafId = requestAnimationFrame(() => this.tick());\n      }\n    }\n\n    add(element, config, originalStyle, initialDelay = 0, staggerDelay = 0) {\n      const duration = Math.max(\n        0,\n        ((this.data.config[CONFIG_DURATION] ?? instance.config.defaults.duration ?? 1000) + 1) / 0.9\n      );\n      const easing = this.data.config[CONFIG_EASING] ?? instance.config.defaults.easing;\n      const forwards =\n        this.data.config[CONFIG_FORWARDS] ?? instance.config.defaults.forwards ?? false;\n      const loopDefaults = instance.config.defaults.loop ?? 'mirror';\n      let loop =\n        this.data.config[CONFIG_LOOP] === true ? loopDefaults : this.data.config[CONFIG_LOOP];\n\n      let options = {\n        duration,\n        easing,\n        fill: 'forwards',\n      };\n\n      let keyframes = [];\n\n      if (this.data.config[CONFIG_TEXT]) {\n        loop = loop ?? loopDefaults;\n        options.easing = this.data.config[CONFIG_EASING] ?? 'linear';\n      }\n\n      keyframes = createKeyframes(config, originalStyle);\n      if (forwards) originalStyle = keyframes[keyframes.length - 1];\n\n      options = {\n        ...options,\n        delay: 0,\n        id: genTmpId(),\n      };\n      const animation = this.data.countData\n        ? animateCount(this.data.countData, options)\n        : element.animate(keyframes, options);\n\n      animation.persist();\n      animation.currentTime = duration * 0.03;\n      animation.pause();\n\n      this.animations.set(animation, {\n        animation,\n        element,\n        duration,\n        staggerDelay,\n        initialDelay,\n        originalStyle,\n        loop,\n        playbackRate: -1,\n        waiting: true,\n        hasStarted: false,\n      });\n    }\n\n    letsGo() {\n      const { element, config, targets, splitConfig } = this.data;\n      const initialDelay = Math.max(0, config[CONFIG_DELAY] ?? instance.config.defaults.delay ?? 0);\n      const originalStyle = element.__usalOGStyle;\n      const splitDelay = Math.max(\n        0,\n        splitConfig[CONFIG_DELAY] ?? instance.config.defaults.splitDelay ?? 0\n      );\n\n      let notReadYet = targets?.()?.length || (originalStyle ? 0 : 1);\n\n      if (targets) {\n        targets(splitDelay).forEach(([target, staggerDelay]) => {\n          const targetOriginalStyle = target.__usalOGStyle || originalStyle;\n          if (!targetOriginalStyle) return;\n          notReadYet--;\n          this.add(\n            target,\n            this.data.splitConfig,\n            targetOriginalStyle,\n            initialDelay,\n            parseInt(staggerDelay)\n          );\n        });\n      } else if (originalStyle) {\n        this.add(element, config, originalStyle, initialDelay);\n      }\n\n      if (notReadYet === 0 && !this.rafId) {\n        this.tick();\n      }\n    }\n  }\n\n  // ============================================================================\n  // Main Animation Controller\n  // ============================================================================\n\n  function tryAnimate(data) {\n    if (data.stop) return;\n    data.hasAnimated = true;\n\n    data.processing = new Promise((resolve) => {\n      data.resolve = resolve;\n      data.controller.letsGo();\n    }).then(() => {\n      data.onfinish();\n      data.processing = null;\n      data.stop = true;\n    });\n  }\n\n  function tryAnimateIfVisible(data, ratio = null) {\n    if (\n      data.config[CONFIG_LOOP] ||\n      data.processing !== null ||\n      (data.hasAnimated && (data.config[CONFIG_ONCE] ?? instance.config.once))\n    )\n      return;\n\n    const _ratio = ratio ?? calculateVisibilityRatio(data.element);\n\n    if (data.stop && _ratio < 0.01) {\n      resetStyle(data);\n      return;\n    }\n\n    const threshold = Math.max(\n      0,\n      Math.min(1, (data.config[CONFIG_THRESHOLD] ?? instance.config.defaults.threshold) / 100)\n    );\n\n    if (_ratio >= threshold) {\n      tryAnimate(data);\n    }\n  }\n\n  // ============================================================================\n  // Element Processing & Cleanup\n  // ============================================================================\n\n  const cleanupElement = (data) =>\n    new Promise((resolve) => {\n      data.onfinish = () => {\n        data.onfinish = () => {};\n\n        requestAnimationFrame(() =>\n          requestAnimationFrame(() => {\n            if (data.targets) {\n              data.targets().forEach(([target]) => {\n                if (target.__usalOGStyle) {\n                  applyStyles(target, target.__usalOGStyle, true);\n                }\n              });\n            }\n\n            if (data.textWrappers) {\n              data.textWrappers.forEach((wrapper) => {\n                if (wrapper?.parentNode) {\n                  wrapper.parentNode.replaceChild(\n                    document.createTextNode(wrapper.textContent),\n                    wrapper\n                  );\n                }\n              });\n            }\n\n            if (data.element.__usalOGStyle) {\n              applyStyles(data.element, data.element.__usalOGStyle, true);\n            }\n\n            resolve();\n          })\n        );\n      };\n\n      if (data.processing === null) data.onfinish();\n      else data.stop = true;\n    });\n\n  function processElement(element, elementObserver) {\n    if (element.__usalProcessing) return;\n\n    let classes = element.getAttribute(DATA_USAL_ATTRIBUTE) || '';\n    classes = classes\n      .replace(/\\/\\/[^\\n\\r]*/g, '')\n      .replace(/\\/\\*.*?\\*\\//gs, '')\n      .trim()\n      .toLowerCase();\n\n    if (!element.__usalID && classes !== '') {\n      element.__usalOGStyle = captureComputedStyle(element);\n      element.__usalID = element.getAttribute(DATA_USAL_ID) ?? genTmpId();\n    }\n\n    const existingData = instance.elements.get(element.__usalID);\n    if (existingData) {\n      if (classes !== existingData.configString) {\n        element.__usalProcessing = true;\n        instance.elements.delete(element.__usalID);\n        elementObserver.unobserve(element);\n        cleanupElement(existingData).then(() => {\n          delete element.__usalProcessing;\n          processElement(element, elementObserver);\n        });\n      }\n      return;\n    }\n\n    if (classes === '') return;\n\n    element.__usalFragment = 1;\n    const config = parseClasses(classes);\n\n    const data = {\n      element,\n      config,\n      splitConfig: [...config],\n      configString: classes,\n      targets: null,\n      state: null,\n      stop: false,\n      hasAnimated: false,\n      processing: null,\n      countData: null,\n      onfinish: () => {},\n      controller: null,\n      resolve: () => {},\n      textWrappers: null,\n    };\n\n    instance.elements.set(element.__usalID, {\n      configString: classes,\n    });\n\n    const splitBy = config[CONFIG_SPLIT]?.split(' ').find((item) =>\n      ['word', 'letter', 'item'].includes(item)\n    );\n\n    data.processing = new Promise((resolve) => {\n      let resolveNow = false;\n      if (config[CONFIG_COUNT]) {\n        setupCount(element, config, data, resolve);\n      } else if (splitBy) {\n        const splitOverrides = parseClasses(config[CONFIG_SPLIT]);\n        const emptyConfig = genEmptyConfig();\n        data.splitConfig = config.map((value, index) => {\n          const override = splitOverrides[index];\n          const empty = emptyConfig[index];\n          if (Array.isArray(override) && Array.isArray(empty)) {\n            return override.length > 0 ? override : value;\n          }\n          return override !== empty ? override : value;\n        });\n        const [targets, textWrappers] = setupSplit(\n          element,\n          splitBy,\n          data.splitConfig[CONFIG_STAGGER],\n          resolve\n        );\n        data.targets = targets;\n        data.textWrappers = textWrappers;\n        resolveNow = textWrappers === null;\n      } else resolveNow = true;\n      if (resolveNow) resolve();\n    }).then(() => {\n      if (data.stop) data.onfinish();\n      else {\n        instance.elements.set(element.__usalID, data);\n        data.controller = new AnimationController(data);\n        resetStyle(data);\n        requestAnimationFrame(async () => {\n          if (config[CONFIG_LOOP]) {\n            tryAnimate(data);\n          } else {\n            tryAnimateIfVisible(data);\n            elementObserver.observe(element);\n          }\n        });\n      }\n      data.processing = null;\n    });\n  }\n\n  // ============================================================================\n  // Observers Setup\n  // ============================================================================\n\n  function setupObservers() {\n    const domObservers = new Set();\n    const resizeObservers = new Set();\n    const observedDOMs = new Set();\n    let lastScan = 0;\n    let throttleOnTailTimer = null;\n\n    const elementObserver = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          const data = instance.elements.get(\n            entry.target.__usalID || entry.target.getAttribute(DATA_USAL_ID)\n          );\n          if (data) {\n            tryAnimateIfVisible(data, entry.intersectionRatio);\n          }\n        }\n      },\n      { threshold: INTERSECTION_THRESHOLDS }\n    );\n\n    function collectAllDOMs(root = document.body, collected = new Set()) {\n      if (collected.has(root)) return collected;\n      collected.add(root);\n\n      for (const el of root.querySelectorAll(SHADOW_CAPABLE_SELECTOR)) {\n        if (el.shadowRoot && !collected.has(el.shadowRoot)) {\n          // noinspection JSCheckFunctionSignatures\n          collectAllDOMs(el.shadowRoot, collected);\n        }\n      }\n\n      return collected;\n    }\n\n    function observeDOM(dom) {\n      const mutationObs = new MutationObserver(handleObserverEvents);\n      mutationObs.observe(dom, {\n        childList: true,\n        subtree: true,\n        attributes: true,\n      });\n      domObservers.add(mutationObs);\n\n      const resizeObs = new ResizeObserver(handleObserverEvents);\n      if (dom === document.body || dom.host) {\n        resizeObs.observe(dom === document.body ? dom : dom.host);\n        resizeObservers.add(resizeObs);\n      }\n    }\n\n    function scanAllDOMs() {\n      // Clean disconnected elements\n      for (const [id, data] of instance.elements) {\n        if (!data || !data.element) {\n          instance.elements.delete(id);\n          continue;\n        }\n        if (!data.element.isConnected) {\n          elementObserver.unobserve(data.element);\n          cleanupElement(data).then(() => {\n            instance.elements.delete(id);\n          });\n        } else {\n          tryAnimateIfVisible(data);\n        }\n      }\n\n      // Process new elements\n      const allDOMs = collectAllDOMs();\n      for (const dom of allDOMs) {\n        if (!observedDOMs.has(dom)) {\n          observeDOM(dom);\n          observedDOMs.add(dom);\n        }\n        const elements = dom.querySelectorAll?.(DATA_USAL_SELECTOR);\n        for (const element of elements) {\n          processElement(element, elementObserver);\n        }\n      }\n      lastScan = Date.now();\n    }\n\n    function handleObserverEvents(events) {\n      const items = Array.isArray(events) ? events : [events];\n      const hasUsalFragment = (target) => !!target.__usalFragment;\n\n      let cancel = null;\n      for (const item of items) {\n        if (item.type === 'attributes') {\n          const attrName = item.attributeName;\n          if (attrName === DATA_USAL_ATTRIBUTE || attrName === DATA_USAL_ID) {\n            processElement(item.target, elementObserver);\n            cancel = true;\n          }\n        }\n        if (cancel === null) {\n          if (hasUsalFragment(item.target)) cancel = true;\n          if (item.type === 'childList') {\n            const hasUsalFragmentChild = [...item.addedNodes, ...item.removedNodes].some(\n              hasUsalFragment\n            );\n            if (hasUsalFragmentChild) cancel = true;\n          }\n        }\n      }\n\n      if (cancel) return;\n\n      const timeSinceLastScan = Date.now() - lastScan;\n      if (timeSinceLastScan >= instance.config.observersDelay) {\n        scanAllDOMs();\n      } else {\n        if (throttleOnTailTimer) clearTimeout(throttleOnTailTimer);\n        throttleOnTailTimer = setTimeout(\n          () => {\n            scanAllDOMs();\n          },\n          Math.max(0, instance.config.observersDelay - timeSinceLastScan)\n        );\n      }\n    }\n\n    scanAllDOMs();\n\n    return () => {\n      clearTimeout(throttleOnTailTimer);\n      domObservers.forEach((obs) => obs.disconnect());\n      resizeObservers.forEach((obs) => obs.disconnect());\n      elementObserver.disconnect();\n      domObservers.clear();\n      resizeObservers.clear();\n      observedDOMs.clear();\n    };\n  }\n\n  // ============================================================================\n  // Initialization\n  // ============================================================================\n\n  function autoInit() {\n    if (!instance.initialized) {\n      instance.initialized = true;\n      instance.observers = setupObservers();\n    }\n  }\n\n  // ============================================================================\n  // Public API\n  // ============================================================================\n\n  const publicAPI = {\n    config(newConfig = {}) {\n      if (arguments.length === 0) return { ...instance.config };\n\n      if (newConfig.defaults) {\n        newConfig.defaults = {\n          ...instance.config.defaults,\n          ...newConfig.defaults,\n        };\n      }\n\n      Object.assign(instance.config, newConfig);\n      return publicAPI;\n    },\n    async destroy() {\n      if (!instance.initialized) return Promise.resolve();\n      if (instance.destroying != null) return instance.destroying;\n      if (instance.destroyTimer) {\n        clearTimeout(instance.destroyTimer);\n      }\n\n      instance.destroying = new Promise((resolve) => {\n        instance.destroyTimer = setTimeout(async () => {\n          instance.destroyTimer = null;\n\n          instance.observers();\n          const elements = Array.from(instance.elements.values());\n\n          await Promise.all(elements.map((data) => cleanupElement(data)));\n\n          instance.elements.clear();\n          instance.observers = () => {};\n          instance.initialized = false;\n          instance.destroying = null;\n          resolve();\n        }, 50);\n      });\n\n      return instance.destroying;\n    },\n\n    async restart() {\n      if (instance.restarting != null) return instance.restarting;\n\n      if (instance.destroyTimer) {\n        clearTimeout(instance.destroyTimer);\n        instance.destroyTimer = null;\n      }\n      if (instance.restartTimer) {\n        clearTimeout(instance.restartTimer);\n      }\n\n      instance.restarting = new Promise((resolve) => {\n        instance.restartTimer = setTimeout(() => {\n          instance.restartTimer = null;\n\n          publicAPI\n            .destroy()\n            .then(\n              () =>\n                new Promise((resolveInit) => {\n                  requestAnimationFrame(() => {\n                    if (document.readyState =