🐐
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// ── 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 = `<span>${list[target]}</span>`;
|
||||
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 = `<span>${list[target]}</span>`;
|
||||
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; });
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user