UNPKG

@gfazioli/mantine-text-animate

Version:

The TextAnimate component allows you to animate text with various effects.

329 lines (325 loc) 10.7 kB
'use client'; 'use strict'; var React = require('react'); const characterSets = { alphanumeric: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", alphabetic: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", numeric: "0123456789", symbols: "!@#$%^&*()_+-=[]{}|;:,.<>?/" }; const easingFunctions = { linear: (t) => t, "ease-in": (t) => t * t, "ease-out": (t) => 1 - (1 - t) ** 2, "ease-in-out": (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2 }; const shuffleArray = (array) => { const result = [...array]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; }; function useTextTicker({ value, initialText = "random", animate = true, characterSet = "alphanumeric", customCharacters = "", delay = 0, speed = 1, easing = "ease-out", randomChangeSpeed = 1, revealDirection = "left-to-right", scrambleDuration, staggerDelay = 50, onCompleted }) { const [displayText, setDisplayText] = React.useState(""); const [isAnimating, setIsAnimating] = React.useState(false); const animationFrameRef = React.useRef(null); const delayTimeoutRef = React.useRef(null); const startTimeRef = React.useRef(0); const charStabilityRef = React.useRef([]); const randomOrderRef = React.useRef([]); const isFirstRenderRef = React.useRef(true); const manualStartRef = React.useRef(false); const animatePropRef = React.useRef(animate); const animationCompletedRef = React.useRef(false); const displayTextRef = React.useRef([]); const charStartTimesRef = React.useRef([]); const getCharacterPool = () => { if (characterSet === "custom" && customCharacters && customCharacters.length > 0) { return customCharacters; } return characterSets[characterSet === "custom" ? "alphanumeric" : characterSet]; }; const generateRandomText = () => { const pool = getCharacterPool(); let result = ""; for (let i = 0; i < value.length; i++) { result += pool[Math.floor(Math.random() * pool.length)]; } return result; }; const generateInitialText = () => { if (initialText === "none") { return ""; } if (initialText === "target") { return value; } return generateRandomText(); }; const getCharIndexOrder = () => { const indices = Array.from({ length: value.length }, (_, i) => i); switch (revealDirection) { case "right-to-left": return indices.reverse(); case "center-out": { const result = []; const mid = Math.floor(value.length / 2); result.push(mid); for (let i = 1; i <= mid; i++) { if (mid - i >= 0) { result.push(mid - i); } if (mid + i < value.length) { result.push(mid + i); } } return result; } case "random": { if (randomOrderRef.current.length === value.length) { return randomOrderRef.current; } const randomOrder = shuffleArray(indices); randomOrderRef.current = randomOrder; return randomOrder; } case "left-to-right": default: return indices; } }; const getOrderMap = () => { const order = getCharIndexOrder(); const map = /* @__PURE__ */ new Map(); order.forEach((charIndex, orderPosition) => { map.set(charIndex, orderPosition); }); return map; }; const animateFrameScramble = (timestamp) => { if (!startTimeRef.current) { startTimeRef.current = timestamp; charStabilityRef.current = Array(value.length).fill(false); const orderMap = getOrderMap(); charStartTimesRef.current = Array.from( { length: value.length }, (_, i) => timestamp + (orderMap.get(i) || 0) * staggerDelay ); } const pool = getCharacterPool(); const newTextArray = []; let allSettled = true; for (let i = 0; i < value.length; i++) { if (charStabilityRef.current[i]) { newTextArray.push(value[i]); continue; } if (timestamp < charStartTimesRef.current[i]) { newTextArray.push(value[i] === " " ? " " : pool[Math.floor(Math.random() * pool.length)]); allSettled = false; continue; } const charElapsed = timestamp - charStartTimesRef.current[i]; if (charElapsed >= scrambleDuration) { charStabilityRef.current[i] = true; newTextArray.push(value[i]); continue; } if (value[i] === " ") { newTextArray.push(" "); } else { newTextArray.push(pool[Math.floor(Math.random() * pool.length)]); } allSettled = false; } displayTextRef.current = newTextArray; setDisplayText(newTextArray.join("")); if (allSettled) { displayTextRef.current = [...value]; setDisplayText(value); setIsAnimating(false); startTimeRef.current = 0; animationCompletedRef.current = true; onCompleted?.(); return; } animationFrameRef.current = requestAnimationFrame(animateFrameScramble); }; const animateFrameDefault = (timestamp) => { if (!startTimeRef.current) { startTimeRef.current = timestamp; charStabilityRef.current = Array(value.length).fill(false); } const elapsed = timestamp - startTimeRef.current; const duration = 1e3 / speed; const progress = Math.min(elapsed / duration, 1); const easingFunction = easingFunctions[easing] || easingFunctions["ease-out"]; const easedProgress = easingFunction(progress); let allStable = true; const pool = getCharacterPool(); const changeThreshold = 1 / randomChangeSpeed; const charOrder = getCharIndexOrder(); const newTextArray = Array(value.length).fill(""); for (let orderIndex = 0; orderIndex < charOrder.length; orderIndex++) { const i = charOrder[orderIndex]; const positionFactor = orderIndex / charOrder.length; const charStability = Math.min(1, easedProgress * 3 - positionFactor); if (charStabilityRef.current[i] || Math.random() < charStability) { charStabilityRef.current[i] = true; newTextArray[i] = value[i]; } else { if (Math.random() < changeThreshold) { newTextArray[i] = pool[Math.floor(Math.random() * pool.length)]; } else { const currentChar = displayTextRef.current[i] || pool[Math.floor(Math.random() * pool.length)]; newTextArray[i] = currentChar; } allStable = false; } } displayTextRef.current = newTextArray; setDisplayText(newTextArray.join("")); if (progress >= 1 || allStable) { displayTextRef.current = [...value]; setDisplayText(value); setIsAnimating(false); startTimeRef.current = 0; animationCompletedRef.current = true; if (onCompleted) { onCompleted(); } return; } animationFrameRef.current = requestAnimationFrame(animateFrameDefault); }; const animateFrame = scrambleDuration != null ? animateFrameScramble : animateFrameDefault; const start = () => { animationCompletedRef.current = false; if (!animatePropRef.current) { manualStartRef.current = true; } else { manualStartRef.current = false; } if (isAnimating) { return; } if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } if (delayTimeoutRef.current) { clearTimeout(delayTimeoutRef.current); delayTimeoutRef.current = null; } startTimeRef.current = 0; charStabilityRef.current = Array(value.length).fill(false); setIsAnimating(true); delayTimeoutRef.current = setTimeout(() => { animationFrameRef.current = requestAnimationFrame(animateFrame); }, delay * 1e3); }; const stop = () => { manualStartRef.current = false; if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } if (delayTimeoutRef.current) { clearTimeout(delayTimeoutRef.current); delayTimeoutRef.current = null; } setIsAnimating(false); }; const reset = () => { stop(); const initial = generateInitialText(); displayTextRef.current = [...initial]; setDisplayText(initial); startTimeRef.current = 0; charStabilityRef.current = Array(value.length).fill(false); animationCompletedRef.current = false; }; React.useEffect(() => { if (isFirstRenderRef.current) { isFirstRenderRef.current = false; const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (prefersReducedMotion && animate) { displayTextRef.current = [...value]; setDisplayText(value); animationCompletedRef.current = true; onCompleted?.(); return; } const initial = generateInitialText(); displayTextRef.current = [...initial]; setDisplayText(initial); } }, []); React.useEffect(() => { if (animate !== animatePropRef.current) { animatePropRef.current = animate; if (animate && !isAnimating) { animationCompletedRef.current = false; start(); } else if (!animate && isAnimating) { stop(); } } else if (animate && !isAnimating && !animationCompletedRef.current) { start(); } }, [isAnimating, animate]); React.useEffect(() => { if (randomOrderRef.current.length !== value.length) { randomOrderRef.current = []; } if (isAnimating) { stop(); startTimeRef.current = 0; charStabilityRef.current = Array(value.length).fill(false); animationCompletedRef.current = false; const initial = generateInitialText(); displayTextRef.current = [...initial]; setDisplayText(initial); delayTimeoutRef.current = setTimeout(() => start(), 0); } else { const initial = generateInitialText(); displayTextRef.current = [...initial]; setDisplayText(initial); } }, [value, initialText, characterSet, customCharacters]); React.useEffect(() => { return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } if (delayTimeoutRef.current) { clearTimeout(delayTimeoutRef.current); } }; }, []); return { text: displayText, start, stop, reset, isAnimating }; } exports.useTextTicker = useTextTicker; //# sourceMappingURL=use-text-ticker.cjs.map