From c1cc1fdc1ca9ac1008400f004ab3e652e1371c2c Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 2 Sep 2026 12:47:58 +0200 Subject: [PATCH] fix(home): counter counts exactly once, no restart on value refresh AnimatedCounter restarted from 0 on every online-count cache refresh (~10s) because the effect depended on value. Rewrote it as a single requestAnimationFrame animation with ease-out; changes to value after the animation only update the display statically. --- src/components/animated-counter.tsx | 34 +++++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/components/animated-counter.tsx b/src/components/animated-counter.tsx index 62977c82d3..e7972585d8 100644 --- a/src/components/animated-counter.tsx +++ b/src/components/animated-counter.tsx @@ -12,25 +12,35 @@ export function AnimatedCounter({ }) { const ref = useRef(null); const isInView = useInView(ref, { once: true, margin: "-50px" }); + const started = useRef(false); const [display, setDisplay] = useState(0); useEffect(() => { if (!isInView) return; + + if (started.current) { + setDisplay(value); + return; + } + started.current = true; + const duration = 1200; - const steps = 30; - const increment = value / steps; - let current = 0; - let step = 0; - const id = setInterval(() => { - step++; - current = Math.min(Math.round(increment * step), value); - setDisplay(current); - if (step >= steps) { - clearInterval(id); + const start = performance.now(); + let raf = 0; + + const tick = (now: number) => { + const progress = Math.min((now - start) / duration, 1); + const eased = 1 - (1 - progress) ** 3; + setDisplay(Math.round(eased * value)); + if (progress < 1) { + raf = requestAnimationFrame(tick); + } else { setDisplay(value); } - }, duration / steps); - return () => clearInterval(id); + }; + + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); }, [isInView, value]); return (