/** * UI Display Handler Module * Manages the display of text and UI elements */ import { BaseModule } from './base-module.js'; class UIDisplayHandlerModule extends BaseModule { constructor() { super('ui-display-handler', 'UI Display Handler'); // Module dependencies this.dependencies = ['paragraph-layout', 'layout-renderer', 'animation-queue']; // DOM elements this.container = null; this.pageLeft = null; this.pageRight = null; this.paragraphContainer = null; // Resources to preload this.cssPath = '/css/style.css'; this.imagesToPreload = [ '/images/book-3057904.png', '/images/brown-wooden-flooring.jpg' ]; // Bind methods using parent's bindMethods utility this.bindMethods([ 'initializeContainers', 'displayText', 'measureText', 'loadCSS', 'showChoices', 'preloadImages' ]); console.log('UIDisplayHandler: Constructor initialized'); } async initialize() { try { this.reportProgress(10, "Initializing UI Display Handler"); // Load CSS and preload images this.reportProgress(20, "Loading CSS and preloading images"); await this.loadCSS(this.cssPath); await this.preloadImages(this.imagesToPreload); this.reportProgress(30, "Getting module dependencies"); // Get references to required modules using parent's getModule method this.paragraphLayout = this.getModule('paragraph-layout'); this.layoutRenderer = this.getModule('layout-renderer'); this.animationQueue = this.getModule('animation-queue'); this.reportProgress(50, "Initializing display containers"); // Initialize container elements this.initializeContainers(); this.reportProgress(70, "Setting up typography"); this.reportProgress(90, "Setting up event listeners"); this.reportProgress(100, "UI Display Handler ready"); return true; } catch (error) { console.error("Error initializing UI Display Handler:", error); return false; } } /** * Load CSS file asynchronously and wait for it to be applied * @param {string} cssPath - Path to CSS file * @returns {Promise} */ loadCSS(cssPath) { return new Promise((resolve, reject) => { // Create link element const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = cssPath; // Set up load and error handlers link.onload = () => { console.log(`UIDisplayHandler: CSS ${cssPath} loaded successfully`); resolve(); }; link.onerror = (error) => { console.error(`UIDisplayHandler: Failed to load CSS ${cssPath}:`, error); reject(error); }; // Add to document head document.head.appendChild(link); }); } /** * Preload images to ensure they're in the cache * @param {Array} imagePaths - Array of image paths to preload * @returns {Promise} */ preloadImages(imagePaths) { if (!imagePaths || imagePaths.length === 0) { return Promise.resolve(); } const promises = imagePaths.map(path => { return new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(); img.onerror = () => { console.warn(`UIDisplayHandler: Failed to preload image ${path}`); resolve(); // Resolve anyway to not block loading }; img.src = path; }); }); return Promise.all(promises); } /** * Initialize the UI containers */ initializeContainers() { // Check if the book container already exists let bookContainer = document.getElementById('book'); if (!bookContainer) { console.log('UIDisplayHandler: Book container not found, creating it'); bookContainer = document.createElement('div'); bookContainer.id = 'book'; document.body.appendChild(bookContainer); } // Create or find page_left this.pageLeft = document.getElementById('page_left'); if (!this.pageLeft) { console.log('UIDisplayHandler: Left page not found, creating it'); this.pageLeft = document.createElement('div'); this.pageLeft.id = 'page_left'; // Create header content const header = document.createElement('div'); header.className = 'header'; header.innerHTML = `

AI Interactive Fiction

An open-world text adventure

`; this.pageLeft.appendChild(header); // Create controls const controls = document.createElement('div'); controls.id = 'controls'; controls.className = 'buttons'; controls.innerHTML = ` speech speed* restart save load options `; this.pageLeft.appendChild(controls); // Create choices container const choicesContainer = document.createElement('div'); choicesContainer.id = 'choices'; choicesContainer.className = 'container'; // Create command history container const commandHistory = document.createElement('div'); commandHistory.id = 'command_history'; choicesContainer.appendChild(commandHistory); // Create command input container const commandInput = document.createElement('div'); commandInput.id = 'command_input'; commandInput.innerHTML = `
`; choicesContainer.appendChild(commandInput); this.pageLeft.appendChild(choicesContainer); // Create remark const remark = document.createElement('div'); remark.id = 'remark'; remark.className = 'l10n-remark'; remark.innerHTML = '*click on page or press spacebar to fast forward text animation'; this.pageLeft.appendChild(remark); bookContainer.appendChild(this.pageLeft); } // Create or find page_right this.pageRight = document.getElementById('page_right'); if (!this.pageRight) { console.log('UIDisplayHandler: Right page not found, creating it'); this.pageRight = document.createElement('div'); this.pageRight.id = 'page_right'; bookContainer.appendChild(this.pageRight); } // Create or find story container this.container = document.getElementById('story'); if (!this.container) { console.log('UIDisplayHandler: Story container not found, creating it'); this.container = document.createElement('div'); this.container.id = 'story'; this.container.className = 'container'; this.pageRight.appendChild(this.container); } // Create paragraph container inside story container this.paragraphContainer = document.getElementById('paragraphs'); if (!this.paragraphContainer) { console.log('UIDisplayHandler: Paragraphs container not found, creating it'); this.paragraphContainer = document.createElement('div'); this.paragraphContainer.id = 'paragraphs'; this.container.appendChild(this.paragraphContainer); } // Create ruler for text measurements let ruler = document.getElementById('ruler'); if (!ruler) { ruler = document.createElement('div'); ruler.id = 'ruler'; document.body.appendChild(ruler); } // Create lighting effect let lighting = document.getElementById('lighting'); if (!lighting) { lighting = document.createElement('div'); lighting.id = 'lighting'; document.body.appendChild(lighting); } console.log('UIDisplayHandler: All containers initialized'); } /** * Measure text width using canvas * @param {string} text - Text to measure * @returns {number} - Width of the text */ measureText(text) { // Use ParagraphLayout's measureText function instead of implementing our own if (this.paragraphLayout && typeof this.paragraphLayout.measureText === 'function') { return this.paragraphLayout.measureText(text); } // Fallback measuring if paragraph layout is not available if (!this.canvas) { this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); this.context.font = `${this.config.typography.fontSize} ${this.config.typography.fontFamily}`; } return this.context.measureText(text).width; } /** * Display text in the UI * @param {string} text - Text to display * @param {Object} options - Display options * @returns {Promise} - Promise resolving to the displayed paragraph element */ displayText(text, options = {}) { if (!text) return Promise.resolve(null); return new Promise((resolve) => { // Generate a unique ID for this paragraph const paragraphId = `p-${Date.now()}-${this.currentParagraphId++}`; // Add to pending paragraphs queue this.pendingParagraphs.push({ id: paragraphId, text, options, resolve }); // If this is the only paragraph, process it immediately if (this.pendingParagraphs.length === 1) { this.processNextParagraph(); } else { console.log(`UIDisplayHandler: Queued paragraph (${this.pendingParagraphs.length} total)`); } }); } /** * Show choices in the UI * @param {Array} choices - Array of choice objects * @param {Function} callback - Callback function for choice selection * @returns {Promise} - Promise resolving to the choices container */ showChoices(choices, callback) { if (!choices || choices.length === 0) { return Promise.resolve(null); } return new Promise((resolve) => { // Find or create choices container let choicesContainer = document.getElementById('choices'); if (!choicesContainer) { // UI Input Handler should create this, but if it doesn't exist yet, create it choicesContainer = document.createElement('div'); choicesContainer.id = 'choices'; choicesContainer.className = 'container'; this.pageLeft.appendChild(choicesContainer); } // Create a dedicated container for this set of choices const choicesGroup = document.createElement('div'); choicesGroup.className = 'choices-group'; choicesContainer.appendChild(choicesGroup); // Create each choice button choices.forEach((choice, index) => { const choiceButton = document.createElement('button'); choiceButton.className = 'choice-button'; choiceButton.textContent = choice.text; // Add index as data attribute choiceButton.dataset.index = index; // Add event listener choiceButton.addEventListener('click', () => { // Disable all buttons in this group Array.from(choicesGroup.querySelectorAll('button')).forEach(btn => { btn.disabled = true; btn.classList.add('selected'); }); // Highlight the selected button choiceButton.classList.add('selected'); // Call the callback if (typeof callback === 'function') { callback(index, choice); } }); choicesGroup.appendChild(choiceButton); }); // Schedule transition for choices to appear setTimeout(() => { choicesGroup.classList.add('visible'); // Scroll to the choices choicesGroup.scrollIntoView({ behavior: 'smooth', block: 'end' }); // Resolve the promise with the choices container resolve(choicesGroup); }, 100); }); } } // Create the singleton instance const uiDisplayHandler = new UIDisplayHandlerModule(); // Export the module export { uiDisplayHandler as UIDisplayHandler };