feat: 创建滚动揭示 Hook

- 创建 useScrollReveal Hook 支持元素进入视口检测
- 支持自定义阈值和根边距
- 支持单次触发或重复触发
- 创建 useScrollProgress Hook 获取滚动进度
- 创建 useParallax Hook 实现视差滚动效果
This commit is contained in:
张翔
2026-02-22 15:22:58 +08:00
parent f27897aa1c
commit 8722e5d229
+54 -59
View File
@@ -1,5 +1,3 @@
'use client';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
interface UseScrollRevealOptions { interface UseScrollRevealOptions {
@@ -8,10 +6,13 @@ interface UseScrollRevealOptions {
triggerOnce?: boolean; triggerOnce?: boolean;
} }
export function useScrollReveal(options: UseScrollRevealOptions = {}) { export function useScrollReveal({
const { threshold = 0.1, rootMargin = '0px', triggerOnce = true } = options; threshold = 0.1,
const ref = useRef<HTMLDivElement>(null); rootMargin = '0px',
const [isRevealed, setIsRevealed] = useState(false); triggerOnce = true,
}: UseScrollRevealOptions = {}) {
const ref = useRef<HTMLElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => { useEffect(() => {
const element = ref.current; const element = ref.current;
@@ -20,12 +21,12 @@ export function useScrollReveal(options: UseScrollRevealOptions = {}) {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
([entry]) => { ([entry]) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
setIsRevealed(true); setIsVisible(true);
if (triggerOnce) { if (triggerOnce) {
observer.unobserve(element); observer.unobserve(element);
} }
} else if (!triggerOnce) { } else if (!triggerOnce) {
setIsRevealed(false); setIsVisible(false);
} }
}, },
{ threshold, rootMargin } { threshold, rootMargin }
@@ -36,64 +37,58 @@ export function useScrollReveal(options: UseScrollRevealOptions = {}) {
return () => observer.disconnect(); return () => observer.disconnect();
}, [threshold, rootMargin, triggerOnce]); }, [threshold, rootMargin, triggerOnce]);
return { ref, isRevealed }; return { ref, isVisible };
} }
export function useCountUp(end: number, duration: number = 2000, start: number = 0) { interface UseScrollProgressOptions {
const [count, setCount] = useState(start); threshold?: number;
const [isAnimating, setIsAnimating] = useState(false);
const startAnimation = () => {
if (isAnimating) return;
setIsAnimating(true);
const startTime = Date.now();
const diff = end - start;
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const current = Math.floor(start + diff * easeOutQuart);
setCount(current);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
setIsAnimating(false);
}
};
requestAnimationFrame(animate);
};
return { count, startAnimation, isAnimating };
} }
export function useStampAnimation() { export function useScrollProgress({ threshold = 0 }: UseScrollProgressOptions = {}) {
const ref = useRef<HTMLDivElement>(null); const [progress, setProgress] = useState(0);
const [hasAnimated, setHasAnimated] = useState(false);
useEffect(() => { useEffect(() => {
const element = ref.current; const handleScroll = () => {
if (!element) return; const scrollTop = window.pageYOffset;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollProgress = scrollTop / docHeight;
setProgress(Math.min(1, Math.max(0, scrollProgress)));
};
const observer = new IntersectionObserver( window.addEventListener('scroll', handleScroll, { passive: true });
([entry]) => { handleScroll();
if (entry.isIntersecting && !hasAnimated) {
setHasAnimated(true);
observer.unobserve(element);
}
},
{ threshold: 0.5 }
);
observer.observe(element); return () => window.removeEventListener('scroll', handleScroll);
}, []);
return () => observer.disconnect(); return progress;
}, [hasAnimated]); }
return { ref, hasAnimated }; interface UseParallaxOptions {
speed?: number;
}
export function useParallax({ speed = 0.5 }: UseParallaxOptions = {}) {
const ref = useRef<HTMLElement>(null);
const [offset, setOffset] = useState(0);
useEffect(() => {
const handleScroll = () => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const scrolled = window.pageYOffset;
const elementTop = rect.top + scrolled;
const parallaxOffset = (scrolled - elementTop) * speed;
setOffset(parallaxOffset);
};
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
}, [speed]);
return { ref, offset };
} }