178 lines
5.9 KiB
TypeScript
178 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
|
|
const INTRO_LINES = [
|
|
"EVANDRO",
|
|
"FONSECA",
|
|
"JUNIOR",
|
|
];
|
|
|
|
export default function PageIntro() {
|
|
const [isVisible, setIsVisible] = useState(true);
|
|
const [counter, setCounter] = useState(0);
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
// Remove the immediate blocking overlay now that the animated loader is ready
|
|
const blocker = document.getElementById("__loading-blocker");
|
|
if (blocker) blocker.remove();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!mounted) return;
|
|
|
|
// Animate counter from 0 to 100
|
|
const duration = 2000;
|
|
const start = performance.now();
|
|
|
|
const tick = (now: number) => {
|
|
const elapsed = now - start;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
// Eased progress
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
setCounter(Math.floor(eased * 100));
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(tick);
|
|
} else {
|
|
// Wait a beat then hide
|
|
setTimeout(() => setIsVisible(false), 400);
|
|
}
|
|
};
|
|
|
|
requestAnimationFrame(tick);
|
|
}, [mounted]);
|
|
|
|
if (!mounted) return null;
|
|
|
|
return createPortal(
|
|
<AnimatePresence>
|
|
{isVisible && (
|
|
<motion.div
|
|
className="fixed inset-0 z-[99999] flex items-center justify-center bg-[#0a0a0a]"
|
|
style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, width: "100vw", height: "100vh" }}
|
|
exit={{
|
|
clipPath: "inset(0 0 100% 0)",
|
|
transition: { duration: 1, ease: [0.76, 0, 0.24, 1] },
|
|
}}
|
|
>
|
|
{/* Horizontal lines decoration */}
|
|
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
|
{[...Array(5)].map((_, i) => (
|
|
<motion.div
|
|
key={i}
|
|
className="absolute left-0 h-px w-full bg-[rgba(204,255,0,0.06)]"
|
|
style={{ top: `${20 + i * 15}%` }}
|
|
initial={{ scaleX: 0 }}
|
|
animate={{ scaleX: 1 }}
|
|
transition={{
|
|
duration: 1.5,
|
|
delay: 0.1 * i,
|
|
ease: [0.76, 0, 0.24, 1],
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Counter */}
|
|
<motion.div
|
|
className="absolute bottom-12 right-12 font-mono text-[clamp(48px,8vw,96px)] font-bold leading-none tracking-tighter text-[rgba(204,255,0,0.15)]"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
{counter.toString().padStart(3, "0")}
|
|
</motion.div>
|
|
|
|
{/* Progress bar */}
|
|
<div className="absolute bottom-8 left-12 right-12 h-px bg-[rgba(255,255,255,0.06)]">
|
|
<motion.div
|
|
className="h-full bg-[var(--slash)]"
|
|
initial={{ width: "0%" }}
|
|
animate={{ width: `${counter}%` }}
|
|
transition={{ duration: 0.1 }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Name reveal */}
|
|
<div className="relative flex flex-col items-center gap-0">
|
|
{INTRO_LINES.map((line, i) => (
|
|
<div key={line} className="overflow-hidden">
|
|
<motion.div
|
|
className="font-display text-[clamp(2.5rem,10vw,7rem)] font-extrabold leading-[0.9] tracking-[-0.06em] text-[#f4f2ed]"
|
|
initial={{ y: "100%", rotateX: -40 }}
|
|
animate={{ y: "0%", rotateX: 0 }}
|
|
transition={{
|
|
duration: 0.8,
|
|
delay: 0.3 + i * 0.12,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
}}
|
|
>
|
|
{line}
|
|
</motion.div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Subtitle */}
|
|
<motion.div
|
|
className="mt-6 overflow-hidden"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 1, duration: 0.6 }}
|
|
>
|
|
<motion.p
|
|
className="font-mono text-[10px] uppercase tracking-[0.3em] text-[rgba(204,255,0,0.7)]"
|
|
initial={{ y: 20 }}
|
|
animate={{ y: 0 }}
|
|
transition={{ delay: 1, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
|
>
|
|
Software Engineer
|
|
</motion.p>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Corner markers */}
|
|
<div className="pointer-events-none absolute inset-0">
|
|
{/* Top-left */}
|
|
<motion.div
|
|
className="absolute left-8 top-8"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 0.3 }}
|
|
transition={{ delay: 0.5 }}
|
|
>
|
|
<div className="h-6 w-px bg-[var(--slash)]" />
|
|
<div className="absolute left-0 top-0 h-px w-6 bg-[var(--slash)]" />
|
|
</motion.div>
|
|
{/* Top-right */}
|
|
<motion.div
|
|
className="absolute right-8 top-8"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 0.3 }}
|
|
transition={{ delay: 0.6 }}
|
|
>
|
|
<div className="absolute right-0 top-0 h-6 w-px bg-[var(--slash)]" />
|
|
<div className="absolute right-0 top-0 h-px w-6 bg-[var(--slash)]" />
|
|
</motion.div>
|
|
{/* Bottom-left */}
|
|
<motion.div
|
|
className="absolute bottom-8 left-8"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 0.3 }}
|
|
transition={{ delay: 0.7 }}
|
|
>
|
|
<div className="h-6 w-px bg-[var(--slash)]" />
|
|
<div className="absolute bottom-0 left-0 h-px w-6 bg-[var(--slash)]" />
|
|
</motion.div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>,
|
|
document.body
|
|
);
|
|
}
|
|
|