feat(site): add engaging interactions (typewriter, progress, skills, particles, testimonials)
Ultraworked with Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
164
main.js
Normal file
164
main.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// ===== 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();
|
||||
});
|
||||
Reference in New Issue
Block a user