// hover_panel.js // Floating holographic superpower panel. Rendered as a DOM overlay anchored to // the hero's screen-projected head position (rises ~60cm above the head in world // space). Name in hero color (monospace), SUPERPOWERS header, 4 lines that type // out sequentially (30ms/char) then pulse, each with an animated SVG icon. import { buildIcon } from './superpower_icons.js'; export class HoverPanel { constructor(container, reduced) { this.container = container; this.reduced = reduced; this.el = document.createElement('div'); this.el.className = 'hover-panel'; this.el.setAttribute('role', 'tooltip'); this.el.setAttribute('aria-hidden', 'true'); container.appendChild(this.el); this.activeId = null; this._timers = []; } _clearTimers() { this._timers.forEach(clearTimeout); this._timers = []; } show(hero) { if (this.activeId === hero.id) return; this.activeId = hero.id; this._clearTimers(); const c = hero.color; this.el.style.setProperty('--hc', c); this.el.innerHTML = `
${hero.name}
${hero.role}
SUPERPOWERS
`; const list = this.el.querySelector('.hp-list'); hero.powers.forEach((p, i) => { const li = document.createElement('li'); li.className = 'hp-line'; const icon = buildIcon(p.icon, c, this.reduced); const txt = document.createElement('span'); txt.className = 'hp-text'; li.appendChild(icon); li.appendChild(txt); list.appendChild(li); if (this.reduced) { txt.textContent = p.label; li.classList.add('hp-shown'); } else { // sequential type-out: 30ms/char, staggered per line const startDelay = i * 520; this._timers.push(setTimeout(() => this._type(li, txt, p.label), startDelay)); } }); this.el.classList.add('hp-visible'); this.el.setAttribute('aria-hidden', 'false'); } _type(li, txt, label) { li.classList.add('hp-shown'); let n = 0; const step = () => { txt.textContent = label.slice(0, n); n++; if (n <= label.length) { this._timers.push(setTimeout(step, 30)); } else { li.classList.add('hp-pulse'); // pulse once typed } }; step(); } hide() { this.activeId = null; this._clearTimers(); this.el.classList.remove('hp-visible'); this.el.setAttribute('aria-hidden', 'true'); } // Position the panel at a screen-space point (px), centered horizontally above it. place(x, y) { this.el.style.left = x + 'px'; this.el.style.top = y + 'px'; } }