// ── PARTICLE CANVAS (must be first) ── const canvas = document.getElementById('bgCanvas'); const ctx = canvas.getContext('2d'); let W, H, mouseX = 0, mouseY = 0; const particles = []; function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; } resize(); window.addEventListener('resize', resize); // ── THEME TOGGLE ── const themeToggle = document.getElementById('themeToggle'); function updateParticleColors() { const s = getComputedStyle(document.body); const dot = s.getPropertyValue('--canvas-dot').trim(); const line = s.getPropertyValue('--canvas-line').trim(); particles.forEach(p => { p.accentColor = line || '124,106,255'; p.baseColor = dot || '255,255,255'; p.color = Math.random() > 0.7 ? p.accentColor : p.baseColor; }); } function setTheme(light) { document.documentElement.classList.toggle('light', light); document.body.classList.toggle('light', light); localStorage.setItem('theme', light ? 'light' : 'dark'); updateParticleColors(); } const saved = localStorage.getItem('theme'); const prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches; if (saved === 'light' || (!saved && prefersLight)) setTheme(true); themeToggle.addEventListener('click', () => setTheme(!document.body.classList.contains('light'))); // ── PARTICLES ── class Particle { constructor() { this.accentColor = '124,106,255'; this.baseColor = '255,255,255'; this.color = this.baseColor; this.reset(); } reset() { this.x = Math.random() * W; this.y = Math.random() * H; this.z = Math.random() * 3 + 0.5; this.vx = (Math.random() - 0.5) * 0.3; this.vy = (Math.random() - 0.5) * 0.3; this.r = Math.random() * 2 + 0.8; this.alpha = Math.random() * 0.55 + 0.15; this.color = Math.random() > 0.7 ? this.accentColor : this.baseColor; } update() { this.x += this.vx; this.y += this.vy; const dx = this.x - mouseX, dy = this.y - mouseY; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 150) { const f = (150 - dist) / 150; this.vx += (dx / dist) * f * 0.3; this.vy += (dy / dist) * f * 0.3; } this.vx *= 0.99; this.vy *= 0.99; if (this.x < -10 || this.x > W + 10 || this.y < -10 || this.y > H + 10) this.reset(); } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.r * this.z, 0, Math.PI * 2); ctx.fillStyle = `rgba(${this.color},${this.alpha})`; ctx.fill(); } } for (let i = 0; i < 280; i++) particles.push(new Particle()); updateParticleColors(); function drawLines() { const s = getComputedStyle(document.body); const lc = s.getPropertyValue('--canvas-line').trim() || '124,106,255'; for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 140) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.strokeStyle = `rgba(${lc},${0.08 * (1 - dist / 140)})`; ctx.lineWidth = 0.5; ctx.stroke(); } } } } (function animLoop() { ctx.clearRect(0, 0, W, H); particles.forEach(p => { p.update(); p.draw(); }); drawLines(); requestAnimationFrame(animLoop); })(); // ── CURSOR GLOW ── const glowEl = document.getElementById('cursorGlow'); document.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; glowEl.style.left = e.clientX + 'px'; glowEl.style.top = e.clientY + 'px'; }); // ── NAVBAR ── const navbar = document.getElementById('navbar'); window.addEventListener('scroll', () => navbar.classList.toggle('scrolled', window.scrollY > 20)); // ── MOBILE NAV ── const hamburger = document.getElementById('hamburger'); const navLinks = document.getElementById('navLinks'); hamburger.addEventListener('click', () => { hamburger.classList.toggle('active'); navLinks.classList.toggle('active'); }); document.querySelectorAll('.nav-links a').forEach(l => { l.addEventListener('click', () => { hamburger.classList.remove('active'); navLinks.classList.remove('active'); }); }); // ── TYPING ── const typedEl = document.getElementById('typed'); const words = ['web applications', 'AI systems', 'scalable backends', 'RAG pipelines', 'intelligent agents']; let wi = 0, ci = 0, deleting = false; function type() { const w = words[wi]; if (deleting) { typedEl.textContent = w.substring(0, ci - 1); ci--; } else { typedEl.textContent = w.substring(0, ci + 1); ci++; } let wait = deleting ? 40 : 80; if (!deleting && ci === w.length) { wait = 2000; deleting = true; } else if (deleting && ci === 0) { deleting = false; wi = (wi + 1) % words.length; wait = 300; } setTimeout(type, wait); } type(); // ── COUNTERS ── let countersDone = false; function animateCounters() { if (countersDone) return; countersDone = true; document.querySelectorAll('.counter').forEach(el => { const target = parseFloat(el.dataset.target); const dec = el.dataset.decimal === 'true'; const start = performance.now(); function tick(now) { const p = Math.min((now - start) / 1500, 1); const ease = 1 - Math.pow(1 - p, 3); el.textContent = dec ? (ease * target).toFixed(2) : Math.floor(ease * target).toLocaleString(); if (p < 1) requestAnimationFrame(tick); } requestAnimationFrame(tick); }); } setTimeout(animateCounters, 600); // ── 3D TILT ── document.querySelectorAll('[data-tilt]').forEach(card => { card.addEventListener('mousemove', e => { const r = card.getBoundingClientRect(); const x = e.clientX - r.left, y = e.clientY - r.top; const rx = ((y - r.height / 2) / (r.height / 2)) * -8; const ry = ((x - r.width / 2) / (r.width / 2)) * 8; card.style.transform = `perspective(600px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(10px) scale(1.02)`; const g = card.querySelector('.proj-glow'); if (g) { g.style.setProperty('--mx', (x / r.width * 100) + '%'); g.style.setProperty('--my', (y / r.height * 100) + '%'); } }); card.addEventListener('mouseleave', () => { card.style.transform = 'perspective(600px) rotateX(0) rotateY(0) translateZ(0) scale(1)'; }); }); // ── REVEAL ON SCROLL ── const revealEls = document.querySelectorAll('.reveal-3d'); revealEls.forEach(el => el.classList.add('hidden-init')); const revealObs = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.remove('hidden-init'); revealObs.unobserve(entry.target); } }); }, { threshold: 0.05, rootMargin: '50px 0px 0px 0px' }); revealEls.forEach(el => { const rect = el.getBoundingClientRect(); if (rect.top < window.innerHeight + 50) el.classList.remove('hidden-init'); else revealObs.observe(el); }); // ── PARALLAX ── document.addEventListener('mousemove', e => { const mx = (e.clientX / window.innerWidth - 0.5) * 2; const my = (e.clientY / window.innerHeight - 0.5) * 2; document.querySelectorAll('.floating-shape').forEach((s, i) => { const d = (i + 1) * 8; s.style.marginLeft = mx * d + 'px'; s.style.marginTop = my * d + 'px'; }); }); // ── SMOOTH SCROLL ── document.querySelectorAll('a[href^="#"]').forEach(a => { a.addEventListener('click', e => { e.preventDefault(); const t = document.querySelector(a.getAttribute('href')); if (t) t.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }); // ── SCROLL PROGRESS BAR ── const scrollProgress = document.getElementById('scrollProgress'); window.addEventListener('scroll', () => { const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; const progress = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0; scrollProgress.style.width = progress + '%'; }); // ── ACTIVE NAV HIGHLIGHT ── const sections = document.querySelectorAll('section[id]'); const navLinksAll = document.querySelectorAll('.nav-links a'); const navObs = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { const id = entry.target.getAttribute('id'); navLinksAll.forEach(link => { link.classList.toggle('active', link.getAttribute('href') === '#' + id); }); } }); }, { threshold: 0.3, rootMargin: '-64px 0px -50% 0px' }); sections.forEach(s => navObs.observe(s));