Spaces:
Running
Running
File size: 4,560 Bytes
86fbe37 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
'use client';
import { useRef, useEffect, useCallback } from 'react';
import './ScrollStack.css';
export const ScrollStackItem = ({ children, itemClassName = '' }) => (
<div className={`scroll-stack-card ${itemClassName}`.trim()}>{children}</div>
);
const ScrollStack = ({
children,
className = '',
itemDistance = 100,
itemScale = 0.03,
itemStackDistance = 30,
baseScale = 0.85,
onStackComplete
}) => {
const containerRef = useRef(null);
const cardsRef = useRef([]);
const animationRef = useRef(null);
const currentScalesRef = useRef([]);
const targetScalesRef = useRef([]);
// Faster lerp for more responsive feel
const lerp = useCallback((start, end, factor) => {
const diff = end - start;
if (Math.abs(diff) < 0.001) return end;
return start + diff * factor;
}, []);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const cards = Array.from(container.querySelectorAll('.scroll-stack-card'));
cardsRef.current = cards;
// Initialize scales
currentScalesRef.current = cards.map(() => 1);
targetScalesRef.current = cards.map(() => 1);
// Mobile detection
const isMobile = window.innerWidth < 768;
const stickyTopBase = isMobile ? 70 : 100;
const stackDistance = isMobile ? 20 : itemStackDistance;
const cardMargin = isMobile ? 80 : itemDistance;
// Set sticky positions
cards.forEach((card, i) => {
card.style.position = 'sticky';
card.style.top = `${stickyTopBase + i * stackDistance}px`;
card.style.marginBottom = i < cards.length - 1 ? `${cardMargin}px` : '0';
card.style.zIndex = i + 1;
});
// Animation settings
const lerpFactor = 0.18; // Higher = more responsive
let isRunning = true;
const animate = () => {
if (!isRunning) return;
cards.forEach((card, i) => {
const rect = card.getBoundingClientRect();
const stickyTop = stickyTopBase + i * stackDistance;
const isStuck = rect.top <= stickyTop + 5;
// Calculate target scale
let targetScale = 1;
if (isStuck) {
let stackedAbove = 0;
for (let j = i + 1; j < cards.length; j++) {
const nextRect = cards[j].getBoundingClientRect();
const nextStickyTop = stickyTopBase + j * stackDistance;
if (nextRect.top <= nextStickyTop + 5) {
stackedAbove++;
}
}
targetScale = Math.max(baseScale, 1 - (stackedAbove * itemScale));
}
targetScalesRef.current[i] = targetScale;
// Smooth interpolation
const currentScale = currentScalesRef.current[i];
const newScale = lerp(currentScale, targetScale, lerpFactor);
currentScalesRef.current[i] = newScale;
// Apply scale transform only (no blur for performance)
card.style.transform = `scale(${newScale.toFixed(4)})`;
});
animationRef.current = requestAnimationFrame(animate);
};
// Start loop
animationRef.current = requestAnimationFrame(animate);
// Scroll listener for onStackComplete
const handleScroll = () => {
const lastCard = cards[cards.length - 1];
if (lastCard) {
const rect = lastCard.getBoundingClientRect();
const stickyTop = 100 + (cards.length - 1) * itemStackDistance;
if (rect.top <= stickyTop + 5) {
onStackComplete?.();
}
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
isRunning = false;
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
window.removeEventListener('scroll', handleScroll);
};
}, [itemDistance, itemScale, itemStackDistance, baseScale, lerp, onStackComplete]);
return (
<div className={`scroll-stack-container ${className}`.trim()} ref={containerRef}>
{children}
<div className="scroll-stack-end" />
</div>
);
};
export default ScrollStack;
|