276 lines
12 KiB
JavaScript
276 lines
12 KiB
JavaScript
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;
|
|
});
|
|
}
|
|
|
|
});
|