f40cf11ef8
CI & Lighthouse / test (push) Has been cancelled
- Remove 8 duplicated generic SVG icons from tech cards - Switch --color-primary from teal (#1D7874) to gold (#D4A574) - Remove particle canvas system (~70 lines JS, canvas element, CSS) - Grain texture + steam now carry atmosphere alone
186 lines
5.5 KiB
JavaScript
186 lines
5.5 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 (removed) =====
|
|
// initParticles removed — grain texture + steam carry the atmosphere
|
|
|
|
// ===== 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();
|
|
initTestimonials();
|
|
initParallax();
|
|
initMetricsCounter();
|
|
});
|