🐐
This commit is contained in:
@@ -1,47 +1,3 @@
|
||||
// ── 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();
|
||||
}
|
||||
|
||||
// ── Hero intro ────────────────────────────────────────────────
|
||||
(function () {
|
||||
const nameEl = document.querySelector('.hero-name');
|
||||
const restEls = [...document.querySelectorAll('.hero-role, .hero-pronouns, .hero-links, .hero-visual')];
|
||||
|
||||
// Store the name and clear it so the typewriter can type it out
|
||||
const fullName = nameEl.textContent.trim();
|
||||
nameEl.textContent = '';
|
||||
|
||||
// Hide hero elements that reveal after typing. They're already hidden by the
|
||||
// .js pre-reveal block in style.css; this just adds the transition.
|
||||
restEls.forEach(el => el.classList.add('fade-up'));
|
||||
|
||||
startHeroTypewriter(nameEl, fullName, restEls);
|
||||
})();
|
||||
|
||||
|
||||
// ── Sprinkle background ────────────────────────────
|
||||
(function () {
|
||||
const canvas = document.getElementById('bgCanvas');
|
||||
@@ -67,6 +23,11 @@ function startHeroTypewriter(nameEl, fullName, restEls) {
|
||||
if (s.x > W) s.x = rand(0, W);
|
||||
if (s.y > H) s.y = rand(0, H);
|
||||
});
|
||||
// Resizing clears the canvas; with no animation loop running, repaint it
|
||||
if (reduceMotion.matches) {
|
||||
cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(draw);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
@@ -109,6 +70,9 @@ function startHeroTypewriter(nameEl, fullName, restEls) {
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
|
||||
// Reduced motion: sprinkles stay where they are
|
||||
if (reduceMotion.matches) return;
|
||||
|
||||
// Drift
|
||||
s.x += s.vx;
|
||||
s.y += s.vy;
|
||||
@@ -138,9 +102,17 @@ function startHeroTypewriter(nameEl, fullName, restEls) {
|
||||
if (s.x > W + MAX_LEN) s.x = -MAX_LEN;
|
||||
});
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
if (!reduceMotion.matches) rafId = requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
// With reduced motion the sprinkles render once as a static backdrop
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
let rafId;
|
||||
reduceMotion.addEventListener('change', () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(draw);
|
||||
});
|
||||
|
||||
window.addEventListener('resize', resize, { passive: true });
|
||||
resize();
|
||||
let mouseX = -9999, mouseY = -9999;
|
||||
@@ -148,7 +120,7 @@ function startHeroTypewriter(nameEl, fullName, restEls) {
|
||||
mouseX = e.clientX;
|
||||
mouseY = e.clientY;
|
||||
}, { passive: true });
|
||||
requestAnimationFrame(draw);
|
||||
rafId = requestAnimationFrame(draw);
|
||||
})();
|
||||
|
||||
|
||||
@@ -193,25 +165,61 @@ function startHeroTypewriter(nameEl, fullName, restEls) {
|
||||
})();
|
||||
|
||||
|
||||
// ── Scroll fade-in ─────────────────────────────────────────────
|
||||
// ── Avatar spin on click ──────────────────────────────────────
|
||||
(function () {
|
||||
// Keep this list in sync with the .js pre-reveal block in style.css
|
||||
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 avatar = document.querySelector('.avatar-ring');
|
||||
|
||||
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);
|
||||
avatar.addEventListener('click', () => {
|
||||
if (avatar.classList.contains('spinning')) return;
|
||||
avatar.classList.add('spinning');
|
||||
});
|
||||
avatar.addEventListener('animationend', () => {
|
||||
avatar.classList.remove('spinning');
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
// ── Tag pill tooltips: tap toggle (touch devices only) ────────
|
||||
(function () {
|
||||
// Hover-capable devices already get the tooltips via :hover
|
||||
if (window.matchMedia('(hover: hover)').matches) return;
|
||||
|
||||
const pills = [...document.querySelectorAll('[data-tip]')];
|
||||
|
||||
function closeAll() {
|
||||
pills.forEach(p => {
|
||||
p.classList.remove('tip-open');
|
||||
p.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
|
||||
pills.forEach(pill => {
|
||||
pill.setAttribute('tabindex', '0');
|
||||
pill.setAttribute('role', 'button');
|
||||
pill.setAttribute('aria-expanded', 'false');
|
||||
// Pseudo-element tooltips are invisible to screen readers
|
||||
pill.setAttribute('aria-description', pill.dataset.tip);
|
||||
|
||||
pill.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const wasOpen = pill.classList.contains('tip-open');
|
||||
closeAll();
|
||||
if (!wasOpen) {
|
||||
pill.classList.add('tip-open');
|
||||
pill.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
});
|
||||
pill.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
pill.click();
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
});
|
||||
|
||||
targets.forEach(el => observer.observe(el));
|
||||
document.addEventListener('click', closeAll);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeAll();
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user