diff --git a/js/main.js b/js/main.js index ce8ecc7..c436b01 100644 --- a/js/main.js +++ b/js/main.js @@ -29,9 +29,99 @@ document.addEventListener('DOMContentLoaded', () => { .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; }); - // ── 4. CUSTOM CURSOR ────────────────────────────────────────────── + 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'); @@ -61,13 +151,13 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // ── 5. SCROLL PROGRESS BAR ──────────────────────────────────────── + // ── 6. SCROLL PROGRESS BAR ──────────────────────────────────────── const progressBar = document.getElementById('progress-bar'); lenis.on('scroll', ({ progress }) => { if (progressBar) progressBar.style.width = `${progress * 100}%`; }); - // ── 6. ACTIVE NAV LINK ──────────────────────────────────────────── + // ── 7. ACTIVE NAV LINK ──────────────────────────────────────────── const navLinks = document.querySelectorAll('.nav-link'); const sections = document.querySelectorAll('section[id]'); @@ -84,7 +174,7 @@ document.addEventListener('DOMContentLoaded', () => { sections.forEach(s => sectionObserver.observe(s)); - // ── 7. MOBILE MENU ──────────────────────────────────────────────── + // ── 8. MOBILE MENU ──────────────────────────────────────────────── const hamburger = document.getElementById('hamburger'); const mobileOverlay = document.getElementById('mobile-overlay'); @@ -105,7 +195,7 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // ── 8. REVEAL TEXT ANIMATIONS ───────────────────────────────────── + // ── 9. REVEAL TEXT ANIMATIONS ───────────────────────────────────── gsap.utils.toArray('.reveal-text').forEach(el => { gsap.from(el, { scrollTrigger: { trigger: el, start: 'top 85%' }, @@ -116,7 +206,7 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // ── 9. CV STAGGERED ANIMATIONS ──────────────────────────────────── + // ── 10. CV STAGGERED ANIMATIONS ──────────────────────────────────── gsap.utils.toArray('.cv-column').forEach(col => { const items = col.querySelectorAll('.group'); if (!items.length) return; @@ -130,7 +220,7 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // ── 10. PARALLAX IMAGES ─────────────────────────────────────────── + // ── 11. PARALLAX IMAGES ─────────────────────────────────────────── gsap.utils.toArray('.parallax-img').forEach(img => { gsap.to(img, { scrollTrigger: { @@ -144,7 +234,7 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // ── 11. DRAGGABLE GALLERY ───────────────────────────────────────── + // ── 12. DRAGGABLE GALLERY ───────────────────────────────────────── const gallery = document.getElementById('gallery'); if (gallery) { let isDown = false, startX, scrollLeft;