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
+311
View File
@@ -0,0 +1,311 @@
# Prototype Text Pipeline Analysis
## Overview
The prototype uses a sophisticated text processing pipeline that achieves professional typography through:
1. SmartyPants (typographic punctuation)
2. Hyphenopoly (hyphenation with pipe markers)
3. Knuth-Plass algorithm (optimal line breaking)
4. Precise character-by-character width measurement
5. Justification ratio application
## Complete Text Flow (Prototype)
```
User Text
SmartyPants.smartypantsu(text, 1)
↓ [Converts quotes, dashes to typographic characters]
hyphenator_en(..., '.hyphenatePipe')
↓ [Inserts | at hyphenation points]
kap(..., measureText, measure.toReversed(), true)
↓ [Knuth-Plass line breaking]
typesetParagraph(paragraph_data, delay, measure)
↓ [DOM creation with justification]
Rendered paragraph with proper spacing
```
## Key Components
### 1. Text Preprocessing (game.js:698)
```javascript
var preview_data = kap(
hyphenator_en(
SmartyPants.smartypantsu(text, 1),
'.hyphenatePipe' // ← CRITICAL: Uses pipe character
),
measureText,
measure.toReversed(),
true // ← hyphenation enabled
);
```
**Purpose**: Creates nodes with accurate widths and hyphenation points
### 2. Character Width Measurement (game.js:380-406)
```javascript
function measureText(str) {
// Special cases
if(str.substr(0, 2) == '</') { ... } // Closing tag
if(str.substr(0, 1) == '<') { ... } // Opening tag
if(str === '|') return 0; // ← Hyphen point has ZERO width
if (str === ' ') str = '\u00A0'; // Non-breaking space
// Actual measurement
ruler = rstack[rstack.length-1];
let textNode = document.createTextNode(str);
ruler.appendChild(textNode);
let width = ruler.getClientRects()[0].width; // ← REAL CSS width
ruler.removeChild(textNode);
return width;
}
```
**Key Insight**: Uses a hidden `#ruler` element to measure actual rendered widths with the exact font/size from CSS.
### 3. Measure Array (game.js:656-696)
The measure array defines different line widths:
**Chapter beginning (with drop cap)**:
```javascript
measure.push(containerWidth); // Full width
measure.push(containerWidth - indentWidth); // Indented
measure.push(containerWidth - indentWidth * 0.9); // More indented
```
**Regular paragraph (indented first line)**:
```javascript
measure.push(containerWidth); // Full width
measure.push(containerWidth - indentWidth * 0.5); // Half indent
```
**Important**: Array is reversed before passing to `kap()`: `measure.toReversed()`
### 4. Knuth-Plass Algorithm (knuth-and-plass.js:1-60)
```javascript
function kap(text, measureText, measure, hyphenation) {
// Strip pipes if hyphenation disabled
if (!hyphenation) {
text = text.replace(/\|/g, '');
}
// Split on: punctuation, spaces, pipes, tags
text.split(/([.,:;!?] |\s|\||<.*?>)/u).forEach(function (fragment) {
if (fragment === ' ') {
// Create glue with stretch/shrink
nodes.push(linebreak.glue(spaceWidth, stretch, shrink));
} else if (fragment === '|') {
// Create penalty node for hyphenation point
nodes.push(linebreak.penalty(hyphenWidth * 0.25, 100, 1));
} else {
// Create box node for word
nodes.push(linebreak.box(fragmentWidth, fragment));
}
});
// Run Knuth-Plass algorithm
let breaks = linebreak(nodes, measure, { tolerance: 3, demerits });
return { nodes, breaks };
}
```
**Node Types**:
- **box**: Word with fixed width (cannot break)
- **glue**: Space with stretch/shrink (for justification)
- **penalty**: Potential break point (like hyphen) with cost
### 5. Rendering with Justification (game.js:295-378)
```javascript
function typesetParagraph(paragraph_data, delay = 0, measure = []) {
// Create paragraph container
let p = document.createElement("p");
p.style.position = 'relative';
p.style.height = lineHeight * (paragraph_data.breaks.length - 1) + 'px';
// Iterate through lines
for(let i = 1; i < paragraph_data.breaks.length; i++) {
let left = 0;
let ratio = paragraph_data.breaks[i].ratio; // ← JUSTIFICATION RATIO
// Iterate through nodes on this line
for(let j = paragraph_data.breaks[i-1].position; j <= paragraph_data.breaks[i].position; j++) {
let node = paragraph_data.nodes[j];
if(node.type === 'box') {
// Handle hyphenated syllables (lines 316-320)
if(j > paragraph_data.breaks[i-1].position + 1 &&
paragraph_data.nodes[j-1].type === 'penalty' && lastChild) {
// Combine with previous syllable
syllable += '\u200c' + node.value; // Zero-width non-joiner
lastChild.innerHTML = syllable;
left += node.width;
} else {
// Create new word span
let word = document.createElement("span");
word.style.position = 'absolute';
word.style.top = lineHeight * (i - 1) * 100 / paragraph_height + '%';
word.style.left = left * 100 / line_width + '%';
word.innerHTML = node.value;
p.appendChild(word);
left += node.width;
}
}
else if(node.type === 'glue') {
// ← CRITICAL: Apply justification ratio to glue
if(ratio > 0) {
left += node.width + ratio * node.stretch;
} else {
left += node.width + ratio * node.shrink;
}
}
else if(node.type === 'penalty' && node.penalty === 100 && j === breaks[i].position) {
// Add hyphen at line break
let word = document.createElement("span");
word.innerHTML = "-";
p.appendChild(word);
}
}
}
return [p, delay];
}
```
**Key Points**:
1. **Justification**: Glue widths are adjusted by `ratio * stretch` or `ratio * shrink`
2. **Hyphenation**: Syllables after penalty nodes are combined with previous word using zero-width non-joiner
3. **Positioning**: All words use `position: absolute` with percentage-based coordinates
## Current Implementation Issues
### Issue 1: Text Processing Pipeline
**Current** (sentence-queue-module.js:266):
```javascript
const processedText = textProcessor ? await textProcessor.process(text) : text;
```
**Problem**:
- `textProcessor.process()` may not pass the correct selector to Hyphenopoly
- Hyphenopoly needs `.hyphenatePipe` selector to use pipe characters
**Fix Needed**:
```javascript
const processedText = textProcessor ?
await textProcessor.hyphenate(
textProcessor.smartyPants(text),
'.hyphenatePipe'
) : text;
```
### Issue 2: Hyphenation with Pipe Character
**Current** (text-processor-module.js:275-286):
```javascript
hyphenate(text) {
if (!this.isHyphenationAvailable()) return text;
try {
return this.hyphenator(text); // ← No selector parameter
} catch (error) {
console.error("Error hyphenating text:", error);
return text;
}
}
```
**Fix Needed**: Add selector parameter
```javascript
hyphenate(text, selector = null) {
if (!this.isHyphenationAvailable()) return text;
try {
return selector ?
this.hyphenator(text, selector) :
this.hyphenator(text);
} catch (error) {
console.error("Error hyphenating text:", error);
return text;
}
}
```
### Issue 3: Knuth-Plass Not Using Pipe Characters
**Current** (public/js/knuth-and-plass.js):
- May not properly split on pipe characters
- May not create penalty nodes
**Fix Needed**: Ensure knuth-and-plass.js matches prototype implementation
### Issue 4: Syllable Combination in Rendering
**Current** (layout-renderer-module.js):
- Does NOT combine hyphenated syllables
- Missing logic for `if(nodes[j-1].type === 'penalty' && lastChild)`
**Fix Needed**: Add syllable combination logic when rendering box nodes after penalty nodes
### Issue 5: Missing #ruler Element
**Current**: No `#ruler` element for text measurement
**Fix Needed**:
1. Add `<div id="ruler"></div>` to HTML
2. Use ruler for character width measurement in paragraph-layout-module.js
## Implementation Plan
### Phase 1: Fix Text Processing Pipeline
1. **Update text-processor-module.js**:
- Add `selector` parameter to `hyphenate()` method
- Update `process()` to pass `.hyphenatePipe` selector
2. **Update sentence-queue-module.js**:
- Pass `.hyphenatePipe` selector when calling text processor
- Ensure processedText includes pipe characters
### Phase 2: Fix Knuth-Plass Integration
1. **Verify knuth-and-plass.js**:
- Ensure it splits on `\|` character
- Creates `penalty` nodes with cost 100
- Handles HTML tags properly
2. **Update paragraph-layout-module.js**:
- Ensure `measureText()` returns 0 for `|` character
- Use `#ruler` element for measurement
- Handle HTML tag stack properly
### Phase 3: Fix Rendering with Justification
1. **Update layout-renderer-module.js**:
- Add syllable combination logic for hyphenated words
- Apply justification ratios to glue widths correctly
- Add hyphens at line breaks when penalty node is at break position
2. **Fix spacing issues**:
- Create space spans with adjusted widths
- Use zero-width non-joiner for syllable combination
### Phase 4: Testing & Refinement
1. **Test with simple text**: "This is a test."
2. **Test with hyphenation**: Long words that span lines
3. **Test with justification**: Full paragraphs
4. **Test with special characters**: Quotes, dashes, etc.
## Success Criteria
✅ SmartyPants converts quotes correctly
✅ Hyphenopoly inserts pipe characters
✅ Knuth-Plass creates proper breaks with hyphenation
✅ Words don't overlap
✅ Words have proper spacing (not smushed)
✅ Justification works (even spacing across line width)
✅ Hyphens appear at line breaks
✅ Drop caps and indentation work