Files
website-cv/js/anagram.js
T
2026-08-09 23:42:23 +02:00

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; });
});
})();