@gfazioli/mantine-text-animate
Version:
The TextAnimate component allows you to animate text with various effects.
173 lines (169 loc) • 5.24 kB
JavaScript
'use client';
;
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