// hud.js — DOM overlays for walking-mode: entry CTA + in-walk HUD
// (crosshair, mode pill, exit button, controls hint). Pure DOM, no
// PlayCanvas dependency. Caller injects the stage element and wires
// callbacks; HUD owns its own timers and class toggling.
const CTA_VISIBLE_MS = 4000; // CTA auto-fades after this idle
const CTA_FADE_MS = 500;
const HINT_VISIBLE_MS = 4000; // controls-hint dims after this
const EYE_OVERLAY_MS = 1200;
export class HUD {
constructor(stage) {
this.stage = stage;
this.cta = stage.querySelector('#cta-walk');
this.overlay = stage.querySelector('#walk-hud');
this.modeInd = stage.querySelector('#walk-mode-indicator');
this.exitBtn = stage.querySelector('#walk-exit');
this.hint = stage.querySelector('#walk-controls-hint');
this.eyeText = stage.querySelector('#walk-eye-overlay');
this.stageControls = stage.querySelector('#stage-controls');
this.heroOverlay = stage.querySelector('#hero-overlay');
this._ctaTimer = null;
this._ctaClick = null;
this._hintTimer = null;
this._eyeTimer = null;
this._exitCallback = null;
this.exitBtn?.addEventListener('click', () => this._exitCallback?.());
}
// ---------- CTA (entry point to walking mode) ----------
showCTA(onClick) {
if (!this.cta) return;
this._ctaClick = () => {
onClick?.();
this.hideCTA();
};
this.cta.hidden = false;
// force reflow so the transition triggers cleanly
void this.cta.offsetWidth;
this.cta.classList.add('is-visible');
this.cta.addEventListener('click', this._ctaClick, { once: true });
clearTimeout(this._ctaTimer);
this._ctaTimer = setTimeout(() => this.hideCTA(), CTA_VISIBLE_MS);
}
hideCTA() {
if (!this.cta) return;
clearTimeout(this._ctaTimer);
this._ctaTimer = null;
this.cta.classList.remove('is-visible');
if (this._ctaClick) {
this.cta.removeEventListener('click', this._ctaClick);
this._ctaClick = null;
}
setTimeout(() => {
if (!this.cta.classList.contains('is-visible')) this.cta.hidden = true;
}, CTA_FADE_MS);
}
isCTAVisible() {
return this.cta && !this.cta.hidden;
}
// ---------- In-walk overlay ----------
enterWalkingUI({ mode = 'walk', onExit } = {}) {
this._exitCallback = onExit ?? null;
this.hideCTA();
if (this.stageControls) this.stageControls.hidden = true;
if (this.heroOverlay) this.heroOverlay.hidden = true;
if (this.overlay) this.overlay.hidden = false;
this.setMode(mode);
this.showHint();
}
exitWalkingUI() {
if (this.overlay) this.overlay.hidden = true;
if (this.stageControls) this.stageControls.hidden = false;
if (this.heroOverlay) this.heroOverlay.hidden = false;
this._exitCallback = null;
clearTimeout(this._hintTimer);
clearTimeout(this._eyeTimer);
}
setMode(mode) {
if (!this.modeInd) return;
this.modeInd.textContent = mode === 'fly' ? '✈ Fly' : '🚶 Walk';
this.modeInd.dataset.mode = mode;
}
showHint() {
if (!this.hint) return;
this.hint.classList.remove('is-dim');
clearTimeout(this._hintTimer);
this._hintTimer = setTimeout(() => this.hint.classList.add('is-dim'),
HINT_VISIBLE_MS);
}
setEyeHeight(value) {
if (!this.eyeText) return;
this.eyeText.textContent = `Eye height: ${value.toFixed(2)}`;
this.eyeText.classList.add('is-visible');
clearTimeout(this._eyeTimer);
this._eyeTimer = setTimeout(() => this.eyeText.classList.remove('is-visible'),
EYE_OVERLAY_MS);
}
// ---------- Collision editor ----------
enterCollisionUI(handlers) {
this._collisionHandlers = handlers;
const t = this.stage.querySelector('#collision-toolbar');
if (!t) return;
t.hidden = false;
if (!this._collisionWired) {
t.querySelector('#coll-build')?.addEventListener('click', () => handlers.onBuild?.());
t.querySelector('#coll-undo')?.addEventListener('click', () => handlers.onUndo?.());
t.querySelector('#coll-reset')?.addEventListener('click', () => handlers.onReset?.());
t.querySelector('#coll-export-obj')?.addEventListener('click', () => handlers.onExportObj?.());
t.querySelector('#coll-save')?.addEventListener('click', () => handlers.onSaveSidecar?.());
t.querySelector('#coll-iso')?.addEventListener('input', (e) =>
handlers.onIsoChange?.(parseFloat(e.target.value)));
t.querySelectorAll('input[name="coll-tool"]').forEach((r) =>
r.addEventListener('change', (e) => handlers.onToolChange?.(e.target.value)));
t.querySelector('#coll-use-collider')?.addEventListener('change', (e) =>
handlers.onUseColliderChange?.(e.target.checked));
this._collisionWired = true;
}
}
setCollisionUseCollider(checked) {
const c = this.stage.querySelector('#coll-use-collider');
if (c) c.checked = !!checked;
}
exitCollisionUI() {
const t = this.stage.querySelector('#collision-toolbar');
if (t) t.hidden = true;
this._collisionHandlers = null;
}
setCollisionStatus(text) {
const s = this.stage.querySelector('#coll-status');
if (s) s.textContent = text;
}
getCollisionTool() {
const r = this.stage.querySelector('input[name="coll-tool"]:checked');
return r?.value ?? 'view';
}
getCollisionBrushSize() {
const r = this.stage.querySelector('#coll-brush');
return r ? parseFloat(r.value) : 0.3;
}
}