// ── Hero typewriter ──────────────────────────────────────────── function startHeroTypewriter(nameEl, fullName, restEls) { const cursor = document.createElement('span'); cursor.className = 'hero-cursor'; nameEl.appendChild(cursor); let i = 0; function typeNext() { if (i < fullName.length) { nameEl.insertBefore(document.createTextNode(fullName[i]), cursor); i++; setTimeout(typeNext, 95); } else { // Cursor done — fade it out, then reveal the rest cursor.style.transition = 'opacity 0.3s'; cursor.style.opacity = '0'; setTimeout(() => { cursor.remove(); restEls.forEach((el, idx) => { setTimeout(() => el.classList.add('visible'), idx * 110); }); }, 0); } } typeNext(); } // ── Loader ──────────────────────────────────────────────────── (function () { const loader = document.getElementById('loader'); const pctEl = document.getElementById('loaderPct'); const nameEl = document.querySelector('.hero-name'); const restEls = [...document.querySelectorAll('.hero-role, .hero-pronouns, .hero-links, .hero-visual')]; // Store the name and clear it immediately (loader covers the page anyway) const fullName = nameEl.textContent.trim(); nameEl.textContent = ''; // Hide hero elements that reveal after typing restEls.forEach(el => el.classList.add('fade-up')); let pct = 0; const tick = setInterval(() => { pct += Math.random() * 15; if (pct >= 100) { pct = 100; clearInterval(tick); setTimeout(() => { loader.classList.add('hide'); // Wait for the loader fade-out (0.6s), then start typing setTimeout(() => startHeroTypewriter(nameEl, fullName, restEls), 600); }, 300); } pctEl.textContent = Math.floor(pct) + '%'; }, 110); })(); // ── Sprinkle background ──────────────────────────── (function () { const canvas = document.getElementById('bgCanvas'); const ctx = canvas.getContext('2d'); const COLORS = ['#e63946', '#f4722b', '#ffbe0b', '#ff006e', '#e07b39', '#ffd166']; const COUNT = 1000; // number of sprinkles const MIN_LEN = 12; // px const MAX_LEN = 18; // px const MIN_W = 4; const MAX_W = 6; let W, H, sprinkles; function rand(a, b) { return a + Math.random() * (b - a); } function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; if (!sprinkles) init(); // Redistribute any sprinkles that landed outside new bounds sprinkles.forEach(s => { if (s.x > W) s.x = rand(0, W); if (s.y > H) s.y = rand(0, H); }); } function init() { sprinkles = []; for (let i = 0; i < COUNT; i++) { sprinkles.push(makeSprinkle()); } } function makeSprinkle() { return { x: rand(0, W), y: rand(0, H), len: rand(MIN_LEN, MAX_LEN), width: rand(MIN_W, MAX_W), color: COLORS[Math.floor(Math.random() * COLORS.length)], angle: rand(0, Math.PI * 2), // very slow drift vx: rand(-0.1, 0.1), vy: rand(-0.1, 0.1), va: rand(-0.005, 0.005), // angular velocity alpha: 1, }; } function draw() { ctx.clearRect(0, 0, W, H); sprinkles.forEach(s => { ctx.save(); ctx.translate(s.x, s.y); ctx.rotate(s.angle); ctx.beginPath(); ctx.moveTo(0, -s.len / 2); ctx.lineTo(0, s.len / 2); ctx.strokeStyle = s.color; ctx.lineWidth = s.width; ctx.lineCap = 'round'; ctx.globalAlpha = s.alpha; ctx.stroke(); ctx.restore(); // Drift s.x += s.vx; s.y += s.vy; if (s.vx > 0.1 || s.vx < -0.1) { s.vx *= 0.98; } if (s.vy > 0.1 || s.vy < -0.1) { s.vy *= 0.98; } s.angle += s.va; // Repel from cursor const dx = s.x - mouseX; const dy = s.y - mouseY; const dist = Math.sqrt(dx * dx + dy * dy); const repelRadius = 80; if (dist < repelRadius && dist > 0) { const force = (repelRadius - dist) / repelRadius; s.vx += (dx / dist) * force * 1.0; s.vy += (dy / dist) * force * 1.0; } // Wrap: when a sprinkle drifts off the screen, reappear on the other side if (s.y < -MAX_LEN) s.y = H + MAX_LEN; if (s.y > H + MAX_LEN) s.y = -MAX_LEN if (s.x < -MAX_LEN) s.x = W + MAX_LEN; if (s.x > W + MAX_LEN) s.x = -MAX_LEN; }); requestAnimationFrame(draw); } window.addEventListener('resize', resize, { passive: true }); resize(); let mouseX = -9999, mouseY = -9999; window.addEventListener('mousemove', (e) => { mouseX = e.clientX; mouseY = e.clientY; }, { passive: true }); requestAnimationFrame(draw); })(); // ── Nav: scroll state & active link ─────────────────────────── (function () { const header = document.getElementById('header'); const links = document.querySelectorAll('.nav-link'); const sections = document.querySelectorAll('section[id]'); const burger = document.getElementById('burger'); window.addEventListener('scroll', () => { header.classList.toggle('scrolled', window.scrollY > 24 || burger.classList.contains('open')); let current = ''; sections.forEach(s => { if (window.scrollY >= s.offsetTop - 100) current = s.id; }); links.forEach(l => { l.classList.toggle('active', l.getAttribute('href') === '#' + current); }); }, { passive: true }); })(); // ── Mobile burger ────────────────────────────────────────────── (function () { const burger = document.getElementById('burger'); const navLinks = document.getElementById('navLinks'); const header = document.getElementById('header'); burger.addEventListener('click', () => { burger.classList.toggle('open'); navLinks.classList.toggle('open'); header.classList.toggle('scrolled', burger.classList.contains('open') || window.scrollY > 24); }); navLinks.querySelectorAll('a').forEach(a => { a.addEventListener('click', () => { burger.classList.remove('open'); navLinks.classList.remove('open'); header.classList.toggle('scrolled', window.scrollY > 24); }); }); })(); // ── Scroll fade-in ───────────────────────────────────────────── (function () { const targets = document.querySelectorAll( '.section-header, .about-grid, .skill-card, .timeline-item, ' + '.project-card, .edu-card' ); targets.forEach(el => el.classList.add('fade-up')); const observer = new IntersectionObserver((entries) => { entries.forEach(e => { if (!e.isIntersecting) return; const siblings = [...e.target.parentElement.querySelectorAll('.fade-up')]; const idx = siblings.indexOf(e.target); setTimeout(() => e.target.classList.add('visible'), idx * 80); observer.unobserve(e.target); }); }, { threshold: 0.1 }); targets.forEach(el => observer.observe(el)); })();