// ── Sprinkle background ──────────────────────────── (function () { const canvas = document.getElementById('bgCanvas'); const ctx = canvas.getContext('2d'); const COLORS = ['#ffd84d', '#8cbcff', '#ff9fca', '#a9e76c', '#b99cff', '#ff9d57']; const INK = '#171717'; 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); }); // Resizing clears the canvas; with no animation loop running, repaint it if (reduceMotion.matches) { cancelAnimationFrame(rafId); rafId = requestAnimationFrame(draw); } } 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.lineCap = 'round'; ctx.globalAlpha = s.alpha; // Ink outline under the colored fill (sticker look): same path, two strokes ctx.strokeStyle = INK; ctx.lineWidth = s.width + 3; ctx.stroke(); ctx.strokeStyle = s.color; ctx.lineWidth = s.width; ctx.stroke(); ctx.restore(); // Reduced motion: sprinkles stay where they are if (reduceMotion.matches) return; // 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; }); 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; window.addEventListener('mousemove', (e) => { mouseX = e.clientX; mouseY = e.clientY; }, { passive: true }); rafId = 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); }); }); })(); // ── Avatar spin on click ────────────────────────────────────── (function () { const avatar = document.querySelector('.avatar-ring'); 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(); } }); }); document.addEventListener('click', closeAll); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeAll(); }); })(); // ── Pepsi gallery: shuffled on load, swipe cycles through on click ─ (function () { const COUNT = 15; const img = document.querySelector('.pepsi-placeholder img'); if (!img) return; const box = img.parentElement; const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); const src = (n) => `Pepsi/Pepsi-${n}.jpg`; // Fisher–Yates shuffle of [1..COUNT]; clicks cycle through this order const order = Array.from({ length: COUNT }, (_, i) => i + 1); for (let i = order.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [order[i], order[j]] = [order[j], order[i]]; } let pos = 0; img.src = src(order[pos]); let busy = false; box.addEventListener('click', () => { if (busy) return; pos = (pos + 1) % order.length; const next = order[pos]; if (reduceMotion.matches) { img.src = src(next); return; } busy = true; const incoming = new Image(); incoming.src = src(next); incoming.alt = img.alt; incoming.draggable = false; incoming.className = 'pepsi-incoming'; // Decode before animating so the swipe never reveals a half-loaded frame incoming.decode().then(() => { box.appendChild(incoming); const opts = { duration: 300, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' }; img.animate( [{ transform: 'translateX(0)' }, { transform: 'translateX(-100%)' }], opts ); incoming.animate( [{ transform: 'translateX(100%)' }, { transform: 'translateX(0)' }], opts ).finished.then(() => { img.src = incoming.src; // already decoded, swaps instantly incoming.remove(); busy = false; }); }).catch(() => { busy = false; }); }); })();