| |
| 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); |
|
|
| |
| 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'))); |
|
|
| |
| 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); |
| })(); |
|
|
| |
| 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'; |
| }); |
|
|
| |
| const navbar = document.getElementById('navbar'); |
| window.addEventListener('scroll', () => navbar.classList.toggle('scrolled', window.scrollY > 20)); |
|
|
| |
| 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'); |
| }); |
| }); |
|
|
| |
| 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(); |
|
|
| |
| 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); |
|
|
| |
| 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)'; |
| }); |
| }); |
|
|
| |
| 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); |
| }); |
|
|
| |
| 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'; |
| }); |
| }); |
|
|
| |
| 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' }); |
| }); |
| }); |
|
|
| |
| 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 + '%'; |
| }); |
|
|
| |
| 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)); |
|
|