Spaces:
Running
Running
| // 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 = ` | |
| <div class="hp-name" style="color:${c}">${hero.name}</div> | |
| <div class="hp-role">${hero.role}</div> | |
| <div class="hp-head">SUPERPOWERS</div> | |
| <ul class="hp-list"></ul>`; | |
| 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'; | |
| } | |
| } | |