This commit is contained in:
NotII
2025-03-28 22:56:57 +00:00
parent 546e7bd245
commit 1bec653206
5 changed files with 274 additions and 24 deletions

View File

@@ -0,0 +1,71 @@
"use client";
import { useState, useEffect, useRef } from "react";
export interface AnimatedCounterProps {
value: number;
duration?: number;
prefix?: string;
suffix?: string;
formatter?: (value: number) => string;
}
export function AnimatedCounter({
value,
duration = 2000,
prefix = "",
suffix = "",
formatter
}: AnimatedCounterProps) {
const [displayValue, setDisplayValue] = useState(0);
const startTimeRef = useRef<number | null>(null);
const frameRef = useRef<number | null>(null);
// Easing function for smoother animation
const easeOutQuad = (t: number): number => t * (2 - t);
useEffect(() => {
// Reset start time on new value
startTimeRef.current = null;
// Cancel any ongoing animation
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
// Animation function
const animate = (timestamp: number) => {
if (!startTimeRef.current) {
startTimeRef.current = timestamp;
}
const elapsed = timestamp - startTimeRef.current;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easeOutQuad(progress);
const nextValue = Math.floor(easedProgress * value);
setDisplayValue(nextValue);
if (progress < 1) {
frameRef.current = requestAnimationFrame(animate);
} else {
setDisplayValue(value);
}
};
// Start the animation
frameRef.current = requestAnimationFrame(animate);
return () => {
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, [value, duration]);
const formattedValue = formatter
? formatter(displayValue)
: `${prefix}${displayValue.toLocaleString()}${suffix}`;
return <span>{formattedValue}</span>;
}