@gfazioli/mantine-text-animate
Version:
The TextAnimate component allows you to animate text with various effects.
157 lines (154 loc) • 4.71 kB
JavaScript
'use client';
import { useState, useRef, useEffect } from '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 - Math.pow(1 - t, 2),
// Acceleration until halfway, then deceleration
"ease-in-out": (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
};
function useNumberTicker({
value,
startValue = 0,
delay = 0,
decimalPlaces = 0,
speed = 1,
easing = "ease-out",
animate = true,
onCompleted
}) {
const [displayValue, setDisplayValue] = useState(startValue);
const [isAnimating, setIsAnimating] = useState(false);
const animationRef = useRef(null);
const delayTimerRef = useRef(null);
const startTimeRef = useRef(0);
const animationCompletedRef = useRef(false);
const animateRef = useRef(animate);
const isFirstRenderRef = useRef(true);
const prevValueRef = useRef(value);
const prevStartValueRef = useRef(startValue);
const formatNumber = (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) => {
if (isAnimating && toValue === value && fromValue === displayValue) 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;
};
useEffect(() => {
if (isFirstRenderRef.current) {
setDisplayValue(startValue);
isFirstRenderRef.current = false;
if (animate) {
startAnimationFrom(startValue, value);
}
}
}, []);
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]);
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]);
useEffect(() => {
return () => {
cleanupAnimation();
};
}, []);
return {
text: formatNumber(displayValue),
displayValue,
start,
stop,
reset,
isAnimating
};
}
export { useNumberTicker };
//# sourceMappingURL=use-number-ticker.mjs.map