feat: Implement initial cinematic portfolio UI with animated components and page structure.

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-18 06:45:42 -03:00
1 parent 9d779390e5
commit 2ede7e98f9
18 files changed
+2107 -147

No files matched your search

@@ -0,0 +1,90 @@
"use client";
import dynamic from "next/dynamic";
import { Suspense } from "react";
import Hero from "../Hero";
import { SectionLoading } from "../SectionLoading";
const About = dynamic(() => import("../About"));
const Skills = dynamic(() => import("../Skills"));
const Experience = dynamic(() => import("../Experience"));
const WorkModalities = dynamic(() => import("../WorkModalities").then(mod => mod.WorkModalities));
const Contact = dynamic(() => import("../Contact"));
const Projects = dynamic(() => import("../Projects"));
const LighthouseMetrics = dynamic(() => import("../LighthouseMetrics"));
const Footer = dynamic(() => import("../Footer"));
const CinematicSection = dynamic(() => import("./CinematicSection"), { ssr: false });
const ScrollVelocityText = dynamic(() => import("./ScrollVelocityText"), { ssr: false });
const SectionDivider = dynamic(() => import("./SectionDivider"), { ssr: false });
export default function CinematicHome() {
return (
<div className="flex min-h-screen flex-col overflow-x-hidden">
<main className="ds-main">
<Hero />
{/* Marquee divider after hero */}
<div className="overflow-hidden bg-[var(--ink)] py-6 text-[rgba(244,242,237,0.06)]">
<ScrollVelocityText
text="SOFTWARE ENGINEERING"
baseVelocity={-1.5}
/>
</div>
<CinematicSection effect="parallax-depth">
<About />
</CinematicSection>
<SectionDivider variant="waveform" />
<CinematicSection effect="clip-reveal">
<Skills />
</CinematicSection>
<div className="overflow-hidden bg-[var(--ink)] py-4 text-[rgba(204,255,0,0.04)]">
<ScrollVelocityText
text="NEXT.JS TAURI RUST NODE.JS TYPESCRIPT"
baseVelocity={1}
/>
</div>
<CinematicSection effect="fade-scale">
<Experience />
</CinematicSection>
<SectionDivider variant="gradient" />
<CinematicSection effect="rotate-in">
<Suspense
fallback={<SectionLoading message="Carregando projetos..." />}
>
<Projects />
</Suspense>
</CinematicSection>
<CinematicSection effect="clip-reveal">
<LighthouseMetrics />
</CinematicSection>
<SectionDivider variant="pulse" />
<CinematicSection effect="parallax-depth">
<WorkModalities />
</CinematicSection>
<div className="overflow-hidden bg-[var(--ink)] py-6 text-[rgba(244,242,237,0.05)]">
<ScrollVelocityText
text="VAMOS CONVERSAR"
baseVelocity={-2}
/>
</div>
<CinematicSection effect="fade-scale">
<Contact />
</CinematicSection>
</main>
<Footer />
</div>
);
}
@@ -0,0 +1,19 @@
"use client";
import dynamic from "next/dynamic";
const CustomCursor = dynamic(() => import("./CustomCursor"), { ssr: false });
const FilmGrain = dynamic(() => import("./FilmGrain"), { ssr: false });
const PageIntro = dynamic(() => import("./PageIntro"), { ssr: false });
const ScrollProgress = dynamic(() => import("./ScrollProgress"), { ssr: false });
export default function CinematicOverlays() {
return (
<>
<PageIntro />
<CustomCursor />
<FilmGrain />
<ScrollProgress />
</>
);
}
@@ -0,0 +1,173 @@
"use client";
import { useRef, useEffect, type ReactNode } from "react";
interface CinematicSectionProps {
children: ReactNode;
className?: string;
effect?: "fade-scale" | "clip-reveal" | "parallax-depth" | "rotate-in";
id?: string;
}
export default function CinematicSection({
children,
className = "",
effect = "fade-scale",
id,
}: CinematicSectionProps) {
const sectionRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = sectionRef.current;
if (!el) return;
// Check reduced motion preference
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
let cleanup: (() => void) | undefined;
const setup = async () => {
const [{ gsap }, { ScrollTrigger }] = await Promise.all([
import("gsap"),
import("gsap/ScrollTrigger"),
]);
gsap.registerPlugin(ScrollTrigger);
switch (effect) {
case "fade-scale": {
gsap.set(el, {
opacity: 0,
scale: 0.92,
transformOrigin: "center center",
});
const tl = gsap.timeline({
scrollTrigger: {
trigger: el,
start: "top 85%",
end: "top 20%",
scrub: 0.8,
},
});
tl.to(el, {
opacity: 1,
scale: 1,
ease: "power2.out",
});
cleanup = () => {
tl.scrollTrigger?.kill();
tl.kill();
};
break;
}
case "clip-reveal": {
gsap.set(el, {
clipPath: "inset(15% 5% 15% 5%)",
opacity: 0.3,
});
const tl = gsap.timeline({
scrollTrigger: {
trigger: el,
start: "top 80%",
end: "top 15%",
scrub: 1,
},
});
tl.to(el, {
clipPath: "inset(0% 0% 0% 0%)",
opacity: 1,
ease: "power3.out",
});
cleanup = () => {
tl.scrollTrigger?.kill();
tl.kill();
};
break;
}
case "parallax-depth": {
gsap.set(el, {
y: 80,
opacity: 0,
rotateX: 4,
transformPerspective: 1200,
transformOrigin: "center bottom",
});
const tl = gsap.timeline({
scrollTrigger: {
trigger: el,
start: "top 90%",
end: "top 30%",
scrub: 0.6,
},
});
tl.to(el, {
y: 0,
opacity: 1,
rotateX: 0,
ease: "power2.out",
});
cleanup = () => {
tl.scrollTrigger?.kill();
tl.kill();
};
break;
}
case "rotate-in": {
gsap.set(el, {
opacity: 0,
rotateY: -5,
x: -60,
transformPerspective: 1000,
transformOrigin: "left center",
});
const tl = gsap.timeline({
scrollTrigger: {
trigger: el,
start: "top 80%",
end: "top 25%",
scrub: 0.8,
},
});
tl.to(el, {
opacity: 1,
rotateY: 0,
x: 0,
ease: "power3.out",
});
cleanup = () => {
tl.scrollTrigger?.kill();
tl.kill();
};
break;
}
}
};
void setup();
return () => {
cleanup?.();
};
}, [effect]);
return (
<div ref={sectionRef} className={className} id={id}>
{children}
</div>
);
}
+119
View File
@@ -0,0 +1,119 @@
"use client";
import { useRef, useEffect } from "react";
interface CinematicTextProps {
text: string;
className?: string;
tag?: "h1" | "h2" | "h3" | "p" | "span";
delay?: number;
splitBy?: "chars" | "words" | "lines";
}
export default function CinematicText({
text,
className = "",
tag: Tag = "h2",
delay = 0,
splitBy = "chars",
}: CinematicTextProps) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
let cleanup: (() => void) | undefined;
const setup = async () => {
const [{ gsap }, { ScrollTrigger }] = await Promise.all([
import("gsap"),
import("gsap/ScrollTrigger"),
]);
gsap.registerPlugin(ScrollTrigger);
const spans = el.querySelectorAll("[data-char]");
if (spans.length === 0) return;
gsap.set(spans, {
opacity: 0,
y: 60,
rotateX: -90,
transformOrigin: "center bottom",
transformPerspective: 600,
});
const tl = gsap.timeline({
scrollTrigger: {
trigger: el,
start: "top 80%",
toggleActions: "play none none none",
},
});
tl.to(spans, {
opacity: 1,
y: 0,
rotateX: 0,
duration: 0.8,
ease: "back.out(1.2)",
stagger: splitBy === "chars" ? 0.02 : splitBy === "words" ? 0.06 : 0.12,
delay,
});
cleanup = () => {
tl.scrollTrigger?.kill();
tl.kill();
};
};
void setup();
return () => {
cleanup?.();
};
}, [text, delay, splitBy]);
const renderContent = () => {
if (splitBy === "chars") {
return text.split("").map((char, i) => (
<span
key={`${char}-${i}`}
data-char
className="inline-block will-change-transform"
style={{ whiteSpace: char === " " ? "pre" : undefined }}
>
{char === " " ? "\u00A0" : char}
</span>
));
}
if (splitBy === "words") {
return text.split(" ").map((word, i) => (
<span key={`${word}-${i}`} className="inline-block overflow-hidden">
<span data-char className="inline-block will-change-transform">
{word}
</span>
{i < text.split(" ").length - 1 && "\u00A0"}
</span>
));
}
// lines
return text.split("\n").map((line, i) => (
<span key={`${line}-${i}`} className="block overflow-hidden">
<span data-char className="block will-change-transform">
{line}
</span>
</span>
));
};
return (
<div ref={containerRef} className="overflow-hidden">
<Tag className={className}>{renderContent()}</Tag>
</div>
);
}
+159
View File
@@ -0,0 +1,159 @@
"use client";
import { useEffect, useRef, useState, useCallback } from "react";
import { motion, useMotionValue, useSpring } from "framer-motion";
export default function CustomCursor() {
const [isHovering, setIsHovering] = useState(false);
const [isClicking, setIsClicking] = useState(false);
const [hoverText, setHoverText] = useState("");
const [isVisible, setIsVisible] = useState(false);
const cursorX = useMotionValue(0);
const cursorY = useMotionValue(0);
const springConfig = { damping: 25, stiffness: 250, mass: 0.5 };
const x = useSpring(cursorX, springConfig);
const y = useSpring(cursorY, springConfig);
const ringSpringConfig = { damping: 20, stiffness: 150, mass: 0.8 };
const ringX = useSpring(cursorX, ringSpringConfig);
const ringY = useSpring(cursorY, ringSpringConfig);
const rafRef = useRef<number>(0);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => {
cursorX.set(e.clientX);
cursorY.set(e.clientY);
if (!isVisible) setIsVisible(true);
});
},
[cursorX, cursorY, isVisible]
);
useEffect(() => {
// Only show custom cursor on desktop with fine pointer
const hasFinePointer = window.matchMedia("(pointer: fine)").matches;
if (!hasFinePointer) return;
const handleDown = () => setIsClicking(true);
const handleUp = () => setIsClicking(false);
const handleLeave = () => setIsVisible(false);
const handleEnter = () => setIsVisible(true);
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mousedown", handleDown);
window.addEventListener("mouseup", handleUp);
document.addEventListener("mouseleave", handleLeave);
document.addEventListener("mouseenter", handleEnter);
// Detect hoverable elements
const observer = new MutationObserver(() => {
attachHoverListeners();
});
observer.observe(document.body, { childList: true, subtree: true });
attachHoverListeners();
function attachHoverListeners() {
const hoverables = document.querySelectorAll(
'a, button, [role="button"], input, textarea, select, [data-cursor]'
);
for (const el of hoverables) {
el.addEventListener("mouseenter", handleElementEnter as EventListener);
el.addEventListener("mouseleave", handleElementLeave as EventListener);
}
}
function handleElementEnter(e: Event) {
const el = e.currentTarget as HTMLElement;
setIsHovering(true);
const cursorData = el.getAttribute("data-cursor");
if (cursorData) setHoverText(cursorData);
}
function handleElementLeave() {
setIsHovering(false);
setHoverText("");
}
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mousedown", handleDown);
window.removeEventListener("mouseup", handleUp);
document.removeEventListener("mouseleave", handleLeave);
document.removeEventListener("mouseenter", handleEnter);
observer.disconnect();
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [handleMouseMove]);
// Don't render on touch devices
if (typeof window !== "undefined" && !window.matchMedia("(pointer: fine)").matches) {
return null;
}
return (
<>
<style jsx global>{`
@media (pointer: fine) {
* { cursor: none !important; }
}
`}</style>
{/* Dot cursor */}
<motion.div
className="pointer-events-none fixed top-0 left-0 z-[9999] mix-blend-difference"
style={{
x,
y,
translateX: "-50%",
translateY: "-50%",
}}
animate={{
scale: isClicking ? 0.5 : isHovering ? 0.5 : 1,
opacity: isVisible ? 1 : 0,
}}
transition={{ duration: 0.15 }}
>
<div
className="rounded-full bg-[#ccff00]"
style={{ width: 8, height: 8 }}
/>
</motion.div>
{/* Ring cursor */}
<motion.div
className="pointer-events-none fixed top-0 left-0 z-[9998] mix-blend-difference"
style={{
x: ringX,
y: ringY,
translateX: "-50%",
translateY: "-50%",
}}
animate={{
width: isHovering ? 80 : 40,
height: isHovering ? 80 : 40,
opacity: isVisible ? (isHovering ? 0.8 : 0.4) : 0,
borderWidth: isHovering ? 2 : 1,
}}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
>
<div
className="flex h-full w-full items-center justify-center rounded-full border-[#ccff00]"
style={{ borderStyle: "solid", borderColor: "#ccff00", borderWidth: "inherit" }}
>
{hoverText && (
<motion.span
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
className="text-[9px] font-bold uppercase tracking-widest text-[#ccff00]"
>
{hoverText}
</motion.span>
)}
</div>
</motion.div>
</>
);
}
+65
View File
@@ -0,0 +1,65 @@
"use client";
import { useEffect, useRef } from "react";
export default function FilmGrain() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animId: number;
let lastFrame = 0;
const fps = 24; // Cinematic film fps
const frameInterval = 1000 / fps;
const resize = () => {
canvas.width = window.innerWidth / 2;
canvas.height = window.innerHeight / 2;
};
resize();
window.addEventListener("resize", resize);
const render = (timestamp: number) => {
animId = requestAnimationFrame(render);
const delta = timestamp - lastFrame;
if (delta < frameInterval) return;
lastFrame = timestamp - (delta % frameInterval);
const { width, height } = canvas;
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const noise = Math.random() * 20;
data[i] = noise;
data[i + 1] = noise;
data[i + 2] = noise;
data[i + 3] = 12; // Very subtle
}
ctx.putImageData(imageData, 0, 0);
};
animId = requestAnimationFrame(render);
return () => {
cancelAnimationFrame(animId);
window.removeEventListener("resize", resize);
};
}, []);
return (
<canvas
ref={canvasRef}
className="pointer-events-none fixed inset-0 z-[9990] h-full w-full opacity-[0.35] mix-blend-overlay"
style={{ imageRendering: "pixelated" }}
aria-hidden="true"
/>
);
}
@@ -0,0 +1,156 @@
"use client";
import { useRef, useMemo } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Float, MeshDistortMaterial, MeshTransmissionMaterial } from "@react-three/drei";
import * as THREE from "three";
function GlassSphere({ position, scale = 1 }: { position: [number, number, number]; scale?: number }) {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state) => {
if (!meshRef.current) return;
const t = state.clock.elapsedTime;
meshRef.current.rotation.x = Math.sin(t * 0.3) * 0.2;
meshRef.current.rotation.z = Math.cos(t * 0.2) * 0.1;
});
return (
<Float speed={1.5} rotationIntensity={0.3} floatIntensity={0.8}>
<mesh ref={meshRef} position={position} scale={scale}>
<sphereGeometry args={[1, 32, 32]} />
<MeshDistortMaterial
color="#ccff00"
transparent
opacity={0.08}
distort={0.4}
speed={2}
wireframe
/>
</mesh>
</Float>
);
}
function WireframeRing({ position, scale = 1 }: { position: [number, number, number]; scale?: number }) {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state) => {
if (!meshRef.current) return;
const t = state.clock.elapsedTime;
meshRef.current.rotation.x = t * 0.1;
meshRef.current.rotation.y = t * 0.15;
});
return (
<Float speed={1} rotationIntensity={0.5} floatIntensity={0.4}>
<mesh ref={meshRef} position={position} scale={scale}>
<torusGeometry args={[1, 0.02, 16, 100]} />
<meshStandardMaterial
color="#ccff00"
transparent
opacity={0.15}
emissive="#ccff00"
emissiveIntensity={0.1}
/>
</mesh>
</Float>
);
}
function DataStream({ count = 50 }: { count?: number }) {
const pointsRef = useRef<THREE.Points>(null);
const positions = useMemo(() => {
const pos = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const i3 = i * 3;
// Create a spiral/helix pattern
const angle = (i / count) * Math.PI * 8;
const radius = 2 + Math.sin(angle * 0.5) * 0.5;
pos[i3] = Math.cos(angle) * radius;
pos[i3 + 1] = (i / count - 0.5) * 12;
pos[i3 + 2] = Math.sin(angle) * radius;
}
return pos;
}, [count]);
useFrame((state) => {
if (!pointsRef.current) return;
const t = state.clock.elapsedTime;
pointsRef.current.rotation.y = t * 0.05;
const posAttr = pointsRef.current.geometry.getAttribute("position") as THREE.BufferAttribute;
const arr = posAttr.array as Float32Array;
for (let i = 0; i < count; i++) {
const i3 = i * 3;
arr[i3 + 1] += 0.02;
if (arr[i3 + 1] > 6) arr[i3 + 1] = -6;
}
posAttr.needsUpdate = true;
});
return (
<points ref={pointsRef}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
args={[positions, 3]}
count={count}
/>
</bufferGeometry>
<pointsMaterial
color="#ccff00"
size={0.04}
transparent
opacity={0.4}
sizeAttenuation
blending={THREE.AdditiveBlending}
depthWrite={false}
/>
</points>
);
}
interface FloatingElementsProps {
variant?: "sphere" | "ring" | "data" | "combined";
className?: string;
}
export default function FloatingElements({
variant = "combined",
className = "",
}: FloatingElementsProps) {
return (
<div className={`pointer-events-none absolute inset-0 ${className}`}>
<Canvas
camera={{ position: [0, 0, 6], fov: 50 }}
dpr={[1, 1.5]}
gl={{ antialias: false, alpha: true, powerPreference: "high-performance" }}
style={{ background: "transparent" }}
>
<ambientLight intensity={0.2} />
<pointLight position={[5, 5, 5]} intensity={0.3} color="#ccff00" />
{(variant === "sphere" || variant === "combined") && (
<>
<GlassSphere position={[3, 1, -2]} scale={0.8} />
<GlassSphere position={[-4, -1, -3]} scale={1.2} />
</>
)}
{(variant === "ring" || variant === "combined") && (
<>
<WireframeRing position={[0, 0, -2]} scale={1.5} />
<WireframeRing position={[-3, 2, -4]} scale={0.8} />
</>
)}
{(variant === "data" || variant === "combined") && (
<DataStream count={60} />
)}
</Canvas>
</div>
);
}
+305
View File
@@ -0,0 +1,305 @@
"use client";
import { useRef, useMemo, useCallback, useEffect } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { Float, MeshDistortMaterial } from "@react-three/drei";
import * as THREE from "three";
function ParticleField({ count = 2000 }: { count?: number }) {
const mesh = useRef<THREE.Points>(null);
const mouseRef = useRef({ x: 0, y: 0 });
const [positions, velocities] = useMemo(() => {
const pos = new Float32Array(count * 3);
const vel = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const i3 = i * 3;
pos[i3] = (Math.random() - 0.5) * 20;
pos[i3 + 1] = (Math.random() - 0.5) * 20;
pos[i3 + 2] = (Math.random() - 0.5) * 15;
vel[i3] = (Math.random() - 0.5) * 0.002;
vel[i3 + 1] = (Math.random() - 0.5) * 0.002;
vel[i3 + 2] = (Math.random() - 0.5) * 0.001;
}
return [pos, vel];
}, [count]);
const sizes = useMemo(() => {
const s = new Float32Array(count);
for (let i = 0; i < count; i++) {
s[i] = Math.random() * 2 + 0.5;
}
return s;
}, [count]);
const handlePointerMove = useCallback((e: { clientX: number; clientY: number }) => {
mouseRef.current.x = (e.clientX / window.innerWidth) * 2 - 1;
mouseRef.current.y = -(e.clientY / window.innerHeight) * 2 + 1;
}, []);
useFrame((state) => {
if (!mesh.current) return;
const geo = mesh.current.geometry;
const posAttr = geo.getAttribute("position") as THREE.BufferAttribute;
const arr = posAttr.array as Float32Array;
const time = state.clock.elapsedTime;
for (let i = 0; i < count; i++) {
const i3 = i * 3;
arr[i3] += velocities[i3] + Math.sin(time * 0.3 + i) * 0.001;
arr[i3 + 1] += velocities[i3 + 1] + Math.cos(time * 0.2 + i) * 0.001;
arr[i3 + 2] += velocities[i3 + 2];
// Mouse influence
const dx = mouseRef.current.x * 5 - arr[i3];
const dy = mouseRef.current.y * 5 - arr[i3 + 1];
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 3) {
const force = (3 - dist) * 0.0003;
arr[i3] += dx * force;
arr[i3 + 1] += dy * force;
}
// Wrap around
if (arr[i3] > 10) arr[i3] = -10;
if (arr[i3] < -10) arr[i3] = 10;
if (arr[i3 + 1] > 10) arr[i3 + 1] = -10;
if (arr[i3 + 1] < -10) arr[i3 + 1] = 10;
if (arr[i3 + 2] > 7.5) arr[i3 + 2] = -7.5;
if (arr[i3 + 2] < -7.5) arr[i3 + 2] = 7.5;
}
posAttr.needsUpdate = true;
// Slow rotation
mesh.current.rotation.y = time * 0.02;
mesh.current.rotation.x = Math.sin(time * 0.01) * 0.1;
});
useEffect(() => {
window.addEventListener("pointermove", handlePointerMove);
return () => window.removeEventListener("pointermove", handlePointerMove);
}, [handlePointerMove]);
return (
<points ref={mesh}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
args={[positions, 3]}
count={count}
/>
<bufferAttribute
attach="attributes-size"
args={[sizes, 1]}
count={count}
/>
</bufferGeometry>
<pointsMaterial
color="#ccff00"
size={0.03}
transparent
opacity={0.6}
sizeAttenuation
blending={THREE.AdditiveBlending}
depthWrite={false}
/>
</points>
);
}
function FloatingGeometry() {
const meshRef = useRef<THREE.Mesh>(null);
const mesh2Ref = useRef<THREE.Mesh>(null);
const mesh3Ref = useRef<THREE.Mesh>(null);
useFrame((state) => {
const t = state.clock.elapsedTime;
if (meshRef.current) {
meshRef.current.rotation.x = t * 0.15;
meshRef.current.rotation.y = t * 0.1;
meshRef.current.position.y = Math.sin(t * 0.4) * 0.3;
}
if (mesh2Ref.current) {
mesh2Ref.current.rotation.x = t * 0.1;
mesh2Ref.current.rotation.z = t * 0.12;
mesh2Ref.current.position.y = Math.cos(t * 0.3) * 0.5;
}
if (mesh3Ref.current) {
mesh3Ref.current.rotation.y = t * 0.08;
mesh3Ref.current.rotation.z = t * 0.06;
}
});
return (
<>
<Float speed={1.5} rotationIntensity={0.4} floatIntensity={0.6}>
<mesh ref={meshRef} position={[4, 1.5, -3]} scale={1.2}>
<icosahedronGeometry args={[1, 1]} />
<MeshDistortMaterial
color="#ccff00"
wireframe
transparent
opacity={0.15}
distort={0.3}
speed={2}
/>
</mesh>
</Float>
<Float speed={1.2} rotationIntensity={0.3} floatIntensity={0.8}>
<mesh ref={mesh2Ref} position={[-5, -2, -4]} scale={0.8}>
<octahedronGeometry args={[1, 0]} />
<MeshDistortMaterial
color="#ffffff"
wireframe
transparent
opacity={0.08}
distort={0.2}
speed={1.5}
/>
</mesh>
</Float>
<Float speed={0.8} rotationIntensity={0.2} floatIntensity={0.5}>
<mesh ref={mesh3Ref} position={[2, -3, -5]} scale={1.5}>
<torusGeometry args={[1, 0.3, 16, 32]} />
<meshStandardMaterial
color="#ccff00"
wireframe
transparent
opacity={0.06}
/>
</mesh>
</Float>
</>
);
}
function GridFloor() {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state) => {
if (meshRef.current) {
const material = meshRef.current.material as THREE.ShaderMaterial;
if (material.uniforms) {
material.uniforms.uTime.value = state.clock.elapsedTime;
}
}
});
const gridShader = useMemo(
() => ({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color("#ccff00") },
},
vertexShader: `
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vUv = uv;
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vec2 grid = abs(fract(vPosition.xz * 0.5) - 0.5) / fwidth(vPosition.xz * 0.5);
float line = min(grid.x, grid.y);
float alpha = 1.0 - min(line, 1.0);
// Distance fade
float dist = length(vPosition.xz) * 0.08;
alpha *= max(0.0, 1.0 - dist);
alpha *= 0.12;
// Pulse wave
float wave = sin(length(vPosition.xz) * 0.3 - uTime * 0.8) * 0.5 + 0.5;
alpha *= 0.5 + wave * 0.5;
gl_FragColor = vec4(uColor, alpha);
}
`,
}),
[]
);
return (
<mesh ref={meshRef} rotation={[-Math.PI / 2, 0, 0]} position={[0, -4, 0]}>
<planeGeometry args={[40, 40, 1, 1]} />
<shaderMaterial
{...gridShader}
transparent
side={THREE.DoubleSide}
depthWrite={false}
/>
</mesh>
);
}
function CameraRig() {
const { camera } = useThree();
const mouseRef = useRef({ x: 0, y: 0 });
useFrame(() => {
const targetX = mouseRef.current.x * 0.5;
const targetY = mouseRef.current.y * 0.3 + 0.5;
camera.position.x += (targetX - camera.position.x) * 0.02;
camera.position.y += (targetY - camera.position.y) * 0.02;
camera.lookAt(0, 0, 0);
});
useEffect(() => {
const handler = (e: MouseEvent) => {
mouseRef.current.x = (e.clientX / window.innerWidth) * 2 - 1;
mouseRef.current.y = -(e.clientY / window.innerHeight) * 2 + 1;
};
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
}, []);
return null;
}
export default function HeroScene() {
return (
<div
className="absolute inset-0 z-0"
style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }}
>
<Canvas
camera={{ position: [0, 0.5, 8], fov: 60, near: 0.1, far: 100 }}
dpr={[1, 1.5]}
gl={{
antialias: false,
alpha: true,
powerPreference: "high-performance",
}}
style={{
background: "transparent",
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
display: "block",
}}
>
<fog attach="fog" args={["#0a0a0a", 5, 25]} />
<ambientLight intensity={0.3} />
<directionalLight position={[5, 5, 5]} intensity={0.5} color="#ccff00" />
<pointLight position={[-5, 3, -5]} intensity={0.3} color="#ffffff" />
<CameraRig />
<ParticleField count={1500} />
<FloatingGeometry />
<GridFloor />
</Canvas>
</div>
);
}
@@ -0,0 +1,104 @@
"use client";
import { useRef, useEffect, type ReactNode } from "react";
interface HorizontalScrollProps {
children: ReactNode;
className?: string;
}
export default function HorizontalScroll({
children,
className = "",
}: HorizontalScrollProps) {
const containerRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
const track = trackRef.current;
if (!container || !track) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
// Disable on mobile for better UX
if (window.innerWidth < 768) return;
let cleanup: (() => void) | undefined;
const setup = async () => {
const [{ gsap }, { ScrollTrigger }] = await Promise.all([
import("gsap"),
import("gsap/ScrollTrigger"),
]);
gsap.registerPlugin(ScrollTrigger);
const totalWidth = track.scrollWidth;
const viewportWidth = window.innerWidth;
const tween = gsap.to(track, {
x: -(totalWidth - viewportWidth),
ease: "none",
scrollTrigger: {
trigger: container,
pin: true,
scrub: 1,
end: () => `+=${totalWidth - viewportWidth}`,
invalidateOnRefresh: true,
},
});
// Animate individual items as they come into view
const items = track.querySelectorAll("[data-scroll-item]");
for (const item of items) {
gsap.fromTo(
item,
{
opacity: 0.3,
scale: 0.9,
rotateY: -8,
transformPerspective: 1000,
},
{
opacity: 1,
scale: 1,
rotateY: 0,
scrollTrigger: {
trigger: item,
containerAnimation: tween,
start: "left 80%",
end: "left 30%",
scrub: true,
},
}
);
}
cleanup = () => {
ScrollTrigger.getAll().forEach((st) => {
if (st.trigger === container || (st.vars as { containerAnimation?: unknown }).containerAnimation === tween) {
st.kill();
}
});
tween.kill();
};
};
void setup();
return () => {
cleanup?.();
};
}, []);
return (
<div ref={containerRef} className={`relative overflow-hidden ${className}`}>
<div
ref={trackRef}
className="flex items-stretch gap-sp-6 will-change-transform"
>
{children}
</div>
</div>
);
}
@@ -0,0 +1,82 @@
"use client";
import { useRef, useState, type ReactNode } from "react";
import { motion } from "framer-motion";
interface MagneticButtonProps {
children: ReactNode;
className?: string;
href?: string;
onClick?: () => void;
strength?: number;
as?: "a" | "button";
target?: string;
rel?: string;
download?: boolean;
"data-cursor"?: string;
}
export default function MagneticButton({
children,
className = "",
href,
onClick,
strength = 0.3,
as = "button",
target,
rel,
download,
"data-cursor": dataCursor,
}: MagneticButtonProps) {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouse = (e: React.MouseEvent) => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
setPosition({
x: (e.clientX - centerX) * strength,
y: (e.clientY - centerY) * strength,
});
};
const handleMouseLeave = () => {
setPosition({ x: 0, y: 0 });
};
const Tag = as === "a" ? motion.a : motion.button;
const linkProps = as === "a" ? { href, target, rel, download } : {};
return (
<div
ref={ref}
onMouseMove={handleMouse}
onMouseLeave={handleMouseLeave}
className="inline-block"
>
<Tag
className={className}
onClick={onClick}
data-cursor={dataCursor}
animate={{
x: position.x,
y: position.y,
}}
transition={{
type: "spring",
stiffness: 350,
damping: 15,
mass: 0.5,
}}
{...linkProps}
>
{children}
</Tag>
</div>
);
}
+161
View File
@@ -0,0 +1,161 @@
"use client";
import { useState, useEffect } from "react";
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);
useEffect(() => {
// 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);
}, []);
return (
<AnimatePresence>
{isVisible && (
<motion.div
className="fixed inset-0 z-[10000] flex items-center justify-center bg-[#0a0a0a]"
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>
);
}
@@ -0,0 +1,49 @@
"use client";
import { motion, useScroll, useSpring, useTransform } from "framer-motion";
export default function ScrollProgress() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
});
const opacity = useTransform(scrollYProgress, [0, 0.02, 0.98, 1], [0, 1, 1, 0]);
return (
<>
{/* Top progress bar */}
<motion.div
className="fixed left-0 right-0 top-0 z-[9995] h-[2px] origin-left bg-[var(--slash)]"
style={{ scaleX, opacity }}
/>
{/* Side scroll indicator */}
<motion.div
className="fixed right-6 top-1/2 z-[9994] hidden -translate-y-1/2 lg:block"
style={{ opacity }}
>
<div className="relative h-[120px] w-px bg-[rgba(255,255,255,0.08)]">
<motion.div
className="absolute left-0 top-0 w-full origin-top bg-[var(--slash)]"
style={{
scaleY: scrollYProgress,
height: "100%",
}}
/>
<motion.div
className="absolute left-1/2 -translate-x-1/2 rounded-full bg-[var(--slash)]"
style={{
width: 6,
height: 6,
top: useTransform(scrollYProgress, [0, 1], ["0%", "100%"]),
boxShadow: "0 0 10px rgba(204, 255, 0, 0.5)",
}}
/>
</div>
</motion.div>
</>
);
}
@@ -0,0 +1,98 @@
"use client";
import { useRef, useEffect, useState } from "react";
import {
motion,
useAnimationFrame,
useMotionValue,
useTransform,
useSpring,
useScroll,
} from "framer-motion";
interface ScrollVelocityTextProps {
text: string;
baseVelocity?: number;
className?: string;
}
function wrap(min: number, max: number, v: number) {
const rangeSize = max - min;
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
}
export default function ScrollVelocityText({
text,
baseVelocity = 2,
className = "",
}: ScrollVelocityTextProps) {
const baseX = useMotionValue(0);
const { scrollY } = useScroll();
const scrollVelocity = useMotionValue(0);
const smoothVelocity = useSpring(scrollVelocity, {
damping: 50,
stiffness: 300,
});
const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
clamp: false,
});
const [repetitions, setRepetitions] = useState(4);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleResize = () => {
setRepetitions(Math.ceil(window.innerWidth / 400) + 2);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
// Track scroll velocity
const prevScrollY = useRef(0);
useEffect(() => {
return scrollY.on("change", (latest) => {
const delta = latest - prevScrollY.current;
prevScrollY.current = latest;
scrollVelocity.set(Math.abs(delta));
});
}, [scrollY, scrollVelocity]);
const directionFactor = useRef(1);
const x = useTransform(baseX, (v) => `${wrap(-25, 0, v)}%`);
useAnimationFrame((_, delta) => {
let moveBy = directionFactor.current * baseVelocity * (delta / 1000);
const vf = velocityFactor.get();
if (vf < 0) {
directionFactor.current = -1;
} else if (vf > 0) {
directionFactor.current = 1;
}
moveBy += directionFactor.current * moveBy * vf;
baseX.set(baseX.get() + moveBy);
});
return (
<div
ref={containerRef}
className={`overflow-hidden whitespace-nowrap ${className}`}
>
<motion.div className="inline-flex" style={{ x }}>
{[...Array(repetitions)].map((_, i) => (
<span
key={i}
className="mr-8 inline-block font-display text-[clamp(3rem,10vw,8rem)] font-extrabold uppercase leading-none tracking-[-0.04em]"
>
{text}
<span className="mx-8 inline-block text-[var(--slash)]">&#x2022;</span>
</span>
))}
</motion.div>
</div>
);
}
+112
View File
@@ -0,0 +1,112 @@
"use client";
import { useRef, useEffect } from "react";
interface SectionDividerProps {
variant?: "gradient" | "pulse" | "waveform";
className?: string;
}
export default function SectionDivider({
variant = "gradient",
className = "",
}: SectionDividerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (variant !== "waveform") return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animId: number;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = 60;
};
resize();
window.addEventListener("resize", resize);
const render = () => {
animId = requestAnimationFrame(render);
const { width, height } = canvas;
const t = performance.now() * 0.001;
ctx.clearRect(0, 0, width, height);
// Draw multiple waveforms
for (let wave = 0; wave < 3; wave++) {
ctx.beginPath();
ctx.strokeStyle = `rgba(204, 255, 0, ${0.1 - wave * 0.03})`;
ctx.lineWidth = 1;
for (let x = 0; x < width; x++) {
const frequency = 0.005 + wave * 0.002;
const amplitude = 8 + wave * 4;
const speed = t * (0.5 + wave * 0.2);
const y =
height / 2 +
Math.sin(x * frequency + speed) * amplitude +
Math.sin(x * frequency * 2.3 + speed * 1.5) * (amplitude * 0.4);
if (x === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}
};
animId = requestAnimationFrame(render);
return () => {
cancelAnimationFrame(animId);
window.removeEventListener("resize", resize);
};
}, [variant]);
if (variant === "gradient") {
return (
<div className={`relative h-px w-full overflow-hidden ${className}`}>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-[var(--slash)] to-transparent opacity-30" />
<div
className="absolute inset-0 bg-gradient-to-r from-transparent via-[var(--slash)] to-transparent opacity-60"
style={{
animation: "shimmerDivider 3s ease-in-out infinite",
}}
/>
</div>
);
}
if (variant === "pulse") {
return (
<div className={`flex items-center justify-center gap-2 py-8 ${className}`}>
{[...Array(5)].map((_, i) => (
<div
key={i}
className="h-1 w-1 rounded-full bg-[var(--slash)]"
style={{
animation: `pulseDot 2s ease-in-out ${i * 0.15}s infinite`,
}}
/>
))}
</div>
);
}
// waveform
return (
<div className={`relative ${className}`}>
<canvas
ref={canvasRef}
className="w-full opacity-60"
style={{ height: 60 }}
aria-hidden="true"
/>
</div>
);
}