Files
webdev-site/main.js
2026-02-12 09:27:05 -05:00

254 lines
7.1 KiB
JavaScript

// ===== 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';
}
});
});
}
// ===== PARALLAX EFFECT FOR DECORATIVE ELEMENTS =====
function initParallax() {
const floatingElements = document.querySelectorAll('.floating-bean, .floating-cup');
if (!floatingElements.length) return;
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
const scrolled = window.scrollY;
floatingElements.forEach((el, index) => {
const speed = 0.05 + (index * 0.02); // Vary speed per element
const yPos = -(scrolled * speed);
// Use CSS variable to avoid overwriting the float animation transform
el.style.transform = `translateY(${yPos}px)`;
});
ticking = false;
});
ticking = true;
}
}, { passive: true });
}
// ===== METRICS COUNTER ANIMATION =====
function initMetricsCounter() {
const gauges = document.querySelectorAll('.gauge');
if (!gauges.length) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
animateGauge(entry.target);
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
gauges.forEach(gauge => {
// Store original values
gauge.dataset.target = gauge.getAttribute('data-score');
// Reset to 0 for animation start
gauge.setAttribute('data-score', '0');
gauge.style.setProperty('--percent', '0%');
observer.observe(gauge);
});
function animateGauge(gauge) {
// Check for reduced motion preference
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const target = parseInt(gauge.dataset.target, 10);
if (prefersReducedMotion) {
gauge.setAttribute('data-score', target);
gauge.style.setProperty('--percent', `${target}%`);
return;
}
const duration = 2000; // ms
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing (easeOutQuart)
const ease = 1 - Math.pow(1 - progress, 4);
const currentVal = Math.floor(ease * target);
gauge.setAttribute('data-score', currentVal);
gauge.style.setProperty('--percent', `${currentVal}%`);
if (progress < 1) {
requestAnimationFrame(update);
} else {
// Ensure final value is exact
gauge.setAttribute('data-score', target);
gauge.style.setProperty('--percent', `${target}%`);
}
}
requestAnimationFrame(update);
}
}
// ===== INITIALIZE ALL =====
document.addEventListener('DOMContentLoaded', () => {
initTypewriter();
initScrollProgress();
initSkillBars();
initParticles();
initTestimonials();
initParallax();
initMetricsCounter();
});