130 lines
3.9 KiB
TypeScript
130 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect } from "react";
|
|
import { usePathname } from "next/navigation";
|
|
import gsap from "gsap";
|
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
|
|
/**
|
|
* AnimationProvider
|
|
* Initializes GSAP + ScrollTrigger globally, refreshes on route changes,
|
|
* and provides smooth defaults.
|
|
*/
|
|
export default function AnimationProvider({ children }: { children: React.ReactNode }) {
|
|
const pathname = usePathname();
|
|
|
|
const initDecorativeBlocks = () => {
|
|
// Clean up previous block triggers to avoid duplicates
|
|
ScrollTrigger.getAll().forEach((t) => {
|
|
if (t.vars && (t.vars as any).id === "block-deco") {
|
|
t.kill();
|
|
}
|
|
});
|
|
|
|
const vh = window.innerHeight || document.documentElement.clientHeight;
|
|
const decorativeBlocks = document.querySelectorAll(".block-decoration");
|
|
decorativeBlocks.forEach((block) => {
|
|
ScrollTrigger.create({
|
|
id: "block-deco",
|
|
trigger: block,
|
|
start: "top 92%",
|
|
onEnter: () => {
|
|
setTimeout(() => {
|
|
block.classList.add("animated");
|
|
}, 150);
|
|
},
|
|
onEnterBack: () => {
|
|
setTimeout(() => {
|
|
block.classList.add("animated");
|
|
}, 150);
|
|
},
|
|
onLeave: () => {
|
|
block.classList.remove("animated");
|
|
},
|
|
onLeaveBack: () => {
|
|
block.classList.remove("animated");
|
|
},
|
|
});
|
|
|
|
// ScrollTrigger does not fire onEnter for blocks already in view at
|
|
// creation — on larger / taller screens those stayed un-animated.
|
|
// Reveal any block already intersecting the viewport.
|
|
const r = block.getBoundingClientRect();
|
|
if (r.top < vh && r.bottom > 0) {
|
|
block.classList.add("animated");
|
|
}
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
// Global GSAP defaults for buttery animations
|
|
gsap.defaults({
|
|
ease: "power3.out",
|
|
duration: 0.8,
|
|
});
|
|
|
|
// Mobile browsers fire `resize` when the address bar shows/hides while
|
|
// scrolling. Refreshing ScrollTrigger on every one of those caused jumpy /
|
|
// re-hidden animations on phones. Ignore those spurious resizes.
|
|
ScrollTrigger.config({ ignoreMobileResize: true });
|
|
|
|
// Run initializations
|
|
initDecorativeBlocks();
|
|
|
|
// Refresh on full window load (when all images, styles, and fonts are in place)
|
|
const handleLoad = () => {
|
|
initDecorativeBlocks();
|
|
ScrollTrigger.refresh(true);
|
|
};
|
|
window.addEventListener("load", handleLoad);
|
|
|
|
// Refresh on window resize — debounced so dragging the window across
|
|
// breakpoints recomputes trigger positions once it settles, instead of
|
|
// thrashing on every intermediate pixel.
|
|
let resizeTimer: ReturnType<typeof setTimeout>;
|
|
const handleResize = () => {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(() => {
|
|
initDecorativeBlocks();
|
|
ScrollTrigger.refresh(true);
|
|
}, 200);
|
|
};
|
|
window.addEventListener("resize", handleResize);
|
|
|
|
return () => {
|
|
clearTimeout(resizeTimer);
|
|
window.removeEventListener("load", handleLoad);
|
|
window.removeEventListener("resize", handleResize);
|
|
ScrollTrigger.getAll().forEach((t) => t.kill());
|
|
};
|
|
}, []);
|
|
|
|
// Listen for route changes and refresh ScrollTriggers so newly rendered content animates in correctly
|
|
useEffect(() => {
|
|
const refreshAnimations = () => {
|
|
initDecorativeBlocks();
|
|
ScrollTrigger.refresh(true);
|
|
};
|
|
|
|
// Run route change handler immediately on navigation
|
|
refreshAnimations();
|
|
|
|
// Staggered refreshes to accommodate page layout calculations and paint frames
|
|
const timers = [
|
|
setTimeout(refreshAnimations, 100),
|
|
setTimeout(refreshAnimations, 400),
|
|
setTimeout(refreshAnimations, 800),
|
|
setTimeout(refreshAnimations, 1500),
|
|
];
|
|
|
|
return () => {
|
|
timers.forEach(clearTimeout);
|
|
};
|
|
}, [pathname]);
|
|
|
|
return <>{children}</>;
|
|
}
|
|
|