// ── Job title slot machine: click the role pill ─────────────── (function () { const pill = document.getElementById('heroRole'); if (!pill) return; const WORDS = [ ['Software', 'Backend', 'Data', 'Computer', 'Frontend', 'Full-Stack', 'Tech', 'Server', 'It Project', 'Cloud'], ['Developer', 'Scientist', 'Engineer', 'Enthusiast', 'Wizard', 'Programmer', 'Manager'], ]; const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); const slots = [...pill.querySelectorAll('.slot')]; // Width of a word in this mono font: 1ch + letter-spacing per character. // Slots hug their current word at rest and expand to their widest word // while spinning (the CSS width transition animates both moves). const widthFor = (word) => `calc(${word.length}ch + ${word.length * 0.08}em)`; slots.forEach((slot) => { slot.style.width = widthFor(slot.textContent.trim()); }); let busy = false; pill.addEventListener('click', () => { if (window.achv) window.achv.mark('slot'); if (busy) return; busy = true; const spins = slots.map((slot, i) => { const list = WORDS[i]; const reel = slot.querySelector('.reel'); const from = Math.max(list.indexOf(reel.textContent.trim()), 0); // Random pick that's never the word already showing let target = Math.floor(Math.random() * (list.length - 1)); if (target >= from) target++; if (reduceMotion.matches) { reel.innerHTML = `${list[target]}`; slot.style.width = widthFor(list[target]); return Promise.resolve(); } // Expand to fit the reel's widest word for the duration of the spin const longest = list.reduce((a, b) => (b.length > a.length ? b : a)); slot.style.width = widthFor(longest); // Walk the reel in list order: a couple of full revolutions, then // land on the target (the second reel spins one revolution longer) const steps = ((target - from + list.length) % list.length) + list.length * (2 + i); // Build the strip top-down ending at the current word, so the column // slides downward and words enter from above. One extra word sits // above the target to back the overshoot. reel.innerHTML = ''; for (let k = 0; k <= steps + 1; k++) { const w = document.createElement('span'); w.textContent = list[(from + steps + 1 - k) % list.length]; reel.appendChild(w); } const h = reel.firstChild.getBoundingClientRect().height; const land = -h; // row where the target word sits const over = land + h * 0.35; // spin runs slightly past the stop const anim = reel.animate( [ { transform: `translateY(${-(steps + 1) * h}px)` }, { transform: `translateY(${over}px)` }, ], { duration: 900 + i * 500, easing: 'cubic-bezier(0.15, 0.85, 0.25, 1)', fill: 'forwards' } ); return anim.finished.then(() => { const settle = reel.animate( [{ transform: `translateY(${over}px)` }, { transform: `translateY(${land}px)` }], { duration: 200, easing: 'ease', fill: 'forwards' } ); return settle.finished.then(() => { reel.innerHTML = `${list[target]}`; settle.cancel(); anim.cancel(); // drop the forward-filled transforms after the swap slot.style.width = widthFor(list[target]); // shrink back to fit }); }); }); Promise.all(spins).finally(() => { busy = false; }); }); })();