@usal/svelte
Version:
Ultimate Scroll Animation Library - Lightweight, powerful, wonderfully simple ✨ | Svelte Package
5 lines • 78.6 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 if initialized\n if (typeof window !== 'undefined' && window.USAL?.initialized()) {\n return window.USAL;\n }\n\n // SSR safety\n if (typeof window === 'undefined') {\n const asyncNoop = async function () {\n return this;\n };\n return {\n config: function () {\n return arguments.length === 0 ? {} : this;\n },\n destroy: asyncNoop,\n restart: asyncNoop,\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 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 const ANIMATION_TYPES = ['fade', 'zoomin', 'zoomout', 'flip', 'slide'];\n\n const SHIMMER_KEYFRAMES = Array.from({ length: 17 }, (_, i) => {\n const progress = i / 16;\n const wave = (Math.sin(progress * Math.PI * 2) + 1) / 2;\n\n return {\n opacity: 0.5 + wave * 0.5,\n filter: `brightness(${1 + wave * 0.3})`,\n offset: progress,\n };\n });\n\n const WEIGHT_KEYFRAMES = Array.from({ length: 17 }, (_, i) => {\n const progress = i / 16;\n const wave = (Math.sin(progress * Math.PI * 2) + 1) / 2;\n\n return {\n fontWeight: Math.round(100 + wave * 800).toString(),\n offset: progress,\n };\n });\n\n // ============================================================================\n // Utilities\n // ============================================================================\n\n const genTmpId = () => `__usal${Date.now()}_${Math.random().toString(36).slice(2)}`;\n\n const 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 const 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 };\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.__usalOriginals;\n delete element.__usalID;\n }\n }\n\n const cancelAllAnimations = (data, element, originalStyle) =>\n new Promise((resolve) => {\n setTimeout(() => {\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 }, 0);\n });\n\n const resetStyle = (data) => {\n if (data.config[CONFIG_LOOP]) return;\n const originalStyle = data.element.__usalOriginals?.style;\n if (data.countData) {\n const span = data.countData.span;\n applyStyles(span, {\n [STYLE_DISPLAY]: 'inline',\n });\n span.textContent = '0';\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.__usalOriginals?.style || 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 const genEmptyConfig = () => {\n const config = new Array(16).fill(null);\n config[CONFIG_TUNING] = [];\n config[CONFIG_STAGGER] = 'index';\n return config;\n };\n\n const parseClasses = (classString) => {\n const config = genEmptyConfig();\n\n classString = classString.replace(/\\/\\/[^\\n\\r]*/g, '').replace(/\\/\\*.*?\\*\\//gs, '');\n classString = classString.toLowerCase().trim();\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') {\n config[CONFIG_TEXT] = parts[1];\n config[CONFIG_LOOP] = 'jump';\n }\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 const 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 const parseTransforms = (str) => {\n const regex = /(\\w|\\w\\w)([+-]\\d+(?:\\.\\d+)?)/g;\n\n let transforms = '';\n let opacity = null;\n let filter = null;\n let perspective = 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 filter = `blur(${Math.max(0, num)}rem)`;\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 (filter) result[STYLE_FILTER] = filter;\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 return Array.from(keyframes.entries())\n .filter(([_, frame]) => Object.keys(frame).length > 0)\n .sort((a, b) => a[0] - b[0])\n .map(([offset, frame]) => ({\n offset: offset / 100,\n ...frame,\n ...(inlineBlock && { display: 'inline-block' }),\n }));\n }\n\n const createKeyframes = (config, originalStyle) => {\n if (!originalStyle) return;\n const isSplitText = config[CONFIG_SPLIT] && !config[CONFIG_SPLIT]?.includes('item');\n if (config[CONFIG_LINE]) return parseTimeline(config[CONFIG_LINE], originalStyle, isSplitText);\n\n const animationType =\n config[CONFIG_ANIMATION] ?? extractAnimation(instance.config.defaults.animation, 0);\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 === 4) fromTimeline = `o+${parseFloat(originalStyle[STYLE_OPACITY]) * 100}`;\n\n const defaultDelta = isSplitText ? 50 : 15;\n const intensity = (lastTuning ?? defaultDelta) / 100;\n\n if (animationType === 1 || animationType === 2) {\n // Zoom\n fromTimeline += `s+${1 + (animationType === 1 ? -intensity : intensity)}`;\n firstTuning = null;\n secondTuning = tuning?.length === 2 ? null : secondTuning;\n } else if (animationType === 3) {\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 !== 3 && 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 ? 0.625 : typeof blur === 'number' && !isNaN(blur) ? blur : 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 (totalDuration = 1000, elementDuration = 50) => {\n if (elementDuration > totalDuration) {\n elementDuration = totalDuration;\n }\n\n const maxDelay = totalDuration - elementDuration;\n\n return targetsData.map((item, index) => {\n const normalizedValue = (metrics[index] - min) / range;\n\n const delay = normalizedValue * maxDelay;\n\n return [item.target, delay];\n });\n };\n }\n\n const setupSplit = (element, splitBy, strategy) => {\n const targets = [];\n\n // Split by child elements\n if (splitBy === 'item') {\n Array.from(element.children).forEach((child) => {\n child.__usalOriginals = {\n style: captureComputedStyle(child),\n innerHTML: null,\n };\n targets.push(child);\n });\n return getStaggerFunction(targets, strategy);\n }\n\n // Split by text\n const createSpan = (content) => {\n const span = document.createElement('span');\n span.textContent = content;\n return span;\n };\n\n const 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 processNode = (node, parent) => {\n const processed =\n node.nodeType === Node.TEXT_NODE\n ? processTextContent(node.textContent)\n : node.nodeType === Node.ELEMENT_NODE\n ? (() => {\n const clone = node.cloneNode(false);\n Array.from(node.childNodes).forEach((child) => processNode(child, clone));\n return clone;\n })()\n : null;\n\n if (processed) parent.appendChild(processed);\n };\n\n const fragment = document.createDocumentFragment();\n Array.from(element.childNodes).forEach((node) => processNode(node, fragment));\n\n element.innerHTML = '';\n element.appendChild(fragment);\n\n return getStaggerFunction(targets, strategy);\n };\n\n // ============================================================================\n // Count Animation Setup\n // ============================================================================\n\n const setupCount = (element, config, data) => {\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\n const 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 const fragment = document.createDocumentFragment();\n\n if (before) fragment.appendChild(document.createTextNode(before));\n\n span = document.createElement('span');\n span.textContent = original;\n fragment.appendChild(span);\n\n if (after) fragment.appendChild(document.createTextNode(after));\n\n node.parentNode.replaceChild(fragment, node);\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) return false;\n\n data.countData = { value, decimals, original, span, thousandSep, decimalSep };\n return true;\n };\n\n // ============================================================================\n // Count Animation\n // ============================================================================\n\n const animateCount = (countData, options) => {\n const { duration, easing, delay, iterations } = options;\n const { value, decimals, original, span, thousandSep, decimalSep } = countData;\n\n let startTime = null;\n let currentTime = 0;\n let playState = 'idle';\n let playbackRate = 1;\n let pausedTime = 0;\n let currentIteration = 0;\n\n const 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;\n }\n };\n\n const easingFunction = getEasingFunction(easing);\n\n const 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 const updateValue = (time) => {\n const progress = Math.max(0, Math.min(1, time / duration));\n const easedProgress = easingFunction(progress);\n const currentValue = value * easedProgress;\n\n span.textContent = formatNumber(currentValue);\n\n if (progress >= 1) {\n span.textContent = original;\n\n if (iterations === Infinity || currentIteration < iterations - 1) {\n currentIteration++;\n currentTime = 0;\n pausedTime = 0;\n startTime = performance.now();\n } else {\n playState = 'finished';\n }\n } else if (progress <= 0 && playbackRate < 0) {\n span.textContent = formatNumber(0);\n\n if (iterations === Infinity || currentIteration < iterations - 1) {\n currentIteration++;\n currentTime = duration;\n pausedTime = duration;\n startTime = performance.now();\n } else {\n playState = 'finished';\n }\n }\n };\n\n return {\n tick() {\n if (playState === 'running') {\n const now = performance.now();\n const elapsed = now - startTime;\n\n if (playbackRate > 0) {\n currentTime = pausedTime + elapsed;\n } else {\n currentTime = pausedTime - elapsed;\n }\n\n if (iterations !== Infinity && currentIteration >= iterations - 1) {\n currentTime = Math.max(0, Math.min(duration, currentTime));\n }\n\n updateValue(currentTime);\n\n if (\n iterations !== Infinity &&\n currentIteration >= iterations - 1 &&\n ((playbackRate > 0 && currentTime >= duration) ||\n (playbackRate < 0 && currentTime <= 0))\n ) {\n playState = 'finished';\n }\n }\n },\n\n play() {\n if (playState === 'finished') {\n if (playbackRate > 0) {\n currentTime = 0;\n pausedTime = 0;\n } else {\n currentTime = duration;\n pausedTime = duration;\n }\n currentIteration = 0;\n } else if (playState === 'paused') {\n pausedTime = currentTime;\n } else if (playState === 'idle') {\n pausedTime = playbackRate > 0 ? 0 : duration;\n currentTime = pausedTime;\n }\n\n startTime = performance.now();\n playState = 'running';\n },\n\n pause() {\n if (playState === 'running') {\n pausedTime = currentTime;\n playState = 'paused';\n }\n },\n\n reset() {\n currentTime = 0;\n pausedTime = 0;\n playState = 'idle';\n startTime = null;\n currentIteration = 0;\n updateValue(0);\n },\n\n persist() {},\n\n effect: {\n getTiming() {\n return { duration, delay, easing, iterations };\n },\n },\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 pausedTime = currentTime;\n updateValue(currentTime);\n if (playState === 'finished') {\n playState = 'paused';\n }\n },\n\n get playbackRate() {\n return playbackRate;\n },\n\n set playbackRate(rate) {\n if (playState === 'paused') {\n pausedTime = currentTime;\n }\n playbackRate = rate;\n },\n };\n };\n // ============================================================================\n // Animation Controller\n // ============================================================================\n\n class AnimationController {\n reset() {\n this.rafId = null;\n this.animations = new Map();\n }\n\n constructor(data) {\n this.data = data;\n this.reset();\n }\n\n add(element, config, delay, originalStyle) {\n const duration = this.data.config[CONFIG_DURATION] ?? instance.config.defaults.duration;\n const easing = this.data.config[CONFIG_EASING] ?? instance.config.defaults.easing;\n const forwards = this.data.config[CONFIG_FORWARDS] ?? instance.config.defaults.forwards;\n const loop =\n this.data.config[CONFIG_LOOP] === true\n ? (instance.config.defaults.loop ?? 'mirror')\n : this.data.config[CONFIG_LOOP];\n\n // Standard animation\n let options = {\n duration,\n delay,\n easing,\n fill: 'forwards',\n };\n\n if (loop === 'jump') options.iterations = Infinity;\n\n let keyframes = [];\n if (this.data.config[CONFIG_TEXT]) {\n options = {\n duration: duration,\n iterations: Infinity,\n delay: delay,\n easing: 'linear',\n iterationStart: 0.5,\n };\n keyframes =\n this.data.config[CONFIG_TEXT] === 'shimmer' ? SHIMMER_KEYFRAMES : WEIGHT_KEYFRAMES;\n } else {\n keyframes = createKeyframes(config, originalStyle);\n }\n\n if (forwards) originalStyle = keyframes[keyframes.length - 1];\n\n const animation = this.data.countData\n ? animateCount(this.data.countData, options)\n : element.animate(keyframes, {\n ...options,\n id: genTmpId(),\n });\n\n animation.persist();\n animation.pause();\n\n this.animations.set(animation, {\n animation,\n element,\n delay,\n originalStyle,\n loop,\n playbackRate: -1,\n waiting: true,\n });\n }\n\n letsGo() {\n const { element, config, targets, splitConfig } = this.data;\n const delay = config[CONFIG_DELAY] ?? instance.config.defaults.delay;\n const duration = config[CONFIG_DURATION] ?? instance.config.defaults.duration;\n const originalStyle = element.__usalOriginals?.style;\n const splitDelay = splitConfig[CONFIG_DELAY] ?? instance.config.defaults.splitDelay;\n\n let notReadYet = targets?.()?.length || (originalStyle ? 0 : 1);\n\n if (targets) {\n targets(duration, splitDelay).forEach(([target, delay]) => {\n const targetOriginalStyle = target.__usalOriginals?.style || originalStyle;\n if (!targetOriginalStyle) return;\n notReadYet--;\n this.add(target, this.data.splitConfig, parseInt(delay), targetOriginalStyle);\n });\n } else if (originalStyle) {\n this.add(element, config, delay, originalStyle);\n }\n\n if (notReadYet === 0 && !this.rafId) {\n this.tick();\n }\n }\n\n timeToSayGoodbye() {\n if (this.animations.size !== 0) return false;\n this.reset();\n cancelAnimationFrame(this.rafId);\n this.data.resolve();\n return true;\n }\n\n tick() {\n const { toCleanup, toAnimate } = this.prepare();\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 prepare() {\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?.();\n\n if (info.loop !== 'mirror') {\n if (animation.playState === 'finished') {\n toCleanup.push([animation, info]);\n continue;\n }\n } else {\n const duration = animation.effect.getTiming().duration;\n if (typeof duration === 'number' && duration > 0) {\n const progress = animation.currentTime / duration;\n\n if (\n !isNaN(progress) &&\n isFinite(progress) &&\n !info.waiting &&\n ((progress >= 0.95 && info.playbackRate > 0) ||\n (progress <= 0.05 && info.playbackRate < 0))\n ) {\n animation.pause();\n info.waiting = true;\n }\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 currentDirection = toAnimate[0].playbackRate;\n if (currentDirection > 0) {\n toAnimate.sort((a, b) => b.delay - a.delay);\n } else {\n toAnimate.sort((a, b) => a.delay - b.delay);\n }\n\n toAnimate.forEach((next) => {\n next.waiting = false;\n next.playbackRate = -currentDirection;\n next.animation.playbackRate = next.playbackRate;\n next.animation.play();\n });\n }\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) clean();\n else\n cancelAllAnimations(this.data, info.element, info.originalStyle).then(() => {\n clean();\n });\n });\n }\n }\n\n // ============================================================================\n // Main Animation Controller\n // ============================================================================\n\n const animate = (data) => {\n if (data.stop) return;\n data.hasAnimated = true;\n\n data.animating = new Promise((resolve) => {\n data.resolve = resolve;\n data.controller.letsGo();\n }).then(() => {\n data.onfinish();\n data.animating = null;\n data.stop = true;\n });\n };\n\n const animateIfVisible = (data, ratio = null) => {\n if (\n data.config[CONFIG_LOOP] ||\n data.animating !== 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 animate(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 const splitByItem = data.config[CONFIG_SPLIT]?.includes('item');\n\n if (data.targets) {\n data.targets().forEach(([target]) => {\n if (target.__usalOriginals?.style) {\n applyStyles(target, target.__usalOriginals.style, true);\n }\n });\n }\n\n const innerHTML = data.element.__usalOriginals?.innerHTML;\n if (innerHTML && !splitByItem && (data.config[CONFIG_SPLIT] || data.countData)) {\n data.element.innerHTML = innerHTML;\n }\n\n if (data.element.__usalOriginals?.style) {\n applyStyles(data.element, data.element.__usalOriginals.style, true);\n }\n\n requestAnimationFrame(() => resolve());\n };\n\n if (data.animating === null) data.onfinish();\n else data.stop = true;\n });\n\n const processElement = (element, elementObserver) => {\n if (!element.__usalID) {\n element.__usalOriginals = {\n style: captureComputedStyle(element),\n innerHTML: element.innerHTML,\n };\n element.__usalID = element.getAttribute(DATA_USAL_ID) ?? genTmpId();\n }\n\n const classes = element.getAttribute(DATA_USAL_ATTRIBUTE) || '';\n\n const existing = instance.elements.get(element.__usalID);\n if (existing) {\n if (classes !== existing.configString) {\n instance.elements.delete(element.__usalID);\n elementObserver.unobserve(element);\n cleanupElement(existing).then(() => {\n processElement(element, elementObserver);\n });\n }\n return;\n }\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 animating: null,\n countData: null,\n onfinish: () => {},\n controller: null,\n resolve: () => {},\n };\n\n data.controller = new AnimationController(data);\n\n // Setup special animations\n if (config[CONFIG_COUNT]) {\n setupCount(element, config, data);\n }\n\n const splitBy = config[CONFIG_SPLIT]?.split(' ').find((item) =>\n ['word', 'letter', 'item'].includes(item)\n );\n if (splitBy) {\n data.targets = setupSplit(element, splitBy, config[CONFIG_STAGGER]);\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 }\n\n // Reset Style initially\n resetStyle(data);\n\n instance.elements.set(element.__usalID, data);\n\n requestAnimationFrame(async () => {\n if (config[CONFIG_LOOP]) {\n animate(data);\n } else {\n animateIfVisible(data);\n elementObserver.observe(element);\n }\n });\n };\n\n // ============================================================================\n // Observers Setup\n // ============================================================================\n\n const 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 animateIfVisible(data, entry.intersectionRatio);\n }\n }\n },\n { threshold: INTERSECTION_THRESHOLDS }\n );\n\n const 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 const 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 const scanAllDOMs = () => {\n // Clean disconnected elements\n instance.elements.forEach((data, id) => {\n if (!data.element.isConnected) {\n elementObserver.unobserve(data.element);\n cleanupElement(data).then(() => {\n instance.elements.delete(id);\n });\n } else {\n animateIfVisible(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 const 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 const 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 Object.assign(instance.config, newConfig);\n return publicAPI;\n },\n\n async destroy() {\n if (!instance.initialized) return Promise.resolve();\n if (instance.destroying != null) return instance.destroying;\n\n instance.observers();\n const elements = Array.from(instance.elements.values());\n\n instance.destroying = Promise.all(elements.map((data) => cleanupElement(data))).then(() => {\n instance.elements.clear();\n instance.observers = () => {};\n instance.initialized = false;\n instance.destroying = null;\n });\n\n return instance.destroying;\n },\n\n async restart() {\n if (instance.restarting != null) return instance.restarting;\n if (instance.destroying != null) return instance.destroying.then(() => publicAPI.restart());\n\n instance.restarting = publicAPI\n .destroy()\n .then(\n () =>\n new Promise((resolve) => {\n requestAnimationFrame(() => {\n if (document.readyState === 'loading') {\n document.addEventListener(\n 'DOMContentLoaded',\n () => {\n autoInit();\n resolve(publicAPI);\n },\n { once: true }\n );\n } else {\n autoInit();\n resolve(publicAPI);\n }\n });\n })\n )\n .finally(() => {\n instance.restarting = null;\n });\n\n return instance.restarting;\n },\n\n initialized: () => instance.initialized,\n version: '{%%VERSION%%}',\n };\n\n // Initialize on DOM ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', autoInit, { once: true });\n } else {\n requestAnimationFrame(autoInit);\n }\n\n return publicAPI;\n})();\n\n// Export for modules\nif (typeof window !== 'undefined') {\n window.USAL = USAL;\n}\n\nexport default USAL;\n"],
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,IAAM,QAAQ,MAAM;AAFpB;AAIE,MAAI,OAAO,WAAW,iBAAe,YAAO,SAAP,mBAAa,gBAAe;AAC/D,WAAO,OAAO;AAAA,EAChB;AAGA,MAAI,OAAO,WAAW,aAAa;AACjC,UAAM,YAAY,WAAkB;AAAA;AAClC,eAAO;AAAA,MACT;AAAA;AACA,WAAO;AAAA,MACL,QAAQ,WAAY;AAClB,eAAO,UAAU,WAAW,IAAI,CAAC,IAAI;AAAA,MACvC;AAAA,MACA,SAAS;AAAA,MACT,SAAS;AAAA,MACT,aAAa,MAAM;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,EACF;AAMA,QAAM,gBAAgB;AAAA,IACpB,UAAU;AAAA,MACR,WAAW;AAAA,MACX,WAAW;AAAA,MACX,UAAU;AAAA,MACV,OAAO;AAAA,MACP,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AAAA,IACA,gBAAgB;AAAA,IAChB,MAAM;AAAA,EACR;AAEA,QAAM,WAAW;AAAA,IACf,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,WAAW,MAAM;AAAA,IAAC;AAAA,IAClB,UAAU,oBAAI,IAAI;AAAA,IAClB,QAAQ,mBAAK;AAAA,EACf;AAMA,QAAM,0BACJ;AAEF,QAAM,sBAAsB;AAC5B,QAAM,eAAe,GAAG,mBAAmB;AAC3C,QAAM,qBAAqB,IAAI,mBAAmB;AAElD,QAAM,mBAAmB;AACzB,QAAM,mBAAmB;AACzB,QAAM,kBAAkB;AACxB,QAAM,eAAe;AACrB,QAAM,mBAAmB;AACzB,QAAM,gBAAgB;AACtB,QAAM,cAAc;AACpB,QAAM,cAAc;AACpB,QAAM,eAAe;AACrB,QAAM,eAAe;AACrB,QAAM,cAAc;AACpB,QAAM,cAAc;AACpB,QAA