Files
website-cv/script.js
2026-03-13 17:50:20 +01:00

231 lines
7.3 KiB
JavaScript

// ── 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();
}
// ── Loader ────────────────────────────────────────────────────
(function () {
const loader = document.getElementById('loader');
const pctEl = document.getElementById('loaderPct');
const nameEl = document.querySelector('.hero-name');
const restEls = [...document.querySelectorAll('.hero-role, .hero-pronouns, .hero-links, .hero-visual')];
// Store the name and clear it immediately (loader covers the page anyway)
const fullName = nameEl.textContent.trim();
nameEl.textContent = '';
// Hide hero elements that reveal after typing
restEls.forEach(el => el.classList.add('fade-up'));
let pct = 0;
const tick = setInterval(() => {
pct += Math.random() * 15;
if (pct >= 100) {
pct = 100;
clearInterval(tick);
setTimeout(() => {
loader.classList.add('hide');
// Wait for the loader fade-out (0.6s), then start typing
setTimeout(() => startHeroTypewriter(nameEl, fullName, restEls), 600);
}, 300);
}
pctEl.textContent = Math.floor(pct) + '%';
}, 110);
})();
// ── Sprinkle background ────────────────────────────
(function () {
const canvas = document.getElementById('bgCanvas');
const ctx = canvas.getContext('2d');
const COLORS = ['#e63946', '#f4722b', '#ffbe0b', '#ff006e', '#e07b39', '#ffd166'];
const COUNT = 1000; // number of sprinkles
const MIN_LEN = 12; // px
const MAX_LEN = 18; // px
const MIN_W = 4;
const MAX_W = 6;
let W, H, sprinkles;
function rand(a, b) { return a + Math.random() * (b - a); }
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
if (!sprinkles) init();
// Redistribute any sprinkles that landed outside new bounds
sprinkles.forEach(s => {
if (s.x > W) s.x = rand(0, W);
if (s.y > H) s.y = rand(0, H);
});
}
function init() {
sprinkles = [];
for (let i = 0; i < COUNT; i++) {
sprinkles.push(makeSprinkle());
}
}
function makeSprinkle() {
return {
x: rand(0, W),
y: rand(0, H),
len: rand(MIN_LEN, MAX_LEN),
width: rand(MIN_W, MAX_W),
color: COLORS[Math.floor(Math.random() * COLORS.length)],
angle: rand(0, Math.PI * 2),
// very slow drift
vx: rand(-0.1, 0.1),
vy: rand(-0.1, 0.1),
va: rand(-0.005, 0.005), // angular velocity
alpha: 1,
};
}
function draw() {
ctx.clearRect(0, 0, W, H);
sprinkles.forEach(s => {
ctx.save();
ctx.translate(s.x, s.y);
ctx.rotate(s.angle);
ctx.beginPath();
ctx.moveTo(0, -s.len / 2);
ctx.lineTo(0, s.len / 2);
ctx.strokeStyle = s.color;
ctx.lineWidth = s.width;
ctx.lineCap = 'round';
ctx.globalAlpha = s.alpha;
ctx.stroke();
ctx.restore();
// Drift
s.x += s.vx;
s.y += s.vy;
if (s.vx > 0.1 || s.vx < -0.1) {
s.vx *= 0.98;
}
if (s.vy > 0.1 || s.vy < -0.1) {
s.vy *= 0.98;
}
s.angle += s.va;
// Repel from cursor
const dx = s.x - mouseX;
const dy = s.y - mouseY;
const dist = Math.sqrt(dx * dx + dy * dy);
const repelRadius = 80;
if (dist < repelRadius && dist > 0) {
const force = (repelRadius - dist) / repelRadius;
s.vx += (dx / dist) * force * 1.0;
s.vy += (dy / dist) * force * 1.0;
}
// Wrap: when a sprinkle drifts off the screen, reappear on the other side
if (s.y < -MAX_LEN) s.y = H + MAX_LEN;
if (s.y > H + MAX_LEN) s.y = -MAX_LEN
if (s.x < -MAX_LEN) s.x = W + MAX_LEN;
if (s.x > W + MAX_LEN) s.x = -MAX_LEN;
});
requestAnimationFrame(draw);
}
window.addEventListener('resize', resize, { passive: true });
resize();
let mouseX = -9999, mouseY = -9999;
window.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
}, { passive: true });
requestAnimationFrame(draw);
})();
// ── Nav: scroll state & active link ───────────────────────────
(function () {
const header = document.getElementById('header');
const links = document.querySelectorAll('.nav-link');
const sections = document.querySelectorAll('section[id]');
const burger = document.getElementById('burger');
window.addEventListener('scroll', () => {
header.classList.toggle('scrolled', window.scrollY > 24 || burger.classList.contains('open'));
let current = '';
sections.forEach(s => {
if (window.scrollY >= s.offsetTop - 100) current = s.id;
});
links.forEach(l => {
l.classList.toggle('active', l.getAttribute('href') === '#' + current);
});
}, { passive: true });
})();
// ── Mobile burger ──────────────────────────────────────────────
(function () {
const burger = document.getElementById('burger');
const navLinks = document.getElementById('navLinks');
const header = document.getElementById('header');
burger.addEventListener('click', () => {
burger.classList.toggle('open');
navLinks.classList.toggle('open');
header.classList.toggle('scrolled', burger.classList.contains('open') || window.scrollY > 24);
});
navLinks.querySelectorAll('a').forEach(a => {
a.addEventListener('click', () => {
burger.classList.remove('open');
navLinks.classList.remove('open');
header.classList.toggle('scrolled', window.scrollY > 24);
});
});
})();
// ── Scroll fade-in ─────────────────────────────────────────────
(function () {
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 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);
});
}, { threshold: 0.1 });
targets.forEach(el => observer.observe(el));
})();