UNPKG

@gfazioli/mantine-text-animate

Version:

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

173 lines (169 loc) 5.24 kB
'use client'; 'use strict'; var React = require('react'); const easingFunctions = { // No easing, no acceleration linear: (t) => t, // Accelerating from zero velocity "ease-in": (t) => t * t, // Decelerating to zero velocity "ease-out": (t) => 1 - (1 - t) ** 2, // Acceleration until halfway, then deceleration "ease-in-out": (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2 }; function useNumberTicker({ value, startValue = 0, delay = 0, decimalPlaces = 0, speed = 1, easing = "ease-out", animate = true, onCompleted, formatValue }) { const [displayValue, setDisplayValue] = React.useState(startValue); const [isAnimating, setIsAnimating] = React.useState(false); const animationRef = React.useRef(null); const delayTimerRef = React.useRef(null); const startTimeRef = React.useRef(0); const animationCompletedRef = React.useRef(false); const animateRef = React.useRef(animate); const isFirstRenderRef = React.useRef(true); const prevValueRef = React.useRef(value); const prevStartValueRef = React.useRef(startValue); const formatNumber = (num) => { if (formatValue) { return formatValue(num); } return Intl.NumberFormat("en-US", { minimumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces }).format(num); }; const animateFrame = (timestamp, from, to) => { if (!startTimeRef.current) { startTimeRef.current = timestamp; } 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); const valueRange = to - from; const currentValue = from + valueRange * easedProgress; setDisplayValue(currentValue); if (progress < 1) { animationRef.current = requestAnimationFrame((time) => animateFrame(time, from, to)); } else { setDisplayValue(to); setIsAnimating(false); startTimeRef.current = 0; animationCompletedRef.current = true; if (onCompleted) { onCompleted(); } } }; const cleanupAnimation = () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); animationRef.current = null; } if (delayTimerRef.current) { clearTimeout(delayTimerRef.current); delayTimerRef.current = null; } }; const startAnimationFrom = (fromValue, toValue) => { const epsilon = 1e-10; if (isAnimating && Math.abs(toValue - value) < epsilon && Math.abs(fromValue - displayValue) < epsilon) { return; } cleanupAnimation(); startTimeRef.current = 0; animationCompletedRef.current = false; setIsAnimating(true); delayTimerRef.current = setTimeout(() => { animationRef.current = requestAnimationFrame( (time) => animateFrame(time, fromValue, toValue) ); }, delay * 1e3); }; const start = () => { animationCompletedRef.current = false; startAnimationFrom(startValue, value); }; const stop = () => { cleanupAnimation(); setIsAnimating(false); }; const reset = () => { stop(); setDisplayValue(startValue); 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) { setDisplayValue(value); animationCompletedRef.current = true; onCompleted?.(); return; } setDisplayValue(startValue); if (animate) { startAnimationFrom(startValue, value); } } }, []); React.useEffect(() => { if (animate !== animateRef.current) { animateRef.current = animate; if (animate) { animationCompletedRef.current = false; cleanupAnimation(); setDisplayValue(startValue); startAnimationFrom(startValue, value); } else if (isAnimating) { stop(); } } }, [animate, value, startValue, isAnimating]); React.useEffect(() => { const valueChanged = value !== prevValueRef.current; const startValueChanged = startValue !== prevStartValueRef.current; prevValueRef.current = value; prevStartValueRef.current = startValue; if (valueChanged || startValueChanged) { if (isAnimating) { startAnimationFrom(displayValue, value); } else if (animationCompletedRef.current && valueChanged) { if (animate) { startAnimationFrom(displayValue, value); } } else if (animate) { startAnimationFrom(startValue, value); } else { setDisplayValue(startValue); } } }, [value, startValue, animate, isAnimating, displayValue]); React.useEffect(() => { return () => { cleanupAnimation(); }; }, []); return { text: formatNumber(displayValue), displayValue, start, stop, reset, isAnimating }; } exports.useNumberTicker = useNumberTicker; //# sourceMappingURL=use-number-ticker.cjs.map