Checkpoint current interactive fiction state

This commit is contained in:
2026-05-14 21:17:43 +02:00
parent c745efd1d2
commit 873049f7e6
183 changed files with 13755 additions and 1459 deletions
+123 -135
View File
@@ -8,14 +8,12 @@ import { BaseModule } from './base-module.js';
class ParagraphLayoutModule extends BaseModule {
constructor() {
super('paragraph-layout', 'Paragraph Layout');
// Module dependencies
this.dependencies = ['text-processor'];
// Caching canvas context for text measurements
this.textMeasureCtx = null;
// Configuration - use parent's config system
this.ruler = null;
this.rulerStack = [];
this.updateConfig({
maxLineWidth: 600,
hyphenationEnabled: true,
@@ -24,8 +22,7 @@ class ParagraphLayoutModule extends BaseModule {
defaultLineHeight: 1.5,
debugMode: false
});
// Bind methods using parent's bindMethods utility
this.bindMethods([
'calculateLayout',
'measureText',
@@ -37,27 +34,20 @@ class ParagraphLayoutModule extends BaseModule {
'loadLayoutDependencies'
]);
}
async initialize() {
try {
this.reportProgress(20, "Initializing paragraph layout");
// Get text processor using parent's getModule method
const textProcessor = this.getModule('text-processor');
if (!textProcessor) {
console.warn("Paragraph Layout: Text Processor not found, will use fallback processing");
console.warn("Paragraph Layout: Text Processor not found, will use unprocessed text");
}
// Load required dependencies
await this.loadLayoutDependencies();
// Create off-screen canvas for text measurements
this.initializeTextMeasurement();
// Set up event listeners for config changes
this.setupEventListeners();
this.reportProgress(100, "Paragraph layout ready");
return true;
} catch (error) {
@@ -65,23 +55,15 @@ class ParagraphLayoutModule extends BaseModule {
return false;
}
}
/**
* Load required dependencies for layout calculations
*/
async loadLayoutDependencies() {
try {
this.reportProgress(30, "Loading layout dependencies");
// Load LinkedList.js first as it's required by linebreak.js
await this.loadScript('/js/linked-list.js');
// Load linebreak.js which is required by knuth-and-plass.js
await this.loadScript('/js/linebreak.js');
// Load knuth-and-plass.js which contains the kap function
await this.loadScript('/js/knuth-and-plass.js');
this.reportProgress(50, "Layout dependencies loaded");
return true;
} catch (error) {
@@ -89,148 +71,163 @@ class ParagraphLayoutModule extends BaseModule {
return false;
}
}
/**
* Initialize text measurement canvas
*/
initializeTextMeasurement() {
// Create off-screen canvas for text measurements
const canvas = document.createElement('canvas');
canvas.width = 2000;
canvas.height = 100;
this.textMeasureCtx = canvas.getContext('2d');
// Set default font
let ruler = document.getElementById('ruler');
if (!ruler) {
ruler = document.createElement('span');
ruler.id = 'ruler';
document.body.appendChild(ruler);
}
Object.assign(ruler.style, {
visibility: 'hidden',
position: 'absolute',
top: '-8000px',
left: '-8000px',
width: 'auto',
display: 'inline',
textIndent: '0',
textAlign: 'left',
hyphens: 'none',
marginBlockEnd: '0'
});
this.ruler = ruler;
this.rulerStack = [ruler];
this.updateFont(this.config.defaultFontSize, this.config.defaultFontFamily);
}
setupEventListeners() {
// Use parent's addEventListener for automatic cleanup
this.addEventListener(document, 'ui:font:change', (event) => {
if (event.detail) {
const { fontSize, fontFamily } = event.detail;
if (fontSize || fontFamily) {
this.updateFont(
fontSize || this.config.defaultFontSize,
fontSize || this.config.defaultFontSize,
fontFamily || this.config.defaultFontFamily
);
}
}
});
// Listen for config changes
this.addEventListener(document, 'ui:hyphenation:toggle', (event) => {
if (event.detail && typeof event.detail.enabled === 'boolean') {
this.updateConfig({ hyphenationEnabled: event.detail.enabled });
}
});
}
/**
* Update the font for text measurements
* @param {string} fontSize - Font size (with units)
* @param {string} fontFamily - Font family
*/
updateFont(fontSize, fontFamily) {
if (!this.textMeasureCtx) {
console.warn("Text measurement context not initialized");
if (!this.ruler) {
console.warn("Text measurement ruler not initialized");
return;
}
// Update config if values are provided
if (fontSize) this.updateConfig({ defaultFontSize: fontSize });
if (fontFamily) this.updateConfig({ defaultFontFamily: fontFamily });
// Set font on measurement context
this.textMeasureCtx.font = `${fontSize} ${fontFamily}`;
this.ruler.style.fontSize = fontSize;
this.ruler.style.fontFamily = fontFamily;
this.ruler.style.fontFeatureSettings = "'kern' on, 'liga' on, 'onum' on, 'clig' on, 'hlig' on";
if (this.config.debugMode) {
console.log(`Font updated: ${fontSize} ${fontFamily}`);
}
}
/**
* Measure text width using canvas context
* @param {string} text - Text to measure
* @returns {number} - Text width in pixels
*/
measureText(text) {
if (!this.textMeasureCtx) return 0;
if (!this.ruler) return 0;
if (!text) return 0;
const metrics = this.textMeasureCtx.measureText(text);
return metrics.width;
if (text.substr(0, 2) === '</') {
if (this.rulerStack.length > 1) {
const child = this.rulerStack.pop();
const parent = this.rulerStack[this.rulerStack.length - 1];
if (child.parentElement === parent) {
parent.removeChild(child);
}
}
return 0;
}
if (text.substr(0, 1) === '<') {
const template = document.createElement('div');
template.innerHTML = text;
const child = template.firstChild;
if (child) {
const parent = this.rulerStack[this.rulerStack.length - 1];
this.rulerStack.push(child);
parent.appendChild(child);
}
return 0;
}
if (text === '|') return 0;
if (text === ' ') text = '\u00A0';
const parent = this.rulerStack[this.rulerStack.length - 1];
const textNode = document.createTextNode(text);
parent.appendChild(textNode);
const rect = parent.getClientRects()[0] || parent.getBoundingClientRect();
const width = rect ? rect.width : 0;
parent.removeChild(textNode);
return width;
}
/**
* Process text for layout (apply hyphenation and smartypants)
* @param {string} text - Text to process
* @returns {string} - Processed text
*/
processTextForLayout(text) {
if (!text) return '';
let processedText = text;
const textProcessor = this.getModule('text-processor');
// Apply text processing if available
if (textProcessor) {
// Apply smartypants (typographic punctuation) if available
if (typeof textProcessor.applySmartypants === 'function') {
processedText = textProcessor.applySmartypants(processedText);
}
// Apply hyphenation if enabled and available
if (this.config.hyphenationEnabled && typeof textProcessor.hyphenateText === 'function') {
processedText = textProcessor.hyphenateText(processedText);
}
if (textProcessor && typeof textProcessor.process === 'function') {
processedText = textProcessor.process(processedText, {
smartypants: true,
hyphenate: this.config.hyphenationEnabled,
hyphenSelector: '.hyphenatePipe'
});
} else if (this.config.debugMode) {
console.log("Text processor not available, skipping text processing");
}
return processedText;
}
/**
* Calculate layout for a paragraph using Knuth and Plass algorithm
* @param {string} text - Text to layout
* @param {Object} options - Layout options
* @returns {Object} - Layout data with line breaks
*/
calculateLayout(text, options = {}) {
if (!text) return null;
try {
// Check if the kap function is available
if (typeof window.kap !== 'function') {
console.error("Paragraph Layout: kap function not available. Make sure knuth-and-plass.js is loaded.");
return null;
}
// Process text for layout (hyphenation, etc)
const processedText = this.processTextForLayout(text);
// Prepare options by merging with defaults
const layoutOptions = {
width: options.width || this.config.maxLineWidth,
fontSize: options.fontSize || this.config.defaultFontSize,
fontFamily: options.fontFamily || this.config.defaultFontFamily,
lineHeight: options.lineHeight || this.config.defaultLineHeight,
tolerance: options.tolerance || 3, // Tolerance for line breaking algorithm
lineHeightPx: options.lineHeightPx,
tolerance: options.tolerance || 3,
demerits: options.demerits || {
line: 10, // Demerits for each line break
flagged: 100, // Demerits for flagged break points (like hyphens)
fitness: 3000 // Demerits for consecutive lines with very different looseness/tightness
line: 10,
flagged: 100,
fitness: 3000
}
};
// Update font for measurement
this.updateFont(layoutOptions.fontSize, layoutOptions.fontFamily);
// Create measure array - this is crucial for proper line breaking
// The first value is the full width, subsequent values can be for indented lines
const measure = [layoutOptions.width];
const numericFontSize = parseFloat(layoutOptions.fontSize) || 16;
const lineHeightPx = layoutOptions.lineHeightPx || (numericFontSize * layoutOptions.lineHeight);
const measure = options.measures || [layoutOptions.width];
if (this.config.debugMode) {
console.log("Paragraph Layout: Calculating layout for text", {
text: processedText,
@@ -238,8 +235,7 @@ class ParagraphLayoutModule extends BaseModule {
options: layoutOptions
});
}
// Use the global Knuth and Plass algorithm function with proper parameters
const layout = window.kap(
processedText,
this.measureText.bind(this),
@@ -248,55 +244,47 @@ class ParagraphLayoutModule extends BaseModule {
layoutOptions.tolerance,
layoutOptions.demerits
);
// If layout failed, return null
if (!layout || !layout.breaks || !layout.nodes) {
console.warn("Paragraph Layout: Failed to calculate layout for text");
return null;
}
if (this.config.debugMode) {
console.log("Paragraph Layout: Layout calculated", {
breaks: layout.breaks.length,
nodes: layout.nodes.length
});
}
// Return layout data with original text for reference
return {
...layout,
originalText: text,
processedText: processedText,
processedText,
width: layoutOptions.width,
lineHeight: layoutOptions.lineHeight
lineHeight: layoutOptions.lineHeight,
lineHeightPx,
fontSize: layoutOptions.fontSize,
fontFamily: layoutOptions.fontFamily
};
} catch (error) {
console.error("Error calculating layout:", error);
return null;
}
}
/**
* Set debug mode
* @param {boolean} enabled - Whether debug mode should be enabled
*/
setDebugMode(enabled) {
// Use parent's updateConfig method
this.updateConfig({ debugMode: enabled });
console.log(`Paragraph Layout: Debug mode ${enabled ? 'enabled' : 'disabled'}`);
}
}
// Create the singleton instance
const ParagraphLayout = new ParagraphLayoutModule();
// Register with the module registry
if (window.moduleRegistry) {
window.moduleRegistry.register(ParagraphLayout);
}
// Export the module
export { ParagraphLayout };
// Keep a reference in window for loader system
window.ParagraphLayout = ParagraphLayout;