/** * Room code lobby display: staggered opacity animation for header, footer, and two code lines. * Implements the overlay component contract: activate, deactivate, update, getStatus. */ /** * @typedef {object} RoomCodeDisplayElements * @property {HTMLElement} header * @property {HTMLElement} footer * @property {HTMLElement} codePart1 * @property {HTMLElement} codePart2 */ /** * @typedef {object} RoomCodeDisplayInputs * @property {HTMLInputElement} code1 * @property {HTMLInputElement} code2 * @property {HTMLInputElement} color1 * @property {HTMLInputElement} color2 * @property {HTMLInputElement} offset1 * @property {HTMLInputElement} offset2 * @property {HTMLInputElement} size * @property {HTMLInputElement} cycle * @property {HTMLInputElement} headerText * @property {HTMLInputElement} headerColor * @property {HTMLInputElement} headerSize * @property {HTMLInputElement} headerOffset * @property {HTMLInputElement} footerText * @property {HTMLInputElement} footerColor * @property {HTMLInputElement} footerSize * @property {HTMLInputElement} footerOffset * @property {HTMLInputElement} headerAppearDelay * @property {HTMLInputElement} headerAppearDuration * @property {HTMLInputElement} headerHideTime * @property {HTMLInputElement} headerHideDuration * @property {HTMLInputElement} line1AppearDelay * @property {HTMLInputElement} line1AppearDuration * @property {HTMLInputElement} line1HideTime * @property {HTMLInputElement} line1HideDuration * @property {HTMLInputElement} line2AppearDelay * @property {HTMLInputElement} line2AppearDuration * @property {HTMLInputElement} line2HideTime * @property {HTMLInputElement} line2HideDuration */ export class RoomCodeDisplay { /** @type {RoomCodeDisplayElements | null} */ #elements = null; /** @type {RoomCodeDisplayInputs | null} */ #inputs = null; /** @type {ReturnType[]} */ #animationTimers = []; /** @type {boolean} */ #active = false; /** * @param {RoomCodeDisplayElements} elements * @param {RoomCodeDisplayInputs} inputs */ init(elements, inputs) { this.#elements = elements; this.#inputs = inputs; } /** * @param {{ roomCode?: string, [key: string]: unknown }} ctx */ activate(ctx) { this.deactivate(); const inputs = this.#inputs; if (!inputs) { return; } this.#active = true; if (ctx?.roomCode != null && String(ctx.roomCode).trim().length > 0) { const raw = String(ctx.roomCode).trim().toUpperCase(); const mid = Math.floor(raw.length / 2); inputs.code1.value = raw.slice(0, mid); inputs.code2.value = raw.slice(mid); } else { inputs.code1.value = ''; inputs.code2.value = ''; } this.#applySettings(); this.#startAnimation(); } deactivate() { this.#active = false; this.#clearAnimationTimers(); const { header, footer, codePart1, codePart2 } = this.#elements ?? {}; if (!header || !footer || !codePart1 || !codePart2) { return; } const fadeTime = '0.3s'; header.style.transition = `opacity ${fadeTime} ease-out`; footer.style.transition = `opacity ${fadeTime} ease-out`; codePart1.style.transition = `opacity ${fadeTime} ease-out`; codePart2.style.transition = `opacity ${fadeTime} ease-out`; header.style.opacity = 0; footer.style.opacity = 0; codePart1.style.opacity = 0; codePart2.style.opacity = 0; } /** * @param {{ roomCode?: string, [key: string]: unknown }} ctx */ update(ctx) { if (!this.#active || !this.#inputs) { return; } const next = ctx?.roomCode != null ? String(ctx.roomCode).trim().toUpperCase() : ''; const current = this.#inputs.code1.value.toUpperCase() + this.#inputs.code2.value.toUpperCase(); if (next && next !== current) { this.activate(ctx); } } getStatus() { const inputs = this.#inputs; const roomCode = inputs ? (inputs.code1.value + inputs.code2.value).toUpperCase() : ''; return { active: this.#active, roomCode, timersRunning: this.#animationTimers.length > 0, }; } #clearAnimationTimers() { this.#animationTimers.forEach((id) => clearTimeout(id)); this.#animationTimers = []; } #applySettings() { const el = this.#elements; const inputs = this.#inputs; if (!el || !inputs) { return; } const { header, footer, codePart1, codePart2, } = el; codePart1.textContent = inputs.code1.value.toUpperCase(); codePart2.textContent = inputs.code2.value.toUpperCase(); codePart1.style.color = inputs.color1.value; codePart2.style.color = inputs.color2.value; codePart1.style.transform = `translateY(${inputs.offset1.value}px)`; codePart2.style.transform = `translateY(${inputs.offset2.value}px)`; codePart1.style.fontSize = `${inputs.size.value}px`; codePart2.style.fontSize = `${inputs.size.value}px`; header.textContent = inputs.headerText.value; header.style.color = inputs.headerColor.value; header.style.fontSize = `${inputs.headerSize.value}px`; header.style.transform = `translateY(${inputs.headerOffset.value}px)`; footer.textContent = inputs.footerText.value; footer.style.color = inputs.footerColor.value; footer.style.fontSize = `${inputs.footerSize.value}px`; footer.style.transform = `translateY(${inputs.footerOffset.value}px)`; } #startAnimation() { const el = this.#elements; const inputs = this.#inputs; if (!el || !inputs) { return; } const { header, footer, codePart1, codePart2 } = el; this.#clearAnimationTimers(); header.style.opacity = 0; footer.style.opacity = 0; codePart1.style.opacity = 0; codePart2.style.opacity = 0; const sec = (input) => parseInt(input.value, 10) * 1000; const cycleDuration = sec(inputs.cycle); const headerAppearDelayMs = sec(inputs.headerAppearDelay); const headerAppearDurationMs = sec(inputs.headerAppearDuration); this.#animationTimers.push( setTimeout(() => { header.style.transition = `opacity ${headerAppearDurationMs / 1000}s ease-out`; header.style.opacity = 1; footer.style.transition = `opacity ${headerAppearDurationMs / 1000}s ease-out`; footer.style.opacity = 1; }, headerAppearDelayMs), ); const line1AppearDelayMs = sec(inputs.line1AppearDelay); const line1AppearDurationMs = sec(inputs.line1AppearDuration); this.#animationTimers.push( setTimeout(() => { codePart1.style.transition = `opacity ${line1AppearDurationMs / 1000}s ease-out`; codePart1.style.opacity = 1; }, line1AppearDelayMs), ); const line1HideTimeMs = sec(inputs.line1HideTime); const line1HideDurationMs = sec(inputs.line1HideDuration); this.#animationTimers.push( setTimeout(() => { codePart1.style.transition = `opacity ${line1HideDurationMs / 1000}s ease-out`; codePart1.style.opacity = 0; }, line1HideTimeMs), ); const line2AppearDelayMs = sec(inputs.line2AppearDelay); const line2AppearDurationMs = sec(inputs.line2AppearDuration); this.#animationTimers.push( setTimeout(() => { codePart2.style.transition = `opacity ${line2AppearDurationMs / 1000}s ease-out`; codePart2.style.opacity = 1; }, line2AppearDelayMs), ); const line2HideTimeMs = sec(inputs.line2HideTime); const line2HideDurationMs = sec(inputs.line2HideDuration); this.#animationTimers.push( setTimeout(() => { codePart2.style.transition = `opacity ${line2HideDurationMs / 1000}s ease-out`; codePart2.style.opacity = 0; }, line2HideTimeMs), ); const headerHideTimeMs = sec(inputs.headerHideTime); const headerHideDurationMs = sec(inputs.headerHideDuration); this.#animationTimers.push( setTimeout(() => { header.style.transition = `opacity ${headerHideDurationMs / 1000}s ease-out`; header.style.opacity = 0; footer.style.transition = `opacity ${headerHideDurationMs / 1000}s ease-out`; footer.style.opacity = 0; }, headerHideTimeMs), ); this.#animationTimers.push( setTimeout(() => { if (this.#active) { this.#startAnimation(); } }, cycleDuration), ); } }