Compare commits

...

4 commits

Author SHA1 Message Date
3f1ec3056d
Added particle effects to the header 2026-02-23 00:03:37 +01:00
fb253d9c6d
Edited style.css 2026-02-23 00:02:09 +01:00
f945905880
Edited index.html 2026-02-23 00:01:57 +01:00
17f271574f
Edited main.js 2026-02-23 00:01:42 +01:00
3 changed files with 379 additions and 181 deletions

View file

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

View file

@ -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>

View file

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