123 lines
3.7 KiB
Plaintext
123 lines
3.7 KiB
Plaintext
---
|
|
interface Props {
|
|
animation?: 'fade-up' | 'fade-down' | 'fade-left' | 'fade-right' | 'zoom-in' | 'zoom-out';
|
|
duration?: number; // in milliseconds
|
|
delay?: number; // in milliseconds
|
|
threshold?: number; // 0-1, percentage of element visible to trigger
|
|
rootMargin?: string; // CSS margin value
|
|
once?: boolean; // animate only once or every time element enters viewport
|
|
class?: string;
|
|
}
|
|
|
|
const {
|
|
animation = 'fade-up',
|
|
duration = 600,
|
|
delay = 0,
|
|
threshold = 0.1,
|
|
rootMargin = '0px',
|
|
once = true,
|
|
class: className = '',
|
|
} = Astro.props;
|
|
|
|
// Generate a unique ID with better entropy
|
|
const id = `scroll-reveal-${crypto.randomUUID().slice(0, 8)}`;
|
|
---
|
|
|
|
<div
|
|
id={id}
|
|
class:list={['scroll-reveal', `reveal-${animation}`, className]}
|
|
data-duration={duration}
|
|
data-delay={delay}
|
|
data-threshold={threshold}
|
|
data-root-margin={rootMargin}
|
|
data-once={once}
|
|
style="display: contents;"
|
|
>
|
|
<slot />
|
|
</div>
|
|
|
|
<style>
|
|
.scroll-reveal {
|
|
opacity: 0;
|
|
will-change: transform, opacity;
|
|
transition-property: opacity, transform;
|
|
}
|
|
|
|
/* Initial states based on animation type */
|
|
.reveal-fade-up { transform: translateY(30px); }
|
|
.reveal-fade-down { transform: translateY(-30px); }
|
|
.reveal-fade-left { transform: translateX(30px); }
|
|
.reveal-fade-right { transform: translateX(-30px); }
|
|
.reveal-zoom-in { transform: scale(0.9); }
|
|
.reveal-zoom-out { transform: scale(1.1); }
|
|
|
|
/* Revealed state */
|
|
.scroll-reveal.revealed {
|
|
opacity: 1;
|
|
transform: translate(0) scale(1);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.scroll-reveal {
|
|
transition: none !important;
|
|
opacity: 1 !important;
|
|
transform: none !important;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// Initialize scroll reveal animations
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const scrollRevealElements = document.querySelectorAll('.scroll-reveal');
|
|
|
|
if (!scrollRevealElements.length) return;
|
|
|
|
// Check if IntersectionObserver is supported
|
|
if ('IntersectionObserver' in window) {
|
|
scrollRevealElements.forEach(element => {
|
|
// Get animation parameters from data attributes
|
|
const duration = parseInt(element.getAttribute('data-duration') || '600', 10);
|
|
const delay = parseInt(element.getAttribute('data-delay') || '0', 10);
|
|
const threshold = parseFloat(element.getAttribute('data-threshold') || '0.1');
|
|
const rootMargin = element.getAttribute('data-root-margin') || '0px';
|
|
const once = element.getAttribute('data-once') === 'true';
|
|
|
|
// Set transition based on parameters
|
|
(element as HTMLElement).style.transition = `opacity ${duration}ms ease ${delay}ms, transform ${duration}ms ease ${delay}ms`;
|
|
|
|
// Create observer for this element
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('revealed');
|
|
|
|
// Unobserve if once is true
|
|
if (once) {
|
|
observer.unobserve(entry.target);
|
|
}
|
|
} else if (!once) {
|
|
// Remove class if element leaves viewport and once is false
|
|
entry.target.classList.remove('revealed');
|
|
}
|
|
});
|
|
},
|
|
{
|
|
threshold,
|
|
rootMargin
|
|
}
|
|
);
|
|
|
|
// Start observing
|
|
observer.observe(element);
|
|
});
|
|
} else {
|
|
// Fallback for browsers that don't support IntersectionObserver
|
|
scrollRevealElements.forEach(element => {
|
|
element.classList.add('revealed');
|
|
});
|
|
}
|
|
});
|
|
</script>
|