document.addEventListener('DOMContentLoaded', () => { // ── 1. LENIS SMOOTH SCROLL ──────────────────────────────────────── const lenis = new Lenis({ duration: 1.2, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), smooth: true, mouseMultiplier: 1, smoothTouch: false, touchMultiplier: 2, }); function raf(time) { lenis.raf(time); requestAnimationFrame(raf); } requestAnimationFrame(raf); // ── 2. GSAP PLUGINS ─────────────────────────────────────────────── gsap.registerPlugin(ScrollTrigger); // ── 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 cursorDot = document.getElementById('cursor-dot'); let mouseX = 0, mouseY = 0, cursorX = 0, cursorY = 0; document.addEventListener('mousemove', (e) => { mouseX = e.clientX; mouseY = e.clientY; gsap.to(cursorDot, { x: mouseX, y: mouseY, duration: 0 }); }); gsap.ticker.add(() => { cursorX += (mouseX - cursorX) * 0.2; cursorY += (mouseY - cursorY) * 0.2; gsap.set(cursor, { x: cursorX, y: cursorY }); }); document.querySelectorAll('.hover-target').forEach(target => { target.addEventListener('mouseenter', () => { gsap.to(cursor, { scale: 2.5, backgroundColor: 'rgba(255,255,255,1)', duration: 0.3 }); gsap.to(cursorDot, { opacity: 0, duration: 0.3 }); }); target.addEventListener('mouseleave', () => { gsap.to(cursor, { scale: 1, backgroundColor: 'transparent', duration: 0.3 }); gsap.to(cursorDot, { opacity: 1, duration: 0.3 }); }); }); } // ── 6. SCROLL PROGRESS BAR ──────────────────────────────────────── const progressBar = document.getElementById('progress-bar'); lenis.on('scroll', ({ progress }) => { if (progressBar) progressBar.style.width = `${progress * 100}%`; }); // ── 7. ACTIVE NAV LINK ──────────────────────────────────────────── const navLinks = document.querySelectorAll('.nav-link'); const sections = document.querySelectorAll('section[id]'); const sectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const id = entry.target.getAttribute('id'); navLinks.forEach(link => { link.classList.toggle('active', link.dataset.section === id); }); } }); }, { 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(); }); }); } // ── 9. REVEAL TEXT ANIMATIONS ───────────────────────────────────── gsap.utils.toArray('.reveal-text').forEach(el => { gsap.from(el, { scrollTrigger: { trigger: el, start: 'top 85%' }, y: 60, opacity: 0, duration: 1.2, ease: 'power3.out', }); }); // ── 10. CV STAGGERED ANIMATIONS ──────────────────────────────────── gsap.utils.toArray('.cv-column').forEach(col => { 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, { scrollTrigger: { trigger: img.parentElement, start: 'top bottom', end: 'bottom top', scrub: true, }, y: 50, ease: 'none', }); }); // ── 12. CV DOWNLOAD MODAL ─────────────────────────────────────── const cvModal = document.getElementById('cv-modal'); const cvClose = document.getElementById('cv-modal-close'); const cvBackdrop = document.getElementById('cv-modal-backdrop'); function openCvModal() { cvModal.classList.add('is-open'); } function closeCvModal() { cvModal.classList.remove('is-open'); } document.querySelectorAll('.cv-download-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); openCvModal(); }); }); if (cvClose) cvClose.addEventListener('click', closeCvModal); if (cvBackdrop) cvBackdrop.addEventListener('click', closeCvModal); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeCvModal(); }); // Auto-close modal after a language link is clicked document.querySelectorAll('.cv-lang-btn').forEach(btn => { btn.addEventListener('click', () => setTimeout(closeCvModal, 400)); }); // ── 13. DRAGGABLE GALLERY ───────────────────────────────────────── const gallery = document.getElementById('gallery'); if (gallery) { let isDown = false, startX, scrollLeft; gallery.addEventListener('mousedown', (e) => { isDown = true; startX = e.pageX - gallery.offsetLeft; scrollLeft = gallery.scrollLeft; }); gallery.addEventListener('mouseleave', () => { isDown = false; }); gallery.addEventListener('mouseup', () => { isDown = false; }); gallery.addEventListener('mousemove', (e) => { if (!isDown) return; e.preventDefault(); gallery.scrollLeft = scrollLeft - (e.pageX - gallery.offsetLeft - startX) * 2; }); } });