Files
ai.interactive.fiction/public/js/ui-display-handler-module.js
T

379 lines
15 KiB
JavaScript

/**
* 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<void>}
*/
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<string>} imagePaths - Array of image paths to preload
* @returns {Promise<void>}
*/
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 = `
<h2 class="byline l10n-by">powered by Generative AI</h2>
<h1 class="title">AI Interactive Fiction</h1>
<h3 class="subtitle">An open-world text adventure</h3>
<div class="separator"><double>❦</double></div>
`;
this.pageLeft.appendChild(header);
// Create controls
const controls = document.createElement('div');
controls.id = 'controls';
controls.className = 'buttons';
controls.innerHTML = `
<a class="l10n-speech" id="speech" title="Toggle text to speech" disabled="disabled">speech</a>
<span><a id="speed_reset"><span class="l10n-speed">speed<sup>*</sup></span></a><input type="range" min="0" max="100" value="50" id="speed" name="speed" /></span>
<a class="l10n-restart" id="rewind" title="Restart story from beginning" disabled="disabled">restart</a>
<a class="l10n-save" id="save" title="Save progress">save</a>
<a class="l10n-load" id="reload" title="Reload from save point" disabled="disabled">load</a>
<a class="l10n-options" id="options" title="Options">options</a>
`;
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 = `
<div class="input-wrapper">
<textarea id="player_input" placeholder="Enter your command..." rows="1" autofocus></textarea>
<span id="cursor"></span>
</div>
`;
choicesContainer.appendChild(commandInput);
this.pageLeft.appendChild(choicesContainer);
// Create remark
const remark = document.createElement('div');
remark.id = 'remark';
remark.className = 'l10n-remark';
remark.innerHTML = '<i><sup>*</sup>click on page or press spacebar to fast forward text animation</i>';
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<HTMLElement>} - 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<Object>} choices - Array of choice objects
* @param {Function} callback - Callback function for choice selection
* @returns {Promise<HTMLElement>} - 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 };