125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, RefObject } from 'react';
|
|
|
|
/**
|
|
* Calculate scroll progress (0-1) based on element's position in viewport
|
|
* @param ref - Reference to the element to track
|
|
* @param offset - Offset from viewport edges (0-1, default 0.1)
|
|
* @returns progress value between 0 and 1
|
|
*/
|
|
export function useScrollProgress(ref: RefObject<HTMLElement>, offset: number = 0.1): number {
|
|
const [progress, setProgress] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (!ref.current) return;
|
|
|
|
let rafId: number;
|
|
|
|
const calculateProgress = () => {
|
|
if (!ref.current) return;
|
|
|
|
const rect = ref.current.getBoundingClientRect();
|
|
const viewportHeight = window.innerHeight;
|
|
const scrollY = window.scrollY;
|
|
const documentHeight = document.documentElement.scrollHeight;
|
|
|
|
// Element enters viewport from bottom
|
|
const enterPoint = viewportHeight * (1 - offset);
|
|
// Element reaches top of viewport
|
|
const exitPoint = viewportHeight * offset;
|
|
|
|
// Calculate progress: 0 when entering, 1 at 30% viewport (accelerated)
|
|
const elementCenter = rect.top + rect.height / 2;
|
|
const totalDistance = enterPoint - exitPoint;
|
|
const currentDistance = enterPoint - elementCenter;
|
|
|
|
// Accelerate progress to reach 1.0 at 30% viewport height
|
|
// Scale factor: 1.67 makes progress reach 1.0 at ~30% instead of 50%
|
|
const rawProgress = (currentDistance / totalDistance) * 1.67;
|
|
let clampedProgress = Math.max(0, Math.min(1, rawProgress));
|
|
|
|
// At bottom of page - ensure elements near bottom can reach 100%
|
|
// Only apply if we're at the very bottom AND this element is below the fold
|
|
if (scrollY + viewportHeight >= documentHeight - 50 && rect.top < viewportHeight) {
|
|
clampedProgress = Math.max(clampedProgress, 1);
|
|
}
|
|
|
|
setProgress(clampedProgress);
|
|
};
|
|
|
|
const handleScroll = () => {
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId);
|
|
}
|
|
rafId = requestAnimationFrame(calculateProgress);
|
|
};
|
|
|
|
// Initial calculation
|
|
calculateProgress();
|
|
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
window.addEventListener('resize', handleScroll, { passive: true });
|
|
|
|
return () => {
|
|
window.removeEventListener('scroll', handleScroll);
|
|
window.removeEventListener('resize', handleScroll);
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId);
|
|
}
|
|
};
|
|
}, [ref, offset]);
|
|
|
|
return progress;
|
|
}
|
|
|
|
/**
|
|
* Calculate parallax offset based on scroll position
|
|
* @param ref - Reference to the element to track
|
|
* @param speed - Parallax speed multiplier (default 0.5)
|
|
* @returns offset in pixels
|
|
*/
|
|
export function useParallax(ref: RefObject<HTMLElement>, speed: number = 0.5): number {
|
|
const [offset, setOffset] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (!ref.current) return;
|
|
|
|
let rafId: number;
|
|
|
|
const calculateOffset = () => {
|
|
if (!ref.current) return;
|
|
|
|
const rect = ref.current.getBoundingClientRect();
|
|
const viewportHeight = window.innerHeight;
|
|
|
|
// Calculate offset based on element position relative to viewport
|
|
const scrolled = viewportHeight - rect.top;
|
|
const parallaxOffset = scrolled * speed;
|
|
|
|
setOffset(parallaxOffset);
|
|
};
|
|
|
|
const handleScroll = () => {
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId);
|
|
}
|
|
rafId = requestAnimationFrame(calculateOffset);
|
|
};
|
|
|
|
calculateOffset();
|
|
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
window.addEventListener('resize', handleScroll, { passive: true });
|
|
|
|
return () => {
|
|
window.removeEventListener('scroll', handleScroll);
|
|
window.removeEventListener('resize', handleScroll);
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId);
|
|
}
|
|
};
|
|
}, [ref, speed]);
|
|
|
|
return offset;
|
|
} |