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>
|
|||
|
|
);
|
|||
|
|
};
|