Compare commits
4 commits
0bb49cb169
...
3f1ec3056d
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f1ec3056d | |||
| fb253d9c6d | |||
| f945905880 | |||
| 17f271574f |
3 changed files with 379 additions and 181 deletions
105
css/style.css
105
css/style.css
|
|
@ -49,6 +49,14 @@ html.lenis {
|
||||||
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
|
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hero entrance elements — hidden before preloader reveals them */
|
||||||
|
.hero-title,
|
||||||
|
.hero-subtitle {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(60px);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
/* Gallery Scroll */
|
/* Gallery Scroll */
|
||||||
.gallery-wrapper::-webkit-scrollbar {
|
.gallery-wrapper::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -57,3 +65,100 @@ html.lenis {
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════
|
||||||
|
GRAIN / FILM OVERLAY
|
||||||
|
═══════════════════════════ */
|
||||||
|
.grain-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 500;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.045;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 180px 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════
|
||||||
|
SCROLL PROGRESS BAR
|
||||||
|
═══════════════════════════ */
|
||||||
|
#progress-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 0%;
|
||||||
|
background: linear-gradient(90deg, #3b82f6, #93c5fd);
|
||||||
|
z-index: 600;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: width 0.05s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════
|
||||||
|
ACTIVE NAV LINK
|
||||||
|
═══════════════════════════ */
|
||||||
|
.nav-link {
|
||||||
|
position: relative;
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
.nav-link::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -4px;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
transition: width 0.4s cubic-bezier(0.76, 0, 0.24, 1);
|
||||||
|
}
|
||||||
|
.nav-link.active {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.nav-link.active::after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════
|
||||||
|
MOBILE HAMBURGER & OVERLAY
|
||||||
|
═══════════════════════════ */
|
||||||
|
#mobile-overlay.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#hamburger.open .ham-line:first-child {
|
||||||
|
transform: translateY(4px) rotate(45deg);
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
#hamburger.open .ham-line:last-child {
|
||||||
|
transform: translateY(-4px) rotate(-45deg);
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Staggered mobile link entrance */
|
||||||
|
#mobile-overlay .mobile-nav-link {
|
||||||
|
transform: translateX(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: transform 0.6s cubic-bezier(0.76, 0, 0.24, 1), opacity 0.6s ease;
|
||||||
|
}
|
||||||
|
#mobile-overlay.open .mobile-nav-link { transform: translateX(0); opacity: 1; }
|
||||||
|
#mobile-overlay.open .mobile-nav-link:nth-child(1) { transition-delay: 0.15s; }
|
||||||
|
#mobile-overlay.open .mobile-nav-link:nth-child(2) { transition-delay: 0.20s; }
|
||||||
|
#mobile-overlay.open .mobile-nav-link:nth-child(3) { transition-delay: 0.25s; }
|
||||||
|
#mobile-overlay.open .mobile-nav-link:nth-child(4) { transition-delay: 0.30s; }
|
||||||
|
#mobile-overlay.open .mobile-nav-link:nth-child(5) { transition-delay: 0.35s; }
|
||||||
|
|
||||||
|
/* ═══════════════════════════
|
||||||
|
EDITORIAL SECTION NUMBERS
|
||||||
|
═══════════════════════════ */
|
||||||
|
.section-num {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
color: #3b82f6;
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
|
||||||
99
index.html
99
index.html
|
|
@ -30,9 +30,16 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@400,500,600,700&display=swap" rel="stylesheet">
|
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@400,500,600,700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-dark text-white font-sans antialiased overflow-x-hidden">
|
<body class="bg-dark text-white font-sans antialiased overflow-x-hidden">
|
||||||
|
|
||||||
|
<!-- Grain Overlay -->
|
||||||
|
<div class="grain-overlay" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<!-- Scroll Progress Bar -->
|
||||||
|
<div id="progress-bar" aria-hidden="true"></div>
|
||||||
|
|
||||||
<!-- Preloader -->
|
<!-- Preloader -->
|
||||||
<div id="preloader" class="fixed inset-0 z-[999] bg-dark flex items-center justify-center">
|
<div id="preloader" class="fixed inset-0 z-[999] bg-dark flex items-center justify-center">
|
||||||
<div class="text-4xl md:text-6xl font-display font-bold overflow-hidden">
|
<div class="text-4xl md:text-6xl font-display font-bold overflow-hidden">
|
||||||
|
|
@ -45,31 +52,64 @@
|
||||||
<div id="cursor-dot" class="hidden md:block fixed top-0 left-0 w-1 h-1 bg-white rounded-full pointer-events-none z-[1000] mix-blend-difference -translate-x-1/2 -translate-y-1/2"></div>
|
<div id="cursor-dot" class="hidden md:block fixed top-0 left-0 w-1 h-1 bg-white rounded-full pointer-events-none z-[1000] mix-blend-difference -translate-x-1/2 -translate-y-1/2"></div>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="fixed w-full z-50 mix-blend-difference p-6 md:p-10 flex justify-between items-center">
|
<nav class="fixed w-full z-[100] mix-blend-difference p-6 md:p-10 flex justify-between items-center">
|
||||||
<div class="font-display font-bold text-2xl tracking-tighter uppercase">
|
<div class="font-display font-bold text-2xl tracking-tighter uppercase">
|
||||||
<a href="#home" class="hover-target">PLEXI09</a>
|
<a href="#about" class="hover-target">PLEXI09</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:flex space-x-8 font-medium text-sm uppercase tracking-widest">
|
<div class="hidden md:flex space-x-8 font-medium text-sm uppercase tracking-widest">
|
||||||
<a href="#about" class="hover-target hover:text-gray-400 transition">About</a>
|
<a href="#about" data-section="about" class="nav-link hover-target transition">About</a>
|
||||||
<a href="#cv" class="hover-target hover:text-gray-400 transition">CV</a>
|
<a href="#cv" data-section="cv" class="nav-link hover-target transition">CV</a>
|
||||||
<a href="#projects" class="hover-target hover:text-gray-400 transition">Projects</a>
|
<a href="#projects" data-section="projects" class="nav-link hover-target transition">Projects</a>
|
||||||
<a href="#photography" class="hover-target hover:text-gray-400 transition">Photography</a>
|
<a href="#photography" data-section="photography" class="nav-link hover-target transition">Photography</a>
|
||||||
<a href="#contact" class="hover-target hover:text-gray-400 transition">Contact</a>
|
<a href="#contact" data-section="contact" class="nav-link hover-target transition">Contact</a>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="hamburger" class="md:hidden flex flex-col justify-center items-end gap-[7px] w-8 h-8 hover-target" aria-label="Toggle menu">
|
||||||
|
<span class="ham-line block w-6 h-px bg-white transition-all duration-500"></span>
|
||||||
|
<span class="ham-line block w-4 h-px bg-white transition-all duration-500"></span>
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section id="home" class="relative h-screen w-full flex items-center justify-center overflow-hidden">
|
||||||
|
<canvas id="hero-canvas" class="absolute inset-0 z-0 opacity-60"></canvas>
|
||||||
|
<div class="relative z-10 text-center pointer-events-none">
|
||||||
|
<h1 class="text-[12vw] leading-none font-display font-bold uppercase tracking-tighter mix-blend-difference hero-title">
|
||||||
|
Developer
|
||||||
|
</h1>
|
||||||
|
<h1 class="text-[12vw] leading-none font-display font-bold uppercase tracking-tighter mix-blend-difference text-transparent stroke-text hero-title">
|
||||||
|
Photographer
|
||||||
|
</h1>
|
||||||
|
<p class="mt-8 text-xl md:text-2xl font-light tracking-wide opacity-0 hero-subtitle">
|
||||||
|
Crafting digital experiences & capturing light.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-10 left-1/2 -translate-x-1/2 text-sm uppercase tracking-widest opacity-50 animate-pulse">
|
||||||
|
Scroll to explore
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Overlay -->
|
||||||
|
<div id="mobile-overlay" class="fixed inset-0 z-[90] bg-[#0a0a0a] flex flex-col justify-center px-10 translate-x-full transition-transform duration-700 ease-in-out">
|
||||||
|
<a href="#about" class="mobile-nav-link hover-target text-5xl sm:text-6xl font-display font-bold uppercase tracking-tighter mb-5 text-gray-700 hover:text-white transition-colors duration-300">01 — About</a>
|
||||||
|
<a href="#cv" class="mobile-nav-link hover-target text-5xl sm:text-6xl font-display font-bold uppercase tracking-tighter mb-5 text-gray-700 hover:text-white transition-colors duration-300">02 — CV</a>
|
||||||
|
<a href="#projects" class="mobile-nav-link hover-target text-5xl sm:text-6xl font-display font-bold uppercase tracking-tighter mb-5 text-gray-700 hover:text-white transition-colors duration-300">03 — Projects</a>
|
||||||
|
<a href="#photography" class="mobile-nav-link hover-target text-5xl sm:text-6xl font-display font-bold uppercase tracking-tighter mb-5 text-gray-700 hover:text-white transition-colors duration-300">04 — Photography</a>
|
||||||
|
<a href="#contact" class="mobile-nav-link hover-target text-5xl sm:text-6xl font-display font-bold uppercase tracking-tighter mb-5 text-gray-700 hover:text-white transition-colors duration-300">05 — Contact</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- About Section -->
|
<!-- About Section -->
|
||||||
<section id="about" class="py-32 px-6 md:px-20 relative z-10 bg-dark">
|
<section id="about" class="py-32 px-6 md:px-20 relative z-10 bg-dark">
|
||||||
<div class="max-w-3xl mx-auto">
|
<div class="max-w-3xl mx-auto">
|
||||||
<div class="space-y-8 reveal-text">
|
<div class="space-y-8">
|
||||||
<h2 class="text-5xl md:text-7xl font-display font-bold uppercase tracking-tighter">The Mind <br>Behind <span class="text-transparent stroke-text">Plexi09</span></h2>
|
<span class="section-num hero-title">01 / About</span>
|
||||||
<p class="text-xl text-gray-400 leading-relaxed font-light">
|
<h2 class="hero-title text-5xl md:text-7xl font-display font-bold uppercase tracking-tighter">The Mind <br>Behind <span class="text-transparent stroke-text">Plexi09</span></h2>
|
||||||
|
<p class="hero-subtitle text-xl text-gray-400 leading-relaxed font-light">
|
||||||
Hey 🖐️! <br>I am self-taught developer and passionate photographer. I believe in the power of open-source, self-hosting, and writing code that feels like poetry.
|
Hey 🖐️! <br>I am self-taught developer and passionate photographer. I believe in the power of open-source, self-hosting, and writing code that feels like poetry.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xl text-gray-400 leading-relaxed font-light">
|
<p class="hero-subtitle text-xl text-gray-400 leading-relaxed font-light">
|
||||||
When I'm not architecting systems or deploying containers, I'm out in the world, freezing moments in time through my lens.
|
When I'm not architecting systems or deploying containers, I'm out in the world, freezing moments in time through my lens.
|
||||||
</p>
|
</p>
|
||||||
<div class="pt-8 border-t border-gray-800 flex gap-12">
|
<div class="hero-subtitle pt-8 border-t border-gray-800 flex gap-12">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm uppercase tracking-widest text-gray-500 mb-2">Software stack</h4>
|
<h4 class="text-sm uppercase tracking-widest text-gray-500 mb-2">Software stack</h4>
|
||||||
<p class="font-medium">TS, React, Tailwin, Python, Java, Docker</p>
|
<p class="font-medium">TS, React, Tailwin, Python, Java, Docker</p>
|
||||||
|
|
@ -87,12 +127,13 @@
|
||||||
<section id="cv" class="py-32 px-6 md:px-20 bg-dark relative z-10 border-t border-gray-900">
|
<section id="cv" class="py-32 px-6 md:px-20 bg-dark relative z-10 border-t border-gray-900">
|
||||||
<div class="max-w-7xl mx-auto">
|
<div class="max-w-7xl mx-auto">
|
||||||
<div class="mb-20 reveal-text">
|
<div class="mb-20 reveal-text">
|
||||||
|
<span class="section-num">02 / Experience</span>
|
||||||
<h2 class="text-5xl md:text-7xl font-display font-bold uppercase tracking-tighter">Experience <br>& <span class="text-transparent stroke-text">Journey</span></h2>
|
<h2 class="text-5xl md:text-7xl font-display font-bold uppercase tracking-tighter">Experience <br>& <span class="text-transparent stroke-text">Journey</span></h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 gap-16">
|
<div class="grid md:grid-cols-2 gap-16">
|
||||||
<!-- Experience -->
|
<!-- Experience -->
|
||||||
<div>
|
<div class="cv-column">
|
||||||
<h3 class="text-2xl font-display font-bold uppercase tracking-widest mb-10 text-gray-500">Work</h3>
|
<h3 class="text-2xl font-display font-bold uppercase tracking-widest mb-10 text-gray-500">Work</h3>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="group p-6 -mx-6 rounded-2xl hover:bg-[#0f0f0f] transition-colors duration-500 hover-target">
|
<div class="group p-6 -mx-6 rounded-2xl hover:bg-[#0f0f0f] transition-colors duration-500 hover-target">
|
||||||
|
|
@ -123,7 +164,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Education -->
|
<!-- Education -->
|
||||||
<div>
|
<div class="cv-column">
|
||||||
<h3 class="text-2xl font-display font-bold uppercase tracking-widest mb-10 text-gray-500">Education</h3>
|
<h3 class="text-2xl font-display font-bold uppercase tracking-widest mb-10 text-gray-500">Education</h3>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="group p-6 -mx-6 rounded-2xl hover:bg-[#0f0f0f] transition-colors duration-500 hover-target">
|
<div class="group p-6 -mx-6 rounded-2xl hover:bg-[#0f0f0f] transition-colors duration-500 hover-target">
|
||||||
|
|
@ -152,7 +193,10 @@
|
||||||
<section id="projects" class="py-32 px-6 md:px-20 bg-[#0f0f0f]">
|
<section id="projects" class="py-32 px-6 md:px-20 bg-[#0f0f0f]">
|
||||||
<div class="max-w-7xl mx-auto">
|
<div class="max-w-7xl mx-auto">
|
||||||
<div class="flex justify-between items-end mb-20 reveal-text">
|
<div class="flex justify-between items-end mb-20 reveal-text">
|
||||||
|
<div>
|
||||||
|
<span class="section-num">03 / Creations</span>
|
||||||
<h2 class="text-5xl md:text-7xl font-display font-bold uppercase tracking-tighter">Creations</h2>
|
<h2 class="text-5xl md:text-7xl font-display font-bold uppercase tracking-tighter">Creations</h2>
|
||||||
|
</div>
|
||||||
<a href="https://git.plexi09.me" class="hidden md:inline-block text-sm uppercase tracking-widest border-b border-white pb-1 hover-target">View Forgejo</a>
|
<a href="https://git.plexi09.me" class="hidden md:inline-block text-sm uppercase tracking-widest border-b border-white pb-1 hover-target">View Forgejo</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -164,9 +208,9 @@
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-3xl font-display font-bold mb-4">Movie Match</h3>
|
<h3 class="text-3xl font-display font-bold mb-4">Movie Match</h3>
|
||||||
<p class="text-gray-400 mb-8 font-light">An application that helps couples and friends discover compatible movies to watch together.</p>
|
<p class="text-gray-400 mb-8 font-light">An application that helps couples and friends discover compatible movies to watch together.</p>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3 flex-wrap">
|
||||||
<span class="px-4 py-1 rounded-full border border-gray-700 text-xs uppercase tracking-wider">Typescript</span>
|
<span class="flex items-center gap-2 px-4 py-1 rounded-full border border-gray-700 text-xs uppercase tracking-wider"><i class="devicon-typescript-plain text-blue-400 text-base"></i> Typescript</span>
|
||||||
<span class="px-4 py-1 rounded-full border border-gray-700 text-xs uppercase tracking-wider">Docker</span>
|
<span class="flex items-center gap-2 px-4 py-1 rounded-full border border-gray-700 text-xs uppercase tracking-wider"><i class="devicon-docker-plain text-base" style="color:#2496ed"></i> Docker</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -176,6 +220,7 @@
|
||||||
<!-- Photography Section -->
|
<!-- Photography Section -->
|
||||||
<section id="photography" class="py-32 px-6 md:px-20 bg-dark overflow-hidden">
|
<section id="photography" class="py-32 px-6 md:px-20 bg-dark overflow-hidden">
|
||||||
<div class="max-w-7xl mx-auto mb-20 reveal-text">
|
<div class="max-w-7xl mx-auto mb-20 reveal-text">
|
||||||
|
<span class="section-num">04 / Photography</span>
|
||||||
<h2 class="text-5xl md:text-7xl font-display font-bold uppercase tracking-tighter">Through<br>The Lens</h2>
|
<h2 class="text-5xl md:text-7xl font-display font-bold uppercase tracking-tighter">Through<br>The Lens</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -222,6 +267,7 @@
|
||||||
<!-- Contact Section -->
|
<!-- Contact Section -->
|
||||||
<section id="contact" class="py-32 px-6 md:px-20 bg-[#0f0f0f] relative overflow-hidden">
|
<section id="contact" class="py-32 px-6 md:px-20 bg-[#0f0f0f] relative overflow-hidden">
|
||||||
<div class="max-w-4xl mx-auto text-center relative z-10">
|
<div class="max-w-4xl mx-auto text-center relative z-10">
|
||||||
|
<span class="section-num inline-block mb-6">05 / Contact</span>
|
||||||
<h2 class="text-[10vw] leading-none font-display font-bold uppercase tracking-tighter mb-12 hover-target">
|
<h2 class="text-[10vw] leading-none font-display font-bold uppercase tracking-tighter mb-12 hover-target">
|
||||||
<a href="mailto:hello@plexi09.me" class="inline-block hover:scale-110 hover:stroke-text transition-all duration-300">Let's Talk</a>
|
<a href="mailto:hello@plexi09.me" class="inline-block hover:scale-110 hover:stroke-text transition-all duration-300">Let's Talk</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
@ -240,6 +286,25 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-dark border-t border-gray-900 py-10 px-6 md:px-20">
|
||||||
|
<div class="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-6">
|
||||||
|
<div>
|
||||||
|
<span class="font-display font-bold text-xl tracking-tighter uppercase text-white">PLEXI09</span>
|
||||||
|
<p class="text-gray-600 text-sm mt-1 uppercase tracking-[0.2em]">Developer & Photographer</p>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:flex gap-10 text-xs uppercase tracking-widest text-gray-500">
|
||||||
|
<a href="#about" class="hover:text-white transition hover-target">About</a>
|
||||||
|
<a href="#cv" class="hover:text-white transition hover-target">CV</a>
|
||||||
|
<a href="#projects" class="hover:text-white transition hover-target">Projects</a>
|
||||||
|
<a href="#photography" class="hover:text-white transition hover-target">Photography</a>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="text-xs border border-gray-800 hover:border-white text-gray-500 hover:text-white transition-all px-5 py-2.5 rounded-full uppercase tracking-widest hover-target">
|
||||||
|
<i class="fas fa-download mr-2"></i> Download CV
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
334
js/main.js
334
js/main.js
|
|
@ -1,15 +1,13 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// 1. Initialize Lenis Smooth Scroll
|
|
||||||
|
// ── 1. LENIS SMOOTH SCROLL ────────────────────────────────────────
|
||||||
const lenis = new Lenis({
|
const lenis = new Lenis({
|
||||||
duration: 1.2,
|
duration: 1.2,
|
||||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||||
direction: 'vertical',
|
|
||||||
gestureDirection: 'vertical',
|
|
||||||
smooth: true,
|
smooth: true,
|
||||||
mouseMultiplier: 1,
|
mouseMultiplier: 1,
|
||||||
smoothTouch: false,
|
smoothTouch: false,
|
||||||
touchMultiplier: 2,
|
touchMultiplier: 2,
|
||||||
infinite: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function raf(time) {
|
function raf(time) {
|
||||||
|
|
@ -18,69 +16,130 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
requestAnimationFrame(raf);
|
requestAnimationFrame(raf);
|
||||||
|
|
||||||
// 2. Preloader Animation
|
// ── 2. GSAP PLUGINS ───────────────────────────────────────────────
|
||||||
const tl = gsap.timeline();
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
tl.to('#loader-text', {
|
|
||||||
y: 0,
|
|
||||||
duration: 1,
|
|
||||||
ease: 'power4.out',
|
|
||||||
delay: 0.2
|
|
||||||
})
|
|
||||||
.to('#loader-text', {
|
|
||||||
y: '-100%',
|
|
||||||
duration: 1,
|
|
||||||
ease: 'power4.in',
|
|
||||||
delay: 0.5
|
|
||||||
})
|
|
||||||
.to('#preloader', {
|
|
||||||
y: '-100%',
|
|
||||||
duration: 1,
|
|
||||||
ease: 'power4.inOut'
|
|
||||||
}, '-=0.5')
|
|
||||||
.from('.hero-title', {
|
|
||||||
y: 100,
|
|
||||||
opacity: 0,
|
|
||||||
duration: 1.5,
|
|
||||||
stagger: 0.2,
|
|
||||||
ease: 'power4.out'
|
|
||||||
}, '-=0.5')
|
|
||||||
.to('.hero-subtitle', {
|
|
||||||
opacity: 1,
|
|
||||||
duration: 1,
|
|
||||||
ease: 'power2.out'
|
|
||||||
}, '-=1');
|
|
||||||
|
|
||||||
// 3. Custom Cursor
|
// ── 3. PRELOADER ──────────────────────────────────────────────────
|
||||||
|
gsap.set('.hero-title, .hero-subtitle', { opacity: 0 });
|
||||||
|
|
||||||
|
gsap.timeline()
|
||||||
|
.to('#loader-text', { y: 0, duration: 1, ease: 'power4.out', delay: 0.2 })
|
||||||
|
.to('#loader-text', { y: '-100%', duration: 1, ease: 'power4.in', delay: 0.5 })
|
||||||
|
.to('#preloader', { y: '-100%', duration: 1, ease: 'power4.inOut' }, '-=0.5')
|
||||||
|
.to('.hero-title', { y: 0, opacity: 1, duration: 1.5, stagger: 0.15, ease: 'power4.out' }, '-=0.4')
|
||||||
|
.to('.hero-subtitle', { y: 0, opacity: 1, duration: 1.2, stagger: 0.12, ease: 'power3.out' }, '-=1.2');
|
||||||
|
|
||||||
|
// ── 4. CANVAS PARTICLES ───────────────────────────────────────────
|
||||||
|
const heroCanvas = document.getElementById('hero-canvas');
|
||||||
|
if (heroCanvas) {
|
||||||
|
const pCtx = heroCanvas.getContext('2d');
|
||||||
|
const heroSec = document.getElementById('home');
|
||||||
|
|
||||||
|
// Track mouse in viewport coords (works on all devices)
|
||||||
|
const pMouse = { x: null, y: null };
|
||||||
|
document.addEventListener('mousemove', (e) => { pMouse.x = e.clientX; pMouse.y = e.clientY; });
|
||||||
|
document.addEventListener('mouseleave', () => { pMouse.x = null; pMouse.y = null; });
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
heroCanvas.width = heroSec.offsetWidth;
|
||||||
|
heroCanvas.height = heroSec.offsetHeight;
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
resizeCanvas();
|
||||||
|
|
||||||
|
const PARTICLE_COUNT = 90;
|
||||||
|
const P2P_DIST = 130; // particle ↔ particle connection radius
|
||||||
|
const CUR_DIST = 200; // cursor ↔ particle connection radius
|
||||||
|
|
||||||
|
class Particle {
|
||||||
|
constructor() { this.init(); }
|
||||||
|
init() {
|
||||||
|
this.x = Math.random() * heroCanvas.width;
|
||||||
|
this.y = Math.random() * heroCanvas.height;
|
||||||
|
this.vx = (Math.random() - 0.5) * 0.45;
|
||||||
|
this.vy = (Math.random() - 0.5) * 0.45;
|
||||||
|
this.radius = Math.random() * 1.4 + 0.4;
|
||||||
|
this.alpha = Math.random() * 0.45 + 0.2;
|
||||||
|
}
|
||||||
|
update() {
|
||||||
|
this.x += this.vx;
|
||||||
|
this.y += this.vy;
|
||||||
|
if (this.x < 0 || this.x > heroCanvas.width) this.vx *= -1;
|
||||||
|
if (this.y < 0 || this.y > heroCanvas.height) this.vy *= -1;
|
||||||
|
}
|
||||||
|
draw() {
|
||||||
|
pCtx.beginPath();
|
||||||
|
pCtx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||||
|
pCtx.fillStyle = `rgba(255,255,255,${this.alpha})`;
|
||||||
|
pCtx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const particles = Array.from({ length: PARTICLE_COUNT }, () => new Particle());
|
||||||
|
|
||||||
|
function line(x1, y1, x2, y2, alpha) {
|
||||||
|
pCtx.beginPath();
|
||||||
|
pCtx.moveTo(x1, y1);
|
||||||
|
pCtx.lineTo(x2, y2);
|
||||||
|
pCtx.strokeStyle = `rgba(255,255,255,${alpha})`;
|
||||||
|
pCtx.lineWidth = 0.6;
|
||||||
|
pCtx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateParticles() {
|
||||||
|
pCtx.clearRect(0, 0, heroCanvas.width, heroCanvas.height);
|
||||||
|
|
||||||
|
// Cursor position relative to the hero section
|
||||||
|
const rect = heroSec.getBoundingClientRect();
|
||||||
|
const cx = pMouse.x !== null ? pMouse.x - rect.left : null;
|
||||||
|
const cy = pMouse.y !== null ? pMouse.y - rect.top : null;
|
||||||
|
|
||||||
|
for (let i = 0; i < particles.length; i++) {
|
||||||
|
const p = particles[i];
|
||||||
|
p.update();
|
||||||
|
p.draw();
|
||||||
|
|
||||||
|
// Particle ↔ particle
|
||||||
|
for (let j = i + 1; j < particles.length; j++) {
|
||||||
|
const q = particles[j];
|
||||||
|
const dx = p.x - q.x, dy = p.y - q.y;
|
||||||
|
const d = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (d < P2P_DIST) line(p.x, p.y, q.x, q.y, (1 - d / P2P_DIST) * 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor ↔ particle
|
||||||
|
if (cx !== null) {
|
||||||
|
const dx = p.x - cx, dy = p.y - cy;
|
||||||
|
const d = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (d < CUR_DIST) line(p.x, p.y, cx, cy, (1 - d / CUR_DIST) * 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animateParticles);
|
||||||
|
}
|
||||||
|
|
||||||
|
animateParticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5. CUSTOM CURSOR ──────────────────────────────────────────────
|
||||||
|
if (window.innerWidth >= 768) {
|
||||||
const cursor = document.getElementById('cursor');
|
const cursor = document.getElementById('cursor');
|
||||||
const cursorDot = document.getElementById('cursor-dot');
|
const cursorDot = document.getElementById('cursor-dot');
|
||||||
const hoverTargets = document.querySelectorAll('.hover-target');
|
let mouseX = 0, mouseY = 0, cursorX = 0, cursorY = 0;
|
||||||
|
|
||||||
if (window.innerWidth >= 768) {
|
|
||||||
let mouseX = 0;
|
|
||||||
let mouseY = 0;
|
|
||||||
let cursorX = 0;
|
|
||||||
let cursorY = 0;
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', (e) => {
|
document.addEventListener('mousemove', (e) => {
|
||||||
mouseX = e.clientX;
|
mouseX = e.clientX;
|
||||||
mouseY = e.clientY;
|
mouseY = e.clientY;
|
||||||
|
gsap.to(cursorDot, { x: mouseX, y: mouseY, duration: 0 });
|
||||||
// Instant dot
|
|
||||||
gsap.to(cursorDot, {
|
|
||||||
x: mouseX,
|
|
||||||
y: mouseY,
|
|
||||||
duration: 0
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Smooth outer circle
|
|
||||||
gsap.ticker.add(() => {
|
gsap.ticker.add(() => {
|
||||||
cursorX += (mouseX - cursorX) * 0.2;
|
cursorX += (mouseX - cursorX) * 0.2;
|
||||||
cursorY += (mouseY - cursorY) * 0.2;
|
cursorY += (mouseY - cursorY) * 0.2;
|
||||||
gsap.set(cursor, { x: cursorX, y: cursorY });
|
gsap.set(cursor, { x: cursorX, y: cursorY });
|
||||||
});
|
});
|
||||||
|
|
||||||
hoverTargets.forEach(target => {
|
document.querySelectorAll('.hover-target').forEach(target => {
|
||||||
target.addEventListener('mouseenter', () => {
|
target.addEventListener('mouseenter', () => {
|
||||||
gsap.to(cursor, { scale: 2.5, backgroundColor: 'rgba(255,255,255,1)', duration: 0.3 });
|
gsap.to(cursor, { scale: 2.5, backgroundColor: 'rgba(255,255,255,1)', duration: 0.3 });
|
||||||
gsap.to(cursorDot, { opacity: 0, duration: 0.3 });
|
gsap.to(cursorDot, { opacity: 0, duration: 0.3 });
|
||||||
|
|
@ -92,137 +151,106 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Interactive Canvas Background (Particles)
|
// ── 6. SCROLL PROGRESS BAR ────────────────────────────────────────
|
||||||
const canvas = document.getElementById('hero-canvas');
|
const progressBar = document.getElementById('progress-bar');
|
||||||
const ctx = canvas.getContext('2d');
|
lenis.on('scroll', ({ progress }) => {
|
||||||
let width, height;
|
if (progressBar) progressBar.style.width = `${progress * 100}%`;
|
||||||
let particles = [];
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
width = window.innerWidth;
|
|
||||||
height = window.innerHeight;
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', resize);
|
|
||||||
resize();
|
|
||||||
|
|
||||||
class Particle {
|
|
||||||
constructor() {
|
|
||||||
this.x = Math.random() * width;
|
|
||||||
this.y = Math.random() * height;
|
|
||||||
this.vx = (Math.random() - 0.5) * 0.5;
|
|
||||||
this.vy = (Math.random() - 0.5) * 0.5;
|
|
||||||
this.radius = Math.random() * 1.5 + 0.5;
|
|
||||||
}
|
|
||||||
update() {
|
|
||||||
this.x += this.vx;
|
|
||||||
this.y += this.vy;
|
|
||||||
if (this.x < 0 || this.x > width) this.vx *= -1;
|
|
||||||
if (this.y < 0 || this.y > height) this.vy *= -1;
|
|
||||||
}
|
|
||||||
draw() {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
particles.push(new Particle());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mouse = { x: null, y: null };
|
|
||||||
window.addEventListener('mousemove', (e) => {
|
|
||||||
mouse.x = e.clientX;
|
|
||||||
mouse.y = e.clientY;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function animateParticles() {
|
// ── 7. ACTIVE NAV LINK ────────────────────────────────────────────
|
||||||
ctx.clearRect(0, 0, width, height);
|
const navLinks = document.querySelectorAll('.nav-link');
|
||||||
|
const sections = document.querySelectorAll('section[id]');
|
||||||
|
|
||||||
particles.forEach(p => {
|
const sectionObserver = new IntersectionObserver((entries) => {
|
||||||
p.update();
|
entries.forEach(entry => {
|
||||||
p.draw();
|
if (entry.isIntersecting) {
|
||||||
|
const id = entry.target.getAttribute('id');
|
||||||
// Connect to mouse
|
navLinks.forEach(link => {
|
||||||
if (mouse.x != null) {
|
link.classList.toggle('active', link.dataset.section === id);
|
||||||
const dx = mouse.x - p.x;
|
});
|
||||||
const dy = mouse.y - p.y;
|
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (dist < 150) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(p.x, p.y);
|
|
||||||
ctx.lineTo(mouse.x, mouse.y);
|
|
||||||
ctx.strokeStyle = `rgba(255, 255, 255, ${1 - dist/150})`;
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
requestAnimationFrame(animateParticles);
|
}, { rootMargin: '-40% 0px -40% 0px', threshold: 0 });
|
||||||
|
|
||||||
|
sections.forEach(s => sectionObserver.observe(s));
|
||||||
|
|
||||||
|
// ── 8. MOBILE MENU ────────────────────────────────────────────────
|
||||||
|
const hamburger = document.getElementById('hamburger');
|
||||||
|
const mobileOverlay = document.getElementById('mobile-overlay');
|
||||||
|
|
||||||
|
if (hamburger && mobileOverlay) {
|
||||||
|
hamburger.addEventListener('click', () => {
|
||||||
|
const isOpen = hamburger.classList.contains('open');
|
||||||
|
hamburger.classList.toggle('open');
|
||||||
|
mobileOverlay.classList.toggle('open');
|
||||||
|
isOpen ? lenis.start() : lenis.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.mobile-nav-link').forEach(link => {
|
||||||
|
link.addEventListener('click', () => {
|
||||||
|
hamburger.classList.remove('open');
|
||||||
|
mobileOverlay.classList.remove('open');
|
||||||
|
lenis.start();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
animateParticles();
|
|
||||||
|
|
||||||
// 5. GSAP Scroll Animations
|
// ── 9. REVEAL TEXT ANIMATIONS ─────────────────────────────────────
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
gsap.utils.toArray('.reveal-text').forEach(el => {
|
||||||
|
gsap.from(el, {
|
||||||
// Reveal Text
|
scrollTrigger: { trigger: el, start: 'top 85%' },
|
||||||
const revealTexts = document.querySelectorAll('.reveal-text');
|
y: 60,
|
||||||
revealTexts.forEach(text => {
|
|
||||||
gsap.from(text, {
|
|
||||||
scrollTrigger: {
|
|
||||||
trigger: text,
|
|
||||||
start: 'top 80%',
|
|
||||||
},
|
|
||||||
y: 50,
|
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 1,
|
duration: 1.2,
|
||||||
ease: 'power3.out'
|
ease: 'power3.out',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parallax Images
|
// ── 10. CV STAGGERED ANIMATIONS ────────────────────────────────────
|
||||||
const parallaxImgs = document.querySelectorAll('.parallax-img');
|
gsap.utils.toArray('.cv-column').forEach(col => {
|
||||||
parallaxImgs.forEach(img => {
|
const items = col.querySelectorAll('.group');
|
||||||
|
if (!items.length) return;
|
||||||
|
gsap.from(items, {
|
||||||
|
scrollTrigger: { trigger: col, start: 'top 82%' },
|
||||||
|
y: 40,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.85,
|
||||||
|
stagger: 0.18,
|
||||||
|
ease: 'power3.out',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 11. PARALLAX IMAGES ───────────────────────────────────────────
|
||||||
|
gsap.utils.toArray('.parallax-img').forEach(img => {
|
||||||
gsap.to(img, {
|
gsap.to(img, {
|
||||||
scrollTrigger: {
|
scrollTrigger: {
|
||||||
trigger: img.parentElement,
|
trigger: img.parentElement,
|
||||||
start: 'top bottom',
|
start: 'top bottom',
|
||||||
end: 'bottom top',
|
end: 'bottom top',
|
||||||
scrub: true
|
scrub: true,
|
||||||
},
|
},
|
||||||
y: 50,
|
y: 50,
|
||||||
ease: 'none'
|
ease: 'none',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Horizontal Scroll for Gallery
|
// ── 12. DRAGGABLE GALLERY ─────────────────────────────────────────
|
||||||
const gallery = document.getElementById('gallery');
|
const gallery = document.getElementById('gallery');
|
||||||
let isDown = false;
|
if (gallery) {
|
||||||
let startX;
|
let isDown = false, startX, scrollLeft;
|
||||||
let scrollLeft;
|
|
||||||
|
|
||||||
gallery.addEventListener('mousedown', (e) => {
|
gallery.addEventListener('mousedown', (e) => {
|
||||||
isDown = true;
|
isDown = true;
|
||||||
gallery.classList.add('active');
|
|
||||||
startX = e.pageX - gallery.offsetLeft;
|
startX = e.pageX - gallery.offsetLeft;
|
||||||
scrollLeft = gallery.scrollLeft;
|
scrollLeft = gallery.scrollLeft;
|
||||||
});
|
});
|
||||||
gallery.addEventListener('mouseleave', () => {
|
gallery.addEventListener('mouseleave', () => { isDown = false; });
|
||||||
isDown = false;
|
gallery.addEventListener('mouseup', () => { isDown = false; });
|
||||||
gallery.classList.remove('active');
|
|
||||||
});
|
|
||||||
gallery.addEventListener('mouseup', () => {
|
|
||||||
isDown = false;
|
|
||||||
gallery.classList.remove('active');
|
|
||||||
});
|
|
||||||
gallery.addEventListener('mousemove', (e) => {
|
gallery.addEventListener('mousemove', (e) => {
|
||||||
if (!isDown) return;
|
if (!isDown) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const x = e.pageX - gallery.offsetLeft;
|
gallery.scrollLeft = scrollLeft - (e.pageX - gallery.offsetLeft - startX) * 2;
|
||||||
const walk = (x - startX) * 2; // Scroll-fast
|
|
||||||
gallery.scrollLeft = scrollLeft - walk;
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue