Files
assetx/landingpage/hooks/useCountUp.ts

84 lines
2.3 KiB
TypeScript
Raw Permalink Normal View History

'use client';
import { useState, useEffect, useRef } from 'react';
/**
* Hook -
* 使 requestAnimationFrame
*/
export function useCountUp(end: number, duration: number = 1500, startRangePercent: number = 0.75) {
const [mounted, setMounted] = useState(false);
const [count, setCount] = useState(end); // 初始值设为目标值,避免 hydration 错误
const elementRef = useRef<HTMLDivElement>(null);
const startValueRef = useRef<number>(end);
const rafRef = useRef<number | null>(null);
// 客户端挂载后设置随机起始值
useEffect(() => {
setMounted(true);
startValueRef.current = Math.floor(end * (startRangePercent + Math.random() * 0.15));
setCount(startValueRef.current);
}, [end, startRangePercent]);
useEffect(() => {
if (!mounted) return;
const cancelAnimation = () => {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
const startAnimation = () => {
cancelAnimation();
const start = startValueRef.current;
const startTime = performance.now();
const tick = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// easeOutCubic 缓动
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
const currentCount = Math.floor(start + (end - start) * easeOutCubic);
setCount(currentCount);
if (progress < 1) {
rafRef.current = requestAnimationFrame(tick);
} else {
setCount(end); // 确保最终值准确
rafRef.current = null;
}
};
rafRef.current = requestAnimationFrame(tick);
};
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
startAnimation();
} else {
cancelAnimation();
setCount(startValueRef.current);
}
},
{ threshold: 0.1 }
);
if (elementRef.current) {
observer.observe(elementRef.current);
}
return () => {
observer.disconnect();
cancelAnimation();
};
}, [end, duration, mounted]);
return { count, elementRef };
}