208 lines
7.0 KiB
TypeScript
Executable File
208 lines
7.0 KiB
TypeScript
Executable File
|
|
import React, { useRef } from 'react';
|
|
import { motion, useScroll, useTransform, Variants, useInView } from 'framer-motion';
|
|
import { WarpOverlay, SandOverlay } from './VisualEffects';
|
|
|
|
export type TransitionType =
|
|
| 'warp'
|
|
| 'wipe-right'
|
|
| 'shutters'
|
|
| 'paint-ripple'
|
|
| 'sand'
|
|
| 'slide'
|
|
| 'gradient';
|
|
|
|
interface Props {
|
|
children: React.ReactNode;
|
|
id: string;
|
|
className?: string;
|
|
transparent?: boolean;
|
|
transitionEffect?: TransitionType;
|
|
fitScreen?: boolean;
|
|
}
|
|
|
|
const SectionWrapper: React.FC<Props> = ({
|
|
children,
|
|
id,
|
|
className = "",
|
|
transparent = false,
|
|
transitionEffect = 'slide',
|
|
fitScreen = true
|
|
}) => {
|
|
const ref = useRef<HTMLElement>(null);
|
|
const isInView = useInView(ref, { amount: 0.2 });
|
|
|
|
// Parallax Logic
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ["start end", "end start"]
|
|
});
|
|
|
|
const y = useTransform(scrollYProgress, [0, 1], ["5%", "-5%"]);
|
|
const smoothEase: [number, number, number, number] = [0.645, 0.045, 0.355, 1.000];
|
|
|
|
// --- RENDER OVERLAYS BASED ON TYPE ---
|
|
|
|
const renderOverlay = () => {
|
|
switch (transitionEffect) {
|
|
case 'warp':
|
|
return (
|
|
<motion.div
|
|
className="absolute inset-0 z-50 pointer-events-none"
|
|
initial={{ opacity: 0 }}
|
|
whileInView={{ opacity: 1 }}
|
|
viewport={{ once: true, margin: "-10%" }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
{/* Only render WarpOverlay when in view to restart the animation/canvas clock */}
|
|
{isInView && <WarpOverlay />}
|
|
<motion.div
|
|
className="absolute inset-0 bg-[#0e0e10]"
|
|
initial={{ opacity: 1 }}
|
|
whileInView={{ opacity: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.8 }}
|
|
/>
|
|
</motion.div>
|
|
);
|
|
|
|
case 'wipe-right':
|
|
return (
|
|
<>
|
|
<motion.div className="absolute inset-0 z-50 bg-[#ff4b4b] pointer-events-none"
|
|
initial={{ x: "0%" }} whileInView={{ x: "100%" }} viewport={{ once: true, amount: 0.2 }} transition={{ duration: 0.7, ease: smoothEase }} />
|
|
<motion.div className="absolute inset-0 z-40 bg-[#20e3b2] pointer-events-none"
|
|
initial={{ x: "0%" }} whileInView={{ x: "100%" }} viewport={{ once: true, amount: 0.2 }} transition={{ duration: 0.7, ease: smoothEase, delay: 0.1 }} />
|
|
</>
|
|
);
|
|
|
|
case 'shutters':
|
|
return (
|
|
<div className="absolute inset-0 z-50 pointer-events-none flex flex-row">
|
|
{[0, 1, 2, 3].map((i) => (
|
|
<motion.div
|
|
key={i}
|
|
className={`relative h-full w-1/4 bg-[#1c1c1f] border-x border-[#333]/50 ${i % 2 === 0 ? 'border-b-4 border-b-[#20e3b2]' : 'border-t-4 border-t-[#ff4b4b]'}`}
|
|
initial={{ scaleY: 1 }}
|
|
whileInView={{ scaleY: 0 }}
|
|
viewport={{ once: true, amount: 0.2 }}
|
|
transition={{
|
|
duration: 0.8,
|
|
ease: [0.76, 0, 0.24, 1], // expoOut
|
|
delay: i * 0.05
|
|
}}
|
|
style={{ originY: i % 2 === 0 ? 0 : 1 }} // Evens shrink to top, Odds shrink to bottom
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
case 'paint-ripple':
|
|
return (
|
|
<>
|
|
{/* The Drop */}
|
|
<motion.div
|
|
className="absolute left-1/2 top-0 z-[60] w-3 h-12 bg-white rounded-full pointer-events-none -translate-x-1/2"
|
|
initial={{ y: "-10vh", opacity: 1 }}
|
|
whileInView={{ y: "50vh", opacity: [1, 1, 0] }}
|
|
viewport={{ once: true, amount: 0.4 }}
|
|
transition={{ duration: 0.6, ease: "easeIn" }}
|
|
/>
|
|
{/* The Ripple */}
|
|
<motion.div
|
|
className="absolute inset-0 z-0 bg-[#e3e1db] pointer-events-none flex items-center justify-center overflow-hidden"
|
|
initial={{ clipPath: "circle(0% at 50% 50%)" }}
|
|
whileInView={{ clipPath: "circle(150% at 50% 50%)" }}
|
|
viewport={{ once: true, amount: 0.4 }}
|
|
transition={{ duration: 0.8, ease: "circOut", delay: 0.55 }}
|
|
/>
|
|
</>
|
|
);
|
|
|
|
case 'gradient':
|
|
return (
|
|
<motion.div
|
|
className="absolute inset-0 z-[20] pointer-events-none"
|
|
style={{
|
|
background: "linear-gradient(to bottom, #e3e1db 0%, transparent 100%)"
|
|
}}
|
|
initial={{ opacity: 1 }}
|
|
whileInView={{ opacity: 0 }}
|
|
viewport={{ once: true, amount: 0.1 }} // Trigger almost immediately on mobile to prevent blocking text
|
|
transition={{ duration: 1.5 }}
|
|
/>
|
|
);
|
|
|
|
case 'sand':
|
|
return (
|
|
<motion.div
|
|
className="absolute inset-0 z-0 overflow-hidden"
|
|
initial={{ opacity: 0 }}
|
|
whileInView={{ opacity: 1 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 1 }}
|
|
>
|
|
<SandOverlay />
|
|
</motion.div>
|
|
)
|
|
|
|
case 'slide':
|
|
return (
|
|
<motion.div
|
|
className="absolute inset-0 z-50 bg-[#ebe9e4] pointer-events-none"
|
|
initial={{ y: "0%" }}
|
|
whileInView={{ y: "100%" }}
|
|
viewport={{ once: true, amount: 0.01 }} // Trigger almost immediately
|
|
transition={{ duration: 1.0, ease: [0.76, 0, 0.24, 1] }}
|
|
/>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// --- CONTENT VARIANTS ---
|
|
const getContentVariants = (): Variants | undefined => {
|
|
if (transitionEffect === 'sand') {
|
|
return {
|
|
hidden: { opacity: 0, scale: 0.95, filter: "blur(10px)" },
|
|
visible: {
|
|
opacity: 1,
|
|
scale: 1,
|
|
filter: "blur(0px)",
|
|
transition: { duration: 1.2, ease: "easeOut" }
|
|
}
|
|
};
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
return (
|
|
<section
|
|
ref={ref}
|
|
id={id}
|
|
className={`w-full relative overflow-hidden snap-start shrink-0 flex flex-col
|
|
min-h-[100dvh]
|
|
${transparent ? 'bg-transparent' : 'bg-theme-main'}
|
|
${transitionEffect === 'gradient' ? 'bg-[#0e0e10]' : ''}
|
|
border-b border-theme/20 ${className}`}
|
|
>
|
|
{renderOverlay()}
|
|
|
|
<motion.div
|
|
style={transitionEffect !== 'slide' ? { y } : undefined}
|
|
initial={transitionEffect === 'sand' ? "hidden" : undefined}
|
|
whileInView={transitionEffect === 'sand' ? "visible" : undefined}
|
|
viewport={{ once: true, amount: 0.2 }}
|
|
variants={getContentVariants()}
|
|
className="w-full flex-grow flex flex-col items-center justify-center px-4 md:px-12 py-20 md:py-28 relative z-10"
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default SectionWrapper;
|