79 lines
2.4 KiB
JavaScript
79 lines
2.4 KiB
JavaScript
// ── Name anagram: clicks cycle through rearrangements ─────────
|
|
(function () {
|
|
const h1 = document.querySelector('.hero-name');
|
|
if (!h1) return;
|
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
|
|
const NAME = 'Nikolaj Gade';
|
|
const CYCLE = [NAME, 'a joking deal', NAME, 'naked oil jag'];
|
|
|
|
// The visual letters rearrange, but assistive tech always gets the name
|
|
h1.setAttribute('aria-label', NAME);
|
|
|
|
// Split into persistent per-letter spans; the exact glyphs get reused,
|
|
// so the capitals travel (see "a jokiNG deal")
|
|
const letters = [];
|
|
h1.textContent = '';
|
|
for (const ch of NAME) {
|
|
if (ch === ' ') {
|
|
h1.appendChild(document.createTextNode(' '));
|
|
continue;
|
|
}
|
|
const s = document.createElement('span');
|
|
s.className = 'ltr';
|
|
s.textContent = ch;
|
|
h1.appendChild(s);
|
|
letters.push({ el: s, ch: ch.toLowerCase() });
|
|
}
|
|
|
|
function arrange(phrase) {
|
|
// Pick a distinct source glyph for every letter of the target
|
|
const pool = [...letters];
|
|
const seq = [];
|
|
for (const ch of phrase) {
|
|
if (ch === ' ') {
|
|
seq.push(null);
|
|
continue;
|
|
}
|
|
const i = pool.findIndex((l) => l.ch === ch.toLowerCase());
|
|
seq.push(pool.splice(i, 1)[0].el);
|
|
}
|
|
|
|
// FLIP: measure, reorder the DOM, measure again, then animate each
|
|
// glyph from its old position into its new one
|
|
const first = new Map(letters.map((l) => [l.el, l.el.getBoundingClientRect()]));
|
|
h1.textContent = '';
|
|
seq.forEach((el) => h1.appendChild(el || document.createTextNode(' ')));
|
|
if (reduceMotion.matches) return Promise.resolve();
|
|
|
|
return Promise.all(
|
|
letters.map((l, k) => {
|
|
const a = first.get(l.el);
|
|
const b = l.el.getBoundingClientRect();
|
|
return l.el.animate(
|
|
[
|
|
{ transform: `translate(${(a.left - b.left).toFixed(1)}px, ${(a.top - b.top).toFixed(1)}px)` },
|
|
{ transform: 'translate(0, 0)' },
|
|
],
|
|
{
|
|
duration: 650,
|
|
delay: k * 18,
|
|
fill: 'backwards',
|
|
easing: 'cubic-bezier(0.3, 0.9, 0.3, 1)',
|
|
}
|
|
).finished;
|
|
})
|
|
);
|
|
}
|
|
|
|
let pos = 0;
|
|
let busy = false;
|
|
h1.addEventListener('click', () => {
|
|
if (window.achv) window.achv.mark('anagram');
|
|
if (busy) return;
|
|
busy = true;
|
|
pos = (pos + 1) % CYCLE.length;
|
|
arrange(CYCLE[pos]).finally(() => { busy = false; });
|
|
});
|
|
})();
|