// ===== TYPEWRITER EFFECT ===== function initTypewriter() { const el = document.querySelector('.typewriter'); if (!el) return; const text = el.getAttribute('data-text'); let i = 0; el.textContent = ''; el.style.opacity = '1'; function type() { if (i < text.length) { el.textContent += text.charAt(i); i++; setTimeout(type, 80); } } setTimeout(type, 500); } // ===== SCROLL PROGRESS BAR ===== function initScrollProgress() { const progressBar = document.querySelector('.scroll-progress'); if (!progressBar) return; function updateProgress() { const docHeight = document.documentElement.scrollHeight - window.innerHeight; const scrolled = window.scrollY; const progress = (scrolled / docHeight) * 100; progressBar.style.setProperty('--scroll-progress', `${progress}%`); progressBar.setAttribute('aria-valuenow', Math.round(progress)); } window.addEventListener('scroll', updateProgress, { passive: true }); updateProgress(); } // ===== ANIMATED SKILLS BARS ===== function initSkillBars() { const skillItems = document.querySelectorAll('.skill-item'); if (!skillItems.length) return; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animate'); observer.unobserve(entry.target); } }); }, { threshold: 0.3 }); skillItems.forEach(item => observer.observe(item)); } // ===== PARTICLE BACKGROUND ===== function initParticles() { const canvas = document.getElementById('particles'); if (!canvas) return; const ctx = canvas.getContext('2d'); let particles = []; let animationId; function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } class Particle { constructor() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.vx = (Math.random() - 0.5) * 0.3; this.vy = (Math.random() - 0.5) * 0.3; this.radius = Math.random() * 1.5 + 0.5; this.opacity = Math.random() * 0.3 + 0.1; } update() { this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > canvas.width) this.vx *= -1; if (this.y < 0 || this.y > canvas.height) this.vy *= -1; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = `rgba(111, 78, 55, ${this.opacity})`; ctx.fill(); } } function init() { resize(); particles = []; const particleCount = Math.floor((canvas.width * canvas.height) / 15000); for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } } function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); animationId = requestAnimationFrame(animate); } window.addEventListener('resize', () => { cancelAnimationFrame(animationId); init(); animate(); }); init(); animate(); } // ===== INTERACTIVE TESTIMONIALS ===== function initTestimonials() { const testimonialCards = document.querySelectorAll('.testimonial-card'); testimonialCards.forEach(card => { const toggle = card.querySelector('.testimonial-toggle'); const excerpt = card.querySelector('.testimonial-excerpt'); const full = card.querySelector('.testimonial-full'); if (!toggle || !excerpt || !full) return; toggle.addEventListener('click', () => { const isExpanded = card.classList.contains('expanded'); card.classList.toggle('expanded'); toggle.textContent = isExpanded ? 'Read More' : 'Read Less'; toggle.setAttribute('aria-expanded', !isExpanded); if (!isExpanded) { excerpt.style.display = 'none'; full.style.display = 'block'; } else { excerpt.style.display = 'block'; full.style.display = 'none'; } }); }); } // ===== INITIALIZE ALL ===== document.addEventListener('DOMContentLoaded', () => { initTypewriter(); initScrollProgress(); initSkillBars(); initParticles(); initTestimonials(); });