158 lines
4.5 KiB
TypeScript
158 lines
4.5 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef, useState } from "react";
|
||
import { gsap } from "gsap";
|
||
|
||
interface PageTransitionProps {
|
||
isOpen: boolean;
|
||
onComplete?: () => void;
|
||
direction?: "reveal" | "unreveal";
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
1. Path data – identical to the original demo
|
||
------------------------------------------------------------------ */
|
||
const paths = {
|
||
step1: {
|
||
unfilled: "M 0 100 V 100 Q 50 100 100 100 V 100 z",
|
||
inBetween: {
|
||
curve1: "M 0 100 V 50 Q 50 0 100 50 V 100 z",
|
||
curve2: "M 0 100 V 50 Q 50 100 100 50 V 100 z",
|
||
},
|
||
filled: "M 0 100 V 0 Q 50 0 100 0 V 100 z",
|
||
},
|
||
step2: {
|
||
filled: "M 0 0 V 100 Q 50 100 100 100 V 0 z",
|
||
inBetween: {
|
||
curve1: "M 0 0 V 50 Q 50 0 100 50 V 0 z",
|
||
curve2: "M 0 0 V 50 Q 50 100 100 50 V 0 z",
|
||
},
|
||
unfilled: "M 0 0 V 0 Q 50 0 100 0 V 0 z",
|
||
},
|
||
};
|
||
|
||
export const PageTransition = ({
|
||
isOpen,
|
||
onComplete,
|
||
direction = "reveal",
|
||
}: PageTransitionProps) => {
|
||
const pathRef = useRef<SVGPathElement>(null);
|
||
const svgRef = useRef<SVGSVGElement>(null); // 👈 new ref for hiding
|
||
const [animating, setAnimating] = useState(false);
|
||
|
||
/* --------------------------------------------------------------
|
||
REVEAL – open second view
|
||
-------------------------------------------------------------- */
|
||
const reveal = () => {
|
||
if (animating) return;
|
||
setAnimating(true);
|
||
|
||
// 👇 Show overlay before animation starts
|
||
gsap.set(svgRef.current, { display: "block" });
|
||
|
||
const tl = gsap.timeline({
|
||
onComplete: () => {
|
||
setAnimating(false);
|
||
onComplete?.();
|
||
// 👇 Hide overlay after animation completes
|
||
gsap.set(svgRef.current, { display: "none" });
|
||
},
|
||
});
|
||
|
||
tl.set(pathRef.current, { attr: { d: paths.step1.unfilled } })
|
||
.to(pathRef.current, {
|
||
duration: 0.8,
|
||
ease: "power4.in",
|
||
attr: { d: paths.step1.inBetween.curve1 },
|
||
})
|
||
.to(pathRef.current, {
|
||
duration: 0.2,
|
||
ease: "power1",
|
||
attr: { d: paths.step1.filled },
|
||
onComplete: () => onComplete?.(),
|
||
})
|
||
.set(pathRef.current, { attr: { d: paths.step2.filled } })
|
||
.to(pathRef.current, {
|
||
duration: 0.2,
|
||
ease: "sine.in",
|
||
attr: { d: paths.step2.inBetween.curve1 },
|
||
})
|
||
.to(pathRef.current, {
|
||
duration: 1,
|
||
ease: "power4",
|
||
attr: { d: paths.step2.unfilled },
|
||
});
|
||
};
|
||
|
||
/* --------------------------------------------------------------
|
||
UNREVEAL – back to first view
|
||
-------------------------------------------------------------- */
|
||
const unreveal = () => {
|
||
if (animating) return;
|
||
setAnimating(true);
|
||
|
||
// 👇 Show overlay before animation starts
|
||
gsap.set(svgRef.current, { display: "block" });
|
||
|
||
const tl = gsap.timeline({
|
||
onComplete: () => {
|
||
setAnimating(false);
|
||
onComplete?.();
|
||
// 👇 Hide overlay after animation completes
|
||
gsap.set(svgRef.current, { display: "none" });
|
||
},
|
||
});
|
||
|
||
tl.set(pathRef.current, { attr: { d: paths.step2.unfilled } })
|
||
.to(pathRef.current, {
|
||
duration: 0.8,
|
||
ease: "power4.in",
|
||
attr: { d: paths.step2.inBetween.curve2 },
|
||
})
|
||
.to(pathRef.current, {
|
||
duration: 0.2,
|
||
ease: "power1",
|
||
attr: { d: paths.step2.filled },
|
||
onComplete: () => onComplete?.(),
|
||
})
|
||
.set(pathRef.current, { attr: { d: paths.step1.filled } })
|
||
.to(pathRef.current, {
|
||
duration: 0.2,
|
||
ease: "sine.in",
|
||
attr: { d: paths.step1.inBetween.curve2 },
|
||
})
|
||
.to(pathRef.current, {
|
||
duration: 1,
|
||
ease: "power4",
|
||
attr: { d: paths.step1.unfilled },
|
||
});
|
||
};
|
||
|
||
/* --------------------------------------------------------------
|
||
React to prop changes
|
||
-------------------------------------------------------------- */
|
||
useEffect(() => {
|
||
if (!isOpen) return;
|
||
if (direction === "reveal") reveal();
|
||
else if (direction === "unreveal") unreveal();
|
||
}, [isOpen, direction]);
|
||
|
||
return (
|
||
<svg
|
||
ref={svgRef}
|
||
className="pointer-events-none fixed top-0 left-0 z-[9999] h-screen w-screen"
|
||
viewBox="0 0 100 100"
|
||
preserveAspectRatio="none"
|
||
style={{ display: "none" }} // 👈 hidden by default
|
||
>
|
||
<path
|
||
ref={pathRef}
|
||
className="overlay__path"
|
||
d="M 0 100 V 100 Q 50 100 100 100 V 100 z"
|
||
vectorEffect="non-scaling-stroke"
|
||
fill="#274b2d"
|
||
/>
|
||
</svg>
|
||
);
|
||
};
|