Remove consolidated reference documentation
This commit is contained in:
@@ -1,311 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# Module System Refactoring TODO
|
|
||||||
|
|
||||||
## High Priority (Critical Architectural Issues)
|
|
||||||
|
|
||||||
### 1. Asynchronous Flow Control Improvements
|
|
||||||
- [ ] Remove all `setTimeout` calls used for synchronization in modules
|
|
||||||
- [X] Replace timeout in `browser-tts-handler.js` with proper Promise handling for voice loading
|
|
||||||
- [X] Eliminate race condition in `tts-player.js` that uses a hard-coded 1000ms timeout
|
|
||||||
- [ ] Remove all `setTimeout` calls in `ui-controller.js` for UI updates
|
|
||||||
- [ ] Implement proper Promise-based flow control in all modules
|
|
||||||
- [ ] Update `kokoro-handler.js` to correctly handle loading events
|
|
||||||
- [ ] Ensure all `async/await` patterns follow best practices
|
|
||||||
- [ ] Fix race conditions in module loading sequences
|
|
||||||
|
|
||||||
### 2. Module State Management
|
|
||||||
- [ ] Fix premature reporting of `FINISHED` state
|
|
||||||
- [ ] Ensure `tts-player.js` properly waits for Kokoro loading before reporting FINISHED
|
|
||||||
- [ ] Add proper state checks in all modules before reporting FINISHED
|
|
||||||
- [ ] Implement proper state transition reporting
|
|
||||||
- [ ] Update modules to use event system for reporting state transitions
|
|
||||||
- [ ] Add better error handling during module initialization
|
|
||||||
|
|
||||||
### 3. Module Dependencies & Loading
|
|
||||||
- [ ] Fix missing dependency declarations
|
|
||||||
- [ ] Update `ui-controller.js` to properly declare its TTS dependency
|
|
||||||
- [ ] Ensure all modules correctly specify all dependencies
|
|
||||||
- [ ] Remove dependency availability checks within modules
|
|
||||||
- [ ] Remove conditional checks like `if (!this.ttsHandler)` in `ui-controller.js`
|
|
||||||
- [ ] Rely on the module loader for dependency management
|
|
||||||
|
|
||||||
## Medium Priority (Functionality & Implementation Issues)
|
|
||||||
|
|
||||||
### 4. TTS Handler Implementation
|
|
||||||
- [ ] Implement missing `tts-handler.js` file content
|
|
||||||
- [ ] Create proper implementation with consistent interface
|
|
||||||
- [ ] Ensure it uses proper event-based communication
|
|
||||||
- [ ] Fix inconsistent event usage across TTS handlers
|
|
||||||
- [ ] Replace direct callbacks with event system
|
|
||||||
- [ ] Standardize event names and parameters
|
|
||||||
|
|
||||||
### 5. Animation Queue Enhancements
|
|
||||||
- [ ] Implement proper queue control mechanisms
|
|
||||||
- [ ] Add pause/resume functionality
|
|
||||||
- [ ] Implement more robust animation timing
|
|
||||||
- [ ] Add priority management for animations
|
|
||||||
|
|
||||||
### 6. UI Controller Cleanup
|
|
||||||
- [ ] Fix duplicate methods in UI Controller
|
|
||||||
- [ ] Deduplicate code for creating UI elements
|
|
||||||
- [ ] Consolidate event handling functions
|
|
||||||
- [ ] Remove redundant `ModuleEvent` class implementation
|
|
||||||
- [ ] Use the shared implementation from `base-module.js`
|
|
||||||
|
|
||||||
### 7. Kokoro Loading Implementation
|
|
||||||
- [ ] Implement proper `requestIdleCallback` for Kokoro loading
|
|
||||||
- [ ] Follow the pattern described in the specification
|
|
||||||
- [ ] Add progress reporting during Kokoro loading
|
|
||||||
- [ ] Fix event handling for loading completion
|
|
||||||
|
|
||||||
## Lower Priority (Refinements & Optimizations)
|
|
||||||
|
|
||||||
### 8. Code Quality & Consistency
|
|
||||||
- [ ] Standardize module registration pattern
|
|
||||||
- [ ] Ensure all modules follow the same pattern
|
|
||||||
- [ ] Fix inconsistencies in export approaches
|
|
||||||
- [ ] Improve module progress reporting
|
|
||||||
- [ ] Make progress reporting more granular
|
|
||||||
- [ ] Add more descriptive status messages
|
|
||||||
|
|
||||||
### 9. Error Handling Improvements
|
|
||||||
- [ ] Add better error recovery mechanisms
|
|
||||||
- [ ] Implement fallbacks for critical failures
|
|
||||||
- [ ] Add user-friendly error messages
|
|
||||||
- [ ] Improve error logging
|
|
||||||
- [ ] Add structured error reporting
|
|
||||||
- [ ] Implement debugging tools
|
|
||||||
|
|
||||||
### 10. Performance Optimizations
|
|
||||||
- [ ] Optimize module loading sequence
|
|
||||||
- [ ] Prioritize critical modules
|
|
||||||
- [ ] Defer non-essential loading
|
|
||||||
- [ ] Improve resource utilization
|
|
||||||
- [ ] Minimize memory footprint
|
|
||||||
- [ ] Reduce CPU usage during animations
|
|
||||||
|
|
||||||
## Documentation & Testing
|
|
||||||
|
|
||||||
### 11. Documentation
|
|
||||||
- [ ] Add JSDoc comments to all public methods
|
|
||||||
- [ ] Create architectural documentation
|
|
||||||
- [ ] Document module dependencies
|
|
||||||
- [ ] Explain event system
|
|
||||||
- [ ] Add example usage for modules
|
|
||||||
|
|
||||||
### 12. Testing
|
|
||||||
- [ ] Create unit tests for modules
|
|
||||||
- [ ] Implement integration tests for module system
|
|
||||||
- [ ] Add browser compatibility tests
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### 13. New Features
|
|
||||||
- [ ] Add module versioning support
|
|
||||||
- [ ] Implement module hot-reloading
|
|
||||||
- [ ] Create plugin system for extending modules
|
|
||||||
- [ ] Add internationalization support for UI
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
# Client Architecture Refactoring TODO List
|
|
||||||
|
|
||||||
## Enhanced SentenceQueueModule Implementation
|
|
||||||
|
|
||||||
### Phase 1: Core Structure and Design
|
|
||||||
|
|
||||||
1. **Design the Sentence Object Structure**
|
|
||||||
- [ ] Define comprehensive sentence object with fields for:
|
|
||||||
- Unique ID
|
|
||||||
- Original text
|
|
||||||
- Processed text (hyphenated, typeset)
|
|
||||||
- Layout information (breaks, nodes, typography)
|
|
||||||
- Audio component (player, duration, data, type)
|
|
||||||
- Status tracking (pending, processing, ready, playing, complete)
|
|
||||||
|
|
||||||
2. **Implement Basic Queue Management**
|
|
||||||
- [ ] Create methods for adding sentences to the queue
|
|
||||||
- [ ] Implement queue processing logic that maintains order
|
|
||||||
- [ ] Add status tracking for each sentence in the queue
|
|
||||||
- [ ] Implement priority handling for urgent sentences
|
|
||||||
|
|
||||||
### Phase 2: Text Processing Integration
|
|
||||||
|
|
||||||
1. **Integrate with Paragraph Layout**
|
|
||||||
- [ ] Connect to ParagraphLayoutModule for text processing
|
|
||||||
- [ ] Implement hyphenation and typesetting in the queue
|
|
||||||
- [ ] Store layout information in the sentence object
|
|
||||||
- [ ] Ensure layout processing happens in parallel with audio
|
|
||||||
|
|
||||||
2. **Text Animation Preparation**
|
|
||||||
- [ ] Calculate animation timing based on text length and settings
|
|
||||||
- [ ] Prepare animation data for each word in the sentence
|
|
||||||
- [ ] Store animation timing in the sentence object
|
|
||||||
- [ ] Create animation player function for the sentence
|
|
||||||
|
|
||||||
### Phase 3: Audio Processing Integration
|
|
||||||
|
|
||||||
1. **TTS System Integration**
|
|
||||||
- [ ] Implement audio generation for Kokoro TTS
|
|
||||||
- [ ] Implement browser TTS handling with duration estimation
|
|
||||||
- [ ] Implement "none" TTS option with duration calculation
|
|
||||||
- [ ] Create consistent player interface for all TTS types
|
|
||||||
|
|
||||||
2. **Audio Data Management**
|
|
||||||
- [ ] Implement audio data storage in sentence objects
|
|
||||||
- [ ] Connect with TTSFactoryModule's IndexedDB for persistent caching
|
|
||||||
- [ ] Add audio preloading capabilities
|
|
||||||
- [ ] Implement audio resource cleanup
|
|
||||||
|
|
||||||
### Phase 4: Playback Coordination
|
|
||||||
|
|
||||||
1. **Synchronized Playback**
|
|
||||||
- [ ] Implement coordinated text animation and audio playback
|
|
||||||
- [ ] Create timing adjustment based on speed settings
|
|
||||||
- [ ] Add event handling for playback states (start, pause, resume, complete)
|
|
||||||
- [ ] Implement sentence transition handling
|
|
||||||
|
|
||||||
2. **User Interaction Handling**
|
|
||||||
- [ ] Add support for fast-forwarding text/audio
|
|
||||||
- [ ] Implement pause/resume functionality
|
|
||||||
- [ ] Handle user interruptions gracefully
|
|
||||||
- [ ] Support skipping to next sentence
|
|
||||||
|
|
||||||
## Component Consolidation
|
|
||||||
|
|
||||||
### Phase 1: Identify and Remove Redundancies
|
|
||||||
|
|
||||||
1. **TTSPlayerModule Refactoring**
|
|
||||||
- [ ] Remove preloadedAudio Map (replaced by sentence objects)
|
|
||||||
- [ ] Remove preloadQueue (replaced by SentenceQueue)
|
|
||||||
- [ ] Update speak method to use SentenceQueue
|
|
||||||
- [ ] Refactor to be a thin wrapper around SentenceQueue
|
|
||||||
|
|
||||||
2. **UIDisplayHandlerModule Refactoring**
|
|
||||||
- [ ] Remove pendingParagraphs queue (replaced by SentenceQueue)
|
|
||||||
- [ ] Update displayText to use SentenceQueue
|
|
||||||
- [ ] Modify processNextParagraph to work with sentence objects
|
|
||||||
- [ ] Update event handling to work with the new architecture
|
|
||||||
|
|
||||||
3. **KokoroTTSModule Refactoring**
|
|
||||||
- [ ] Replace pendingGenerations Map with SentenceQueue integration
|
|
||||||
- [ ] Update generateSpeech to work with sentence objects
|
|
||||||
- [ ] Modify iframe communication to support the new structure
|
|
||||||
- [ ] Ensure backward compatibility during transition
|
|
||||||
|
|
||||||
4. **TextBufferModule Refactoring**
|
|
||||||
- [ ] Move sentence preparation logic to SentenceQueue
|
|
||||||
- [ ] Update text handling to work with the new architecture
|
|
||||||
- [ ] Ensure proper integration with SentenceQueue
|
|
||||||
- [ ] Maintain high-level text management responsibilities
|
|
||||||
|
|
||||||
### Phase 2: Interface Updates
|
|
||||||
|
|
||||||
1. **Update Module Interfaces**
|
|
||||||
- [ ] Create consistent interfaces for interacting with SentenceQueue
|
|
||||||
- [ ] Update event system to work with sentence objects
|
|
||||||
- [ ] Implement progress reporting for sentence processing
|
|
||||||
- [ ] Add debugging and monitoring capabilities
|
|
||||||
|
|
||||||
2. **Documentation and Examples**
|
|
||||||
- [ ] Document the new architecture and interfaces
|
|
||||||
- [ ] Create usage examples for common scenarios
|
|
||||||
- [ ] Update developer guidelines
|
|
||||||
- [ ] Add migration guide for existing code
|
|
||||||
|
|
||||||
## Testing and Validation
|
|
||||||
|
|
||||||
1. **Unit Testing**
|
|
||||||
- [ ] Create tests for SentenceQueue core functionality
|
|
||||||
- [ ] Test text processing integration
|
|
||||||
- [ ] Test audio processing integration
|
|
||||||
- [ ] Test playback coordination
|
|
||||||
|
|
||||||
2. **Integration Testing**
|
|
||||||
- [ ] Test interaction between SentenceQueue and other modules
|
|
||||||
- [ ] Validate timing and synchronization
|
|
||||||
- [ ] Test error handling and recovery
|
|
||||||
- [ ] Verify performance under load
|
|
||||||
|
|
||||||
3. **User Experience Testing**
|
|
||||||
- [ ] Validate text animation quality
|
|
||||||
- [ ] Test audio playback quality
|
|
||||||
- [ ] Verify synchronization from user perspective
|
|
||||||
- [ ] Test accessibility features
|
|
||||||
|
|
||||||
## Implementation Strategy
|
|
||||||
|
|
||||||
1. **Phased Rollout**
|
|
||||||
- [ ] Implement SentenceQueue core structure
|
|
||||||
- [ ] Add text processing integration
|
|
||||||
- [ ] Add audio processing integration
|
|
||||||
- [ ] Implement playback coordination
|
|
||||||
- [ ] Gradually replace existing components
|
|
||||||
|
|
||||||
2. **Backward Compatibility**
|
|
||||||
- [ ] Maintain support for existing interfaces during transition
|
|
||||||
- [ ] Implement adapter patterns where needed
|
|
||||||
- [ ] Add feature flags for enabling/disabling new architecture
|
|
||||||
- [ ] Create fallback mechanisms for error recovery
|
|
||||||
|
|
||||||
3. **Performance Optimization**
|
|
||||||
- [ ] Implement parallel processing where possible
|
|
||||||
- [ ] Optimize memory usage for sentence objects
|
|
||||||
- [ ] Add resource management for audio data
|
|
||||||
- [ ] Implement efficient queue processing algorithms
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
# AI Interactive Fiction Engine - Technical Documentation
|
|
||||||
|
|
||||||
## 1. Overview
|
|
||||||
|
|
||||||
This document provides a comprehensive technical explanation of the AI Interactive Fiction Engine implemented in `game.js`. The engine leverages inkjs for narrative management and incorporates advanced text rendering features, including custom animations, sophisticated typography using SmartyPants and Hyphenopoly, and optimal line breaking via the Knuth and Plass algorithm, to create rich, dynamic interactive fiction experiences.
|
|
||||||
|
|
||||||
## 2. Core Technologies
|
|
||||||
|
|
||||||
The engine is built upon several key technologies:
|
|
||||||
|
|
||||||
* **inkjs**: Manages the core interactive fiction logic, including story state, branching narrative, choices, and variables.
|
|
||||||
* **Knuth and Plass Line Breaking Algorithm**: Adapted implementation for optimal paragraph justification and line breaking, ensuring high-quality text layout. (Based on Bram Stein's "typeset" library).
|
|
||||||
* **Hyphenopoly**: Provides high-quality, language-aware text hyphenation, crucial for justified text layout.
|
|
||||||
* **SmartyPants**: Enhances typography by converting plain text elements (quotes, dashes) into their typographically correct equivalents.
|
|
||||||
* **Custom Animation System**: A queue-based system handles the precise timing and visual reveal of text elements.
|
|
||||||
* **JavaScript (ES6+)**: The primary implementation language.
|
|
||||||
* **HTML/CSS**: Used for structuring the UI and styling the visual presentation.
|
|
||||||
|
|
||||||
## 3. Engine Architecture and Flow
|
|
||||||
|
|
||||||
The engine follows a well-defined process from initialization to interactive playback.
|
|
||||||
|
|
||||||
### 3.1. Initialization (`window.onload`)
|
|
||||||
|
|
||||||
Execution begins when the window loads:
|
|
||||||
|
|
||||||
1. **Initial State Setup**: Global state variables (`running`, `fastForwardingAll`, `speech`, `speed`, `delay`) are initialized.
|
|
||||||
2. **Background Effects**: Lighting animations for background visuals are configured.
|
|
||||||
3. **Text Libraries**: Hyphenopoly is initialized for hyphenation support.
|
|
||||||
4. **Story Loading**: The compiled Ink story (JSON format) is fetched and loaded.
|
|
||||||
5. **Event Listeners**: Listeners for user interactions (keyboard input, clicks, slider changes) are attached.
|
|
||||||
6. **Playback Start**: The `continueStory()` function is called to begin rendering the narrative.
|
|
||||||
|
|
||||||
### 3.2. The Main Playback Loop (`continueStory()`)
|
|
||||||
|
|
||||||
The `continueStory()` function is the engine's core, orchestrating the display of narrative content and handling user choices.
|
|
||||||
|
|
||||||
**Step-by-step Process:**
|
|
||||||
|
|
||||||
1. **Setup**: Animation state variables (`fade_in`, `running`, `fastForwardingAll`) are reset for the current turn.
|
|
||||||
2. **Story Content Iteration**:
|
|
||||||
```javascript
|
|
||||||
while(story.canContinue) {
|
|
||||||
// Fetch next paragraph & associated tags from inkjs
|
|
||||||
var paragraphText = story.Continue();
|
|
||||||
var tags = story.currentTags;
|
|
||||||
|
|
||||||
// Process tags (Section 3.3)
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// Process and render text (Section 4)
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
The loop continues as long as inkjs indicates more content is available for the current narrative sequence.
|
|
||||||
|
|
||||||
3. **Choice Handling**: After exhausting the `story.canContinue` content, the engine checks for available choices:
|
|
||||||
```javascript
|
|
||||||
story.currentChoices.forEach(function(choice) {
|
|
||||||
// Create interactive DOM elements for each choice
|
|
||||||
// Attach event listeners to handle choice selection
|
|
||||||
// Apply appropriate styling
|
|
||||||
});
|
|
||||||
```
|
|
||||||
When a user selects a choice, an event handler calls `story.ChooseChoiceIndex(choice.index)` and then triggers `continueStory()` again to display the subsequent narrative branch.
|
|
||||||
|
|
||||||
4. **End of Story**: If `story.canContinue` is false and there are no choices, the story concludes or reaches a waiting point.
|
|
||||||
|
|
||||||
### 3.3. Tag Processing
|
|
||||||
|
|
||||||
Ink tags are used to control various non-textual aspects of the presentation:
|
|
||||||
|
|
||||||
* `AUDIO <url>`: Triggers playback of an audio file.
|
|
||||||
* `IMAGE <url>`: Displays an image.
|
|
||||||
* `BACKGROUND <url/color>`: Changes the background style.
|
|
||||||
* `CHAPTER`: Applies special formatting for chapter headings (often including drop caps).
|
|
||||||
* `SEPARATOR`: Inserts a decorative visual separator element.
|
|
||||||
* Custom CSS Classes: Tags can directly add CSS classes to paragraph elements for specific styling.
|
|
||||||
|
|
||||||
Tags are processed within the `continueStory` loop before the associated paragraph text is rendered.
|
|
||||||
|
|
||||||
## 4. Advanced Text Rendering Pipeline
|
|
||||||
|
|
||||||
A key feature of this engine is its sophisticated text rendering pipeline, designed to produce professional-quality typography and layout.
|
|
||||||
|
|
||||||
### 4.1. Overview
|
|
||||||
|
|
||||||
Text rendering involves several stages for each paragraph:
|
|
||||||
|
|
||||||
1. **Preprocessing**: Apply SmartyPants for typographic correctness and Hyphenopoly for hyphenation points.
|
|
||||||
2. **Line Breaking Calculation**: Use the Knuth and Plass algorithm (`kap` function) to determine optimal line breaks for the entire paragraph.
|
|
||||||
3. **Typesetting and DOM Creation**: Use the `typesetParagraph` function to generate and position DOM elements based on the line break data.
|
|
||||||
4. **Animation Scheduling**: Schedule the appearance of each text fragment using the custom animation system.
|
|
||||||
|
|
||||||
### 4.2. Knuth and Plass Line Breaking
|
|
||||||
|
|
||||||
The engine employs an adaptation of the Knuth and Plass line breaking algorithm to achieve superior text justification compared to standard browser methods.
|
|
||||||
|
|
||||||
#### 4.2.1. Origin and Adaptation
|
|
||||||
|
|
||||||
* The core logic is based on [Bram Stein's "typeset" library](https://github.com/bramstein/typeset).
|
|
||||||
* **Key Adaptations**:
|
|
||||||
* **HTML Tag Support**: Introduced `linebreak.tag()` nodes to preserve inline HTML formatting (e.g., `<b>`, `<i>`, custom spans) during line breaking.
|
|
||||||
* **Punctuation Handling**: Implemented custom logic for refined spacing around punctuation marks (`.,:;!?`), splitting width into symbolic (25%) and spacing (75%) components.
|
|
||||||
* **Hyphenation Integration**: Tightly integrated with Hyphenopoly, treating hyphenation points as potential breaks with a specific, reduced penalty (25% of normal hyphen width).
|
|
||||||
|
|
||||||
#### 4.2.2. Algorithm Principles
|
|
||||||
|
|
||||||
* **Optimization Problem**: Treats line breaking as minimizing the overall "badness" (demerits) of a paragraph's layout.
|
|
||||||
* **Global View**: Considers the entire paragraph at once, unlike greedy algorithms.
|
|
||||||
* **Box, Glue, Penalty Model**: Represents text as:
|
|
||||||
* **Boxes**: Unbreakable units (words, tagged elements).
|
|
||||||
* **Glue**: Flexible spaces that can stretch or shrink.
|
|
||||||
* **Penalties**: Opportunities for line breaks (e.g., after spaces, hyphens), with associated costs (demerits).
|
|
||||||
* **Demerits System**: Assigns penalties based on line tightness, hyphenation, and relationships between consecutive lines (fitness classes).
|
|
||||||
* **Optimal Path**: Finds the sequence of line breaks with the lowest total demerit score.
|
|
||||||
|
|
||||||
#### 4.2.3. Implementation (`knuth-and-plass.js`, `linebreak.js`)
|
|
||||||
|
|
||||||
* **`knuth-and-plass.js` (Adapter Layer)**:
|
|
||||||
* Provides the `kap(text, measureText, measure, hyphenation)` function.
|
|
||||||
* Parses the input text (already processed by SmartyPants/Hyphenopoly) into Box, Glue, Penalty, and Tag nodes.
|
|
||||||
* Handles special spacing rules for punctuation.
|
|
||||||
* Interfaces with the core `linebreak.js` module.
|
|
||||||
* **`linebreak.js` (Core Algorithm)**:
|
|
||||||
* Contains the `linebreak(nodes, lines, settings)` function.
|
|
||||||
* Implements the main algorithm logic: ratio calculation, demerit calculation, fitness class tracking, active node management (using the LinkedList), and path finding.
|
|
||||||
* Defines node types: `linebreak.box()`, `linebreak.glue()`, `linebreak.penalty()`, `linebreak.tag()`.
|
|
||||||
* **Customization Parameters**:
|
|
||||||
* `tolerance`: Controls acceptable line stretch/shrink (default: 2).
|
|
||||||
* `demerits`: Configurable penalties for lines, flagged breaks (e.g., consecutive hyphens), and fitness class mismatches.
|
|
||||||
|
|
||||||
#### 4.2.4. Supporting Data Structure: LinkedList (`linked-list.js`)
|
|
||||||
|
|
||||||
* A custom, modern ES6 implementation of a doubly linked list, derived from the `typeset` library.
|
|
||||||
* **Purpose**: Efficiently manages the "active nodes" (potential breakpoints being considered) within the `linebreak.js` algorithm. Critical for performance.
|
|
||||||
* **Features**: ES6 class syntax, getter methods (`size`, `first`), method chaining.
|
|
||||||
* **Known Bug**: The `get last()` method incorrectly references `this.last` instead of `this.tail`, potentially causing infinite recursion if called. **This should be corrected to `return this.tail;`**.
|
|
||||||
|
|
||||||
#### 4.2.5. Advantages
|
|
||||||
|
|
||||||
* Superior justification and word spacing.
|
|
||||||
* Reduced and more aesthetically placed hyphenation.
|
|
||||||
* Avoids jarringly tight or loose lines.
|
|
||||||
* Produces professional, book-like typography.
|
|
||||||
* Preserves rich text formatting during layout optimization.
|
|
||||||
|
|
||||||
### 4.3. Typography Enhancements (SmartyPants)
|
|
||||||
|
|
||||||
* Before line breaking, text is processed by `SmartyPants.smartypantsu(text, 1)`.
|
|
||||||
* This converts:
|
|
||||||
* Straight quotes (`"`, `'`) to curly quotes (“ ”, ‘ ’).
|
|
||||||
* Double hyphens (`--`) to em-dashes (—).
|
|
||||||
* Triple hyphens (`---`) to em-dashes (—) (configurable).
|
|
||||||
* Three periods (`...`) to ellipses (…).
|
|
||||||
|
|
||||||
### 4.4. Hyphenation (Hyphenopoly)
|
|
||||||
|
|
||||||
* Applied after SmartyPants using `hyphenator_en(text, '.hyphenatePipe')`.
|
|
||||||
* Inserts soft hyphen characters (`­` or similar markers) at valid break points within words, based on linguistic rules.
|
|
||||||
* These potential hyphenation points are then treated as `Penalty` nodes by the line-breaking algorithm, allowing for more flexible justification.
|
|
||||||
|
|
||||||
### 4.5. Typesetting and DOM Rendering (`typesetParagraph()`)
|
|
||||||
|
|
||||||
This function translates the calculated layout data from `kap` into visible, animated DOM elements.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function typesetParagraph(paragraph_data, delay = 0, measure = []) {
|
|
||||||
// ... setup paragraph element <p> ...
|
|
||||||
|
|
||||||
// Iterate through lines defined by paragraph_data.breaks
|
|
||||||
for(let i = 1; i < paragraph_data.breaks.length; i++) {
|
|
||||||
// Iterate through nodes (words, spaces, hyphens, tags) within the line
|
|
||||||
for(let j = paragraph_data.breaks[i-1].position; j <= paragraph_data.breaks[i].position; j++) {
|
|
||||||
const node = paragraph_data.nodes[j];
|
|
||||||
// Create appropriate DOM element (e.g., <span> for words/tags)
|
|
||||||
// Set absolute position (left, top) based on calculated widths and line height
|
|
||||||
// Apply styles (opacity: 0 initially)
|
|
||||||
// Schedule fade-in animation using scheduleTimeout() (Section 5)
|
|
||||||
// Update the running 'delay' total for the next element
|
|
||||||
}
|
|
||||||
// Handle line breaks, adjusting vertical position
|
|
||||||
}
|
|
||||||
|
|
||||||
return [p, delay]; // Return the populated <p> element and the final delay value
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Each word, space, or preserved tag becomes a separate, absolutely positioned DOM element (typically a `<span>`).
|
|
||||||
* Positions are determined precisely based on the widths calculated during the line-breaking phase.
|
|
||||||
* Initial opacity is set to 0, and fade-in animations are scheduled sequentially.
|
|
||||||
|
|
||||||
## 5. Animation System
|
|
||||||
|
|
||||||
The engine features a sophisticated animation system for revealing text dynamically.
|
|
||||||
|
|
||||||
### 5.1. Animation Queue (`timeoutQueue`)
|
|
||||||
|
|
||||||
* A central array `timeoutQueue = []` tracks all pending `setTimeout` calls.
|
|
||||||
* Each entry is an object storing the function to execute, its arguments, and the `timeoutId`.
|
|
||||||
|
|
||||||
### 5.2. Scheduling Animations (`scheduleTimeout`)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function scheduleTimeout(func, delay, ...args) {
|
|
||||||
// Creates timeoutObject with execute method and null timeoutId
|
|
||||||
// Uses setTimeout to schedule execution after 'delay'
|
|
||||||
// Inside setTimeout callback: executes func, removes object from timeoutQueue
|
|
||||||
// Pushes timeoutObject onto timeoutQueue
|
|
||||||
// Returns the timeoutId
|
|
||||||
}
|
|
||||||
```
|
|
||||||
* Provides a managed way to schedule functions, ensuring they are tracked.
|
|
||||||
* Enables batch operations like fast-forwarding or cancellation.
|
|
||||||
|
|
||||||
### 5.3. Timing and Delay (`window.delay`, `window.speed`)
|
|
||||||
|
|
||||||
* `window.delay`: Accumulates the total delay time as elements are scheduled. Each new element's animation starts after the previous one finishes.
|
|
||||||
* `window.speed`: Controls the duration of each word's fade-in, typically adjusted by a UI slider. The delay increment for a word is often calculated like `delay += window.speed * word.length;`.
|
|
||||||
* Speed Adjustment: The UI slider uses a non-linear function (`Math.pow(100.0 - value, 3) / 10000 * 10 + 0.01`) for finer control at slower speeds.
|
|
||||||
|
|
||||||
### 5.4. Fast Forwarding (`fastForward`, `fastForwardingAll`)
|
|
||||||
|
|
||||||
* The `fastForward()` function:
|
|
||||||
1. Iterates through `timeoutQueue`.
|
|
||||||
2. Calls `clearTimeout()` for each pending animation.
|
|
||||||
3. Immediately executes the scheduled function (`timeoutObject.execute()`).
|
|
||||||
4. Clears the `timeoutQueue`.
|
|
||||||
5. Resets `window.delay` to 0.
|
|
||||||
* Triggered by user actions (spacebar, click) or programmatically.
|
|
||||||
* `window.fastForwardingAll` flag indicates if fast-forward is continuously active (e.g., holding space).
|
|
||||||
* Visual Feedback: The page border changes color (e.g., red) when `fastForwardingAll` is true.
|
|
||||||
|
|
||||||
### 5.5. Animation Interruption (`stopAllTimeouts`)
|
|
||||||
|
|
||||||
* The `stopAllTimeouts()` function:
|
|
||||||
1. Iterates through `timeoutQueue`.
|
|
||||||
2. Calls `clearTimeout()` for each pending animation.
|
|
||||||
3. Clears the `timeoutQueue`.
|
|
||||||
* Used during navigation, loading saves, or story resets to prevent outdated animations from playing.
|
|
||||||
|
|
||||||
## 6. Additional Features
|
|
||||||
|
|
||||||
### 6.1. Text-to-Speech (TTS)
|
|
||||||
|
|
||||||
* Integrates with an external TTS service (e.g., ElevenLabs API).
|
|
||||||
* A UI button toggles speech playback.
|
|
||||||
* Attempts to synchronize audio playback with text animation reveal (implementation details not fully specified in the source document).
|
|
||||||
* Playback can be interrupted by user interactions or fast-forwarding.
|
|
||||||
|
|
||||||
### 6.2. State Management and Persistence
|
|
||||||
|
|
||||||
* **Ink State**: The core narrative state (current position, variable values, etc.) is managed by inkjs and can be serialized using `story.state.toJson()`.
|
|
||||||
* **Local Storage**: Used to persist the serialized Ink state and potentially other UI/game states (like rendered history) across browser sessions.
|
|
||||||
* **Save/Load**: Functionality allows users to explicitly save the current state and reload it later. This involves restoring the Ink state via `story.state.loadJson()` and potentially re-rendering the story history up to that point.
|
|
||||||
|
|
||||||
## 7. External Libraries & Credits
|
|
||||||
|
|
||||||
The engine relies on the following external libraries:
|
|
||||||
|
|
||||||
* **inkjs**
|
|
||||||
* **Author**: Yannick Lohse (y-lohse)
|
|
||||||
* **Source**: [https://github.com/y-lohse/inkjs](https://github.com/y-lohse/inkjs)
|
|
||||||
* **Description**: JavaScript port of Inkle's Ink narrative scripting language.
|
|
||||||
* **Hyphenopoly**
|
|
||||||
* **Author**: Hermann Monnich (mnater)
|
|
||||||
* **Source**: [https://github.com/mnater/Hyphenopoly](https://github.com/mnater/Hyphenopoly)
|
|
||||||
* **Website**: [https://mnater.github.io/Hyphenopoly/](https://mnater.github.io/Hyphenopoly/)
|
|
||||||
* **Description**: High-quality JavaScript hyphenation library.
|
|
||||||
* **SmartyPants**
|
|
||||||
* **Original Author**: John Gruber
|
|
||||||
* **JavaScript Port Author**: Example: Othree (othree)
|
|
||||||
* **Source (Example Port)**: [https://github.com/othree/smartypants.js](https://github.com/othree/smartypants.js)
|
|
||||||
* **Description**: Typography prettifier for quotes, dashes, etc.
|
|
||||||
* **typeset (Basis for Knuth & Plass / LinkedList)**
|
|
||||||
* **Author**: Bram Stein (bramstein)
|
|
||||||
* **Source**: [https://github.com/bramstein/typeset](https://github.com/bramstein/typeset)
|
|
||||||
* **Description**: Original JavaScript implementation of the Knuth-Plass line breaking algorithm, adapted for this engine.
|
|
||||||
|
|
||||||
## 8. Practical Refactoring Recommendations (Updated)
|
|
||||||
|
|
||||||
Based on the detailed analysis of the engine's functionality and the goal of optimizing for **modularity, separation of concerns, and reusability**, the following refined refactoring strategy is recommended. This approach breaks the system into highly focused, potentially reusable components, moving away from reliance on global state and tightly coupled logic.
|
|
||||||
|
|
||||||
### Core Component Modules:
|
|
||||||
|
|
||||||
1. **`AnimationQueue` (`animation-queue.js`)**
|
|
||||||
* **Responsibility**: Solely manage the timing and execution queue for all scheduled animations (primarily text reveal).
|
|
||||||
* **Core Functions**: `schedule(func, delay, ...args)`, `fastForward()`, `stop()`, `setSpeed(value)`.
|
|
||||||
* **State**: Manages the internal `queue` array, the current `delay` accumulator, and the animation `speed`.
|
|
||||||
* **Reusability**: Highly reusable for any system requiring sequenced, timed execution with speed control and interruption.
|
|
||||||
|
|
||||||
2. **`TextProcessor` (`text-processor.js`)**
|
|
||||||
* **Responsibility**: Encapsulate text pre-processing steps required before layout calculation.
|
|
||||||
* **Core Functions**: `process(text)` method that applies typographic enhancements (SmartyPants) and hyphenation (Hyphenopoly).
|
|
||||||
* **Dependencies**: Takes instances or functions of the SmartyPants and Hyphenopoly libraries during construction.
|
|
||||||
* **Reusability**: Reusable wherever this specific text processing pipeline (SmartyPants + Hyphenopoly) is needed.
|
|
||||||
|
|
||||||
3. **`ParagraphLayout` (`paragraph-layout.js`)**
|
|
||||||
* **Responsibility**: Interface with the Knuth-Plass line breaking algorithm (`kap` function) to calculate optimal line breaks.
|
|
||||||
* **Core Functions**: `calculateLayout(processedText, measures)` method.
|
|
||||||
* **Dependencies**: Takes the `kap` function and a `measureText` function (capable of measuring text widths in the target rendering context) during construction.
|
|
||||||
* **Reusability**: Reusable in any system needing high-quality paragraph line breaking, provided the `kap` algorithm and a text measurement function are supplied.
|
|
||||||
|
|
||||||
4. **`LayoutRenderer` (`layout-renderer.js`)**
|
|
||||||
* **Responsibility**: Translate the abstract layout data (from `ParagraphLayout`) into concrete visual elements (DOM nodes in this case). Handle visual tag rendering.
|
|
||||||
* **Core Functions**: `renderParagraph(layoutData, measures)` creates positioned DOM elements (e.g., `<span>` for words/tags), applies styles (initial opacity 0), and *uses* `AnimationQueue.schedule` to initiate fade-in animations. `renderVisualTag(tagType, tagData)` handles rendering for `IMAGE`, `BACKGROUND`, `CHAPTER`, `SEPARATOR`, and applying CSS classes.
|
|
||||||
* **Dependencies**: Takes an `AnimationQueue` instance and potentially configuration for visual elements.
|
|
||||||
* **Reusability**: Moderately reusable. The core logic of translating layout data is reusable, but the specific DOM creation part is tied to HTML/DOM rendering. Could be adapted for other rendering targets (e.g., Canvas).
|
|
||||||
|
|
||||||
5. **`AudioManager` (`audio-manager.js`)**
|
|
||||||
* **Responsibility**: Manage loading and playback of non-TTS audio effects triggered by tags (e.g., `AUDIO <url>`).
|
|
||||||
* **Core Functions**: `loadSound(id, url)`, `playSound(id)`, `stopSound(id)`, `stopAllSounds()`.
|
|
||||||
* **Reusability**: Highly reusable component for basic audio management in web applications.
|
|
||||||
|
|
||||||
6. **`TtsPlayer` (`tts-player.js`)**
|
|
||||||
* **Responsibility**: Handle interactions with the Text-to-Speech service (e.g., ElevenLabs API), manage playback, and potentially synchronize with the `AnimationQueue`.
|
|
||||||
* **Core Functions**: `speak(text)`, `stopSpeaking()`, `setVoice(voiceId)`, `configure(apiKey, ...)`.
|
|
||||||
* **Dependencies**: May need the `AnimationQueue` for synchronization if required.
|
|
||||||
* **Reusability**: Reusable for adding TTS functionality, specific implementation depends on the chosen TTS provider API.
|
|
||||||
|
|
||||||
7. **`PersistenceManager` (`persistence-manager.js`)**
|
|
||||||
* **Responsibility**: Handle saving and loading the game state.
|
|
||||||
* **Core Functions**: `saveState(stateObject)`, `loadState()`. `stateObject` would contain Ink state JSON, potentially UI state, scroll position, etc.
|
|
||||||
* **Dependencies**: Configurable storage backend (e.g., `localStorage` wrapper).
|
|
||||||
* **Reusability**: Highly reusable for saving/loading application state to various storage mechanisms.
|
|
||||||
|
|
||||||
8. **`InkStoryPlayer` (`ink-story-player.js`)**
|
|
||||||
* **Responsibility**: Orchestrate the narrative flow specific to the Ink story. Manage the `inkjs.Story` instance.
|
|
||||||
* **Core Functions**: `loadStory(storyContentJson)`, `continueStory(containerElement)`, `chooseChoice(index)`. It drives the `story.Continue()` loop, gets text and tags. It *delegates* tasks:
|
|
||||||
* Text processing -> `TextProcessor`
|
|
||||||
* Layout calculation -> `ParagraphLayout`
|
|
||||||
* Rendering paragraphs/visuals -> `LayoutRenderer`
|
|
||||||
* Handling `AUDIO` tags -> `AudioManager`
|
|
||||||
* Handling speech requests (if implemented via tags) -> `TtsPlayer`
|
|
||||||
* Saving/Loading -> `PersistenceManager`
|
|
||||||
* **Dependencies**: `inkjs.Story` class, `TextProcessor`, `ParagraphLayout`, `LayoutRenderer`, `AudioManager`, `TtsPlayer`, `PersistenceManager`.
|
|
||||||
* **Reusability**: Specific to using Ink narratives but uses reusable components for its operations.
|
|
||||||
|
|
||||||
9. **`UiController` (`ui-controller.js`)**
|
|
||||||
* **Responsibility**: Manage user interface interactions (event listeners) and update UI elements (sliders, buttons, status indicators).
|
|
||||||
* **Core Functions**: `setupEventListeners()`, methods to handle specific events (e.g., `handleSpeedChange`, `handleFastForwardToggle`, `handleChoiceClick`, `handleSaveClick`, `handleLoadClick`, `handleTtsToggle`).
|
|
||||||
* **Dependencies**: Interacts primarily with `InkStoryPlayer` (e.g., to choose choice, trigger save/load), `AnimationQueue` (to set speed, trigger fast-forward), `TtsPlayer` (to toggle speech), `AudioManager` (potentially to control master volume).
|
|
||||||
* **Reusability**: Specific to the application's UI structure but follows a standard controller pattern.
|
|
||||||
|
|
||||||
### Main Application Integration (`animated-fiction.js` or `main.js`):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// main.js (Example Setup)
|
|
||||||
import { AnimationQueue } from './animation-queue.js';
|
|
||||||
import { TextProcessor } from './text-processor.js';
|
|
||||||
// ... import other modules ...
|
|
||||||
import { Story as InkStory } from './ink.js'; // Assuming inkjs is available
|
|
||||||
|
|
||||||
class AnimatedFictionApp {
|
|
||||||
constructor(config) {
|
|
||||||
this.config = config; // Story content URL, container element, API keys, etc.
|
|
||||||
this.storyContent = null; // Loaded later
|
|
||||||
|
|
||||||
// 1. Instantiate Core Components
|
|
||||||
this.animationQueue = new AnimationQueue();
|
|
||||||
// Provide external libraries/functions to components that need them
|
|
||||||
this.textProcessor = new TextProcessor(SmartyPants, hyphenator_en); // Assuming these are globally available or imported
|
|
||||||
this.paragraphLayout = new ParagraphLayout(kap, this.measureDomText.bind(this)); // `kap` algorithm, bind measure func
|
|
||||||
this.layoutRenderer = new LayoutRenderer(this.animationQueue);
|
|
||||||
this.audioManager = new AudioManager();
|
|
||||||
this.ttsPlayer = new TtsPlayer({ apiKey: config.ttsApiKey /*, optional animationQueue */ });
|
|
||||||
this.persistenceManager = new PersistenceManager({ storage: localStorage }); // Configure storage backend
|
|
||||||
|
|
||||||
// 2. Instantiate Orchestrator & UI
|
|
||||||
this.storyPlayer = new InkStoryPlayer({
|
|
||||||
InkStory: InkStory, // Pass the inkjs Story constructor
|
|
||||||
textProcessor: this.textProcessor,
|
|
||||||
paragraphLayout: this.paragraphLayout,
|
|
||||||
layoutRenderer: this.layoutRenderer,
|
|
||||||
audioManager: this.audioManager,
|
|
||||||
ttsPlayer: this.ttsPlayer,
|
|
||||||
persistenceManager: this.persistenceManager,
|
|
||||||
});
|
|
||||||
this.uiController = new UiController({
|
|
||||||
storyPlayer: this.storyPlayer,
|
|
||||||
animationQueue: this.animationQueue,
|
|
||||||
ttsPlayer: this.ttsPlayer,
|
|
||||||
// Pass references to DOM elements (buttons, sliders)
|
|
||||||
speedSliderElement: document.getElementById('speedSlider'),
|
|
||||||
choiceContainerElement: document.getElementById('choices'),
|
|
||||||
// ... other elements
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method needed by ParagraphLayout, tied to rendering context
|
|
||||||
measureDomText(text, style = '') {
|
|
||||||
// Implementation to measure text width in the DOM
|
|
||||||
// (e.g., create temporary span, apply style, measure offsetWidth)
|
|
||||||
// ... return width ...
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
// Fetch story content, etc.
|
|
||||||
const response = await fetch(this.config.storyUrl);
|
|
||||||
this.storyContent = await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if (!this.storyContent) throw new Error("Story not loaded");
|
|
||||||
|
|
||||||
// Setup initial states, load saved game if applicable
|
|
||||||
const savedState = this.persistenceManager.loadState();
|
|
||||||
const initialInkState = savedState ? savedState.inkJson : null;
|
|
||||||
|
|
||||||
this.storyPlayer.loadStory(this.storyContent, initialInkState);
|
|
||||||
this.uiController.setupEventListeners();
|
|
||||||
// Set initial speed from config or saved state
|
|
||||||
this.animationQueue.setSpeed(this.config.initialSpeed || 0.05);
|
|
||||||
|
|
||||||
// Begin the story
|
|
||||||
this.storyPlayer.continueStory(document.getElementById(this.config.storyContainerId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize and start the application
|
|
||||||
window.onload = async () => {
|
|
||||||
const app = new AnimatedFictionApp({
|
|
||||||
storyUrl: 'story.json',
|
|
||||||
storyContainerId: 'story',
|
|
||||||
ttsApiKey: 'YOUR_API_KEY' // Example config
|
|
||||||
// ... other configurations
|
|
||||||
});
|
|
||||||
await app.load();
|
|
||||||
app.start();
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits of this Refined Structure:
|
|
||||||
|
|
||||||
* **High Modularity**: Each class has a single, well-defined responsibility.
|
|
||||||
* **Improved Reusability**: Components like `AnimationQueue`, `TextProcessor`, `ParagraphLayout`, `AudioManager`, `TtsPlayer`, `PersistenceManager` have minimal dependencies on the specific interactive fiction context and can be reused elsewhere.
|
|
||||||
* **Clear Separation of Concerns**: Narrative logic (`InkStoryPlayer`) is separated from rendering (`LayoutRenderer`), timing (`AnimationQueue`), text manipulation (`TextProcessor`), UI (`UiController`), and side effects (Audio/TTS/Persistence).
|
|
||||||
* **Testability**: Individual components can be unit-tested more easily by mocking their dependencies.
|
|
||||||
* **Maintainability**: Changes within one module are less likely to impact others. Replacing a component (e.g., swapping the TTS provider) becomes more manageable.
|
|
||||||
|
|
||||||
This refactoring focuses squarely on creating independent, reusable building blocks, aligning perfectly with the goals specified.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Animated Fiction Engine - Modular Refactoring
|
|
||||||
|
|
||||||
This directory contains the refactored, modular version of the interactive fiction engine, previously implemented monolithically in `references/game.js`. The refactoring follows the recommendations outlined in Chapter 8 of `references/Documentation.md`.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The engine is now broken down into several distinct, reusable components, each with a specific responsibility. This improves maintainability, testability, and allows for easier integration into different projects or replacement of individual components.
|
|
||||||
|
|
||||||
## Modules
|
|
||||||
|
|
||||||
The core modules are:
|
|
||||||
|
|
||||||
1. **`animation-queue.js` (`AnimationQueue`)**: Manages the timing and execution queue for animations, primarily text reveal. Handles scheduling, fast-forwarding, stopping, and speed control.
|
|
||||||
2. **`text-processor.js` (`TextProcessor`)**: Encapsulates text pre-processing steps (SmartyPants for typography, Hyphenopoly for hyphenation).
|
|
||||||
3. **`paragraph-layout.js` (`ParagraphLayout`)**: Interfaces with the Knuth-Plass line breaking algorithm (`kap` function) to calculate optimal paragraph layouts. Requires a text measurement function.
|
|
||||||
4. **`layout-renderer.js` (`LayoutRenderer`)**: Translates the calculated layout data into DOM elements, handles visual tag rendering (images, backgrounds, etc.), and schedules animations using the `AnimationQueue`.
|
|
||||||
5. **`audio-manager.js` (`AudioManager`)**: Manages loading and playback of non-TTS audio effects triggered by Ink tags (`AUDIO`, `AUDIOLOOP`).
|
|
||||||
6. **`tts-player.js` (`TtsPlayer`)**: Handles interactions with Text-to-Speech services (using `tts-factory.js` for selection) and manages TTS playback.
|
|
||||||
7. **`persistence-manager.js` (`PersistenceManager`)**: Handles saving and loading the game state (Ink state JSON and rendered history) using a configurable storage backend (defaulting to `localStorage`).
|
|
||||||
8. **`ink-story-player.js` (`InkStoryPlayer`)**: Orchestrates the Ink narrative flow. Manages the `inkjs.Story` instance, processes story content and tags, and delegates tasks to other modules (text processing, layout, rendering, audio, TTS, persistence).
|
|
||||||
9. **`ui-controller.js` (`UiController`)**: Manages user interface interactions (buttons, sliders, keyboard shortcuts) and updates UI elements. Interacts with other modules to trigger actions (e.g., change speed, save/load, choose choice).
|
|
||||||
10. **`animated-fiction.js` (`AnimatedFiction`)**: The main application class that integrates all the modules. It handles initialization, loading the story, and starting the application flow.
|
|
||||||
|
|
||||||
## Supporting Libraries
|
|
||||||
|
|
||||||
This engine relies on several external and internal libraries located in `public/js`:
|
|
||||||
|
|
||||||
* `ink.js` (Loaded via CDN in `modular-index.html`, wrapped in `window.inkjs`)
|
|
||||||
* `smartypants.js`
|
|
||||||
* `linked-list.js` (Used by `linebreak.js`)
|
|
||||||
* `linebreak.js` (Core Knuth-Plass algorithm)
|
|
||||||
* `knuth-and-plass.js` (Adapter for `linebreak.js`)
|
|
||||||
* `Hyphenopoly_Loader.js` & `Hyphenopoly.js` (Hyphenation library)
|
|
||||||
* `kokoro-js.js`, `kokoro-handler.js`, `tts-handler.js`, `tts-factory.js` (Text-to-Speech components)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. **Include Libraries**: Ensure all necessary supporting libraries (`smartypants.js`, `ink.js`, `Hyphenopoly`, etc.) are loaded in your HTML file *before* the main application module.
|
|
||||||
2. **Include Main Module**: Load the main application module (`animated-fiction.js`) using `<script type="module">`.
|
|
||||||
3. **HTML Structure**: Ensure your HTML has the necessary container elements (e.g., `#story`, `#choices`) and UI controls (buttons, sliders) with the expected IDs, or configure the `UiController` with the correct element references.
|
|
||||||
4. **Initialization**: The `animated-fiction.js` script will automatically instantiate the `AnimatedFiction` class on `window.onload`, load the story specified in its configuration (`Herrenhaus.ink.json` by default), and start the interactive experience.
|
|
||||||
|
|
||||||
Refer to `public/modular-index.html` for a working example of how to structure the HTML and include the necessary scripts.
|
|
||||||
|
|
||||||
## Original Files
|
|
||||||
|
|
||||||
* The original monolithic implementation can be found in `references/game.js`.
|
|
||||||
* The documentation detailing the original implementation and the refactoring plan is in `references/Documentation.md`.
|
|
||||||
@@ -1,598 +0,0 @@
|
|||||||
# Code Guidlines
|
|
||||||
|
|
||||||
**1. Asynchronous Programming Principles:**
|
|
||||||
|
|
||||||
* **Primary Mechanism:** Use `async`/`await` and Promises for handling asynchronous operations.
|
|
||||||
* **Non-Blocking:** Ensure the main thread remains responsive. Long-running operations (like Kokoro loading) should be handled in a way that doesn't block UI updates or animations (e.g., using `requestIdleCallback` if appropriate, or careful yielding).
|
|
||||||
* **Event-Driven Communication:** Use a dedicated event system (like the `ModuleEvent` class created) for communication between the loader and modules (e.g., for progress updates, state changes, messages) instead of injecting callbacks directly from the loader into module methods.
|
|
||||||
|
|
||||||
**2. Module System Standards & Dependency Management:**
|
|
||||||
|
|
||||||
* **Native ES Modules:** Utilize the browser's native ES Module system (`import`/`export`, `<script type="module">`) without relying on build tools.
|
|
||||||
* **Lean Loader:** The `loader.js` file should be focused *only* on:
|
|
||||||
* Orchestrating the loading of module scripts.
|
|
||||||
* Monitoring module initialization progress and state via the event system.
|
|
||||||
* Displaying the loading status UI.
|
|
||||||
* Hiding the overlay and potentially starting the main application loop *after* all modules are finished.
|
|
||||||
* **Module Responsibility:** All module-specific logic, configuration, resource loading (like CSS, images, or specific libraries like Kokoro), and detailed progress reporting should reside *within* the respective module file, not in `loader.js`.
|
|
||||||
* **Dependency Declaration:** Modules must declare their dependencies (e.g., `ui-controller` depends on `tts` and `animation-queue`).
|
|
||||||
* **Loader Enforces Order:** The loader is responsible for ensuring that a module's `init` phase only begins *after* all its declared dependencies have reached the `FINISHED` state.
|
|
||||||
* **Rely on Dependency Management:** Modules should *assume* their dependencies will be loaded and ready before their `init` function is called by the loader. There should be **no** conditional checks within a module like `if (dependencyModule)` with fallbacks for when the dependency isn't ready.
|
|
||||||
|
|
||||||
**3. Module Interface & Code Sharing:**
|
|
||||||
|
|
||||||
* **Base Class:** Use a `BaseModule` class that all modules extend. This enforces a consistent interface (e.g., `initializeInterface`, `getState`) and provides shared functionality (e.g., `changeState`, `reportProgress`, event dispatching).
|
|
||||||
* **Module Registry:** Use a central `moduleRegistry` to register modules and facilitate dependency checking and management.
|
|
||||||
* **Preserve Functionality:** When adapting existing modules (like `ui-controller`) to the new `BaseModule` interface, all original functionality must be preserved and integrated correctly, not replaced with placeholders.
|
|
||||||
|
|
||||||
**4. State Management:**
|
|
||||||
|
|
||||||
* **Defined States:** Modules must adhere to the defined states: `PENDING`, `LOADING` (script loading), `WAITING` (waiting for dependencies), `INITIALIZING` (running `init` logic), `FINISHED`, `ERROR`.
|
|
||||||
* **Accurate Reporting:** Modules must accurately report their state transitions via the event system. A module (like `tts`) should not report `FINISHED` until all its critical internal operations (including background loading like Kokoro) are complete. The loader's UI must display these states correctly.
|
|
||||||
|
|
||||||
**5. Handling `setTimeout` and Fallbacks:**
|
|
||||||
|
|
||||||
* **`setTimeout` for Flow Control/Synchronization:** **Strictly prohibited.** Using `setTimeout` to wait for asynchronous operations to complete, fix timing issues, or manage dependencies is considered a hack and indicates a flaw in the asynchronous architecture. Proper use of `async`/`await`, Promises, and the loader's dependency management should make this unnecessary.
|
|
||||||
* **`setTimeout` for Delays:** Acceptable *only* within well-encapsulated components for specific, justifiable reasons (like debouncing, throttling, or potentially *very* short delays *if absolutely unavoidable* after direct DOM manipulation, though this should also be minimized). It must **not** be used to paper over asynchronous race conditions or timing problems. The `AnimationQueue` is an acceptable place for internal scheduling timeouts, but application code calling it should rely on its event-driven nature.
|
|
||||||
* **Fallbacks for Missing Dependencies:** **Strictly prohibited.** Code within a module should not check if a dependency exists and provide a fallback path. The module loader's responsibility is to guarantee dependencies are met before initializing the module. Errors should be handled for *actual* failures during initialization, not for unmet dependencies (which indicates a loader bug).
|
|
||||||
|
|
||||||
Adhering to these principles will lead to a cleaner, more robust, and maintainable asynchronous module loading system.
|
|
||||||
|
|
||||||
# Module Loader System Architecture
|
|
||||||
|
|
||||||
The module loader system is designed to manage the loading and initialization of modular components in a structured, dependency-aware manner with visual progress reporting.
|
|
||||||
|
|
||||||
## Overall Architecture
|
|
||||||
|
|
||||||
1. **Module Registry Pattern**: Uses a centralized registry to track and manage all modules and their states.
|
|
||||||
|
|
||||||
2. **Event-Driven Communication**: Modules communicate with the loader and each other through custom events.
|
|
||||||
|
|
||||||
3. **Progress Visualization**: Provides a visual loading overlay with per-module progress tracking.
|
|
||||||
|
|
||||||
4. **State Management**: Tracks each module through defined states (PENDING, LOADING, WAITING, INITIALIZING, FINISHED, ERROR).
|
|
||||||
|
|
||||||
5. **Dependency Resolution**: Handles module dependencies to ensure proper initialization order.
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
1. **ModuleRegistry**: Central repository for all modules
|
|
||||||
- Tracks registration and availability of modules
|
|
||||||
- Manages promises for module readiness
|
|
||||||
- Provides dependency resolution through `waitForModule` and `waitForModules`
|
|
||||||
|
|
||||||
2. **BaseModule**: Abstract base class that all modules extend
|
|
||||||
- Implements standard lifecycle methods
|
|
||||||
- Handles progress reporting and state changes
|
|
||||||
- Provides consistent interface for the loader
|
|
||||||
|
|
||||||
3. **ModuleLoader**: Main orchestrator of the loading process
|
|
||||||
- Dynamically loads module scripts
|
|
||||||
- Creates and manages the visual loading interface
|
|
||||||
- Initializes modules in the correct order
|
|
||||||
- Tracks and displays overall loading progress
|
|
||||||
|
|
||||||
4. **ModuleEvent**: Custom event system for inter-module communication
|
|
||||||
|
|
||||||
## Loading Sequence
|
|
||||||
|
|
||||||
1. HTML page loads and includes the loader script as a module
|
|
||||||
2. DOMContentLoaded triggers the loader initialization
|
|
||||||
3. Loader creates the loading UI and registers event listeners
|
|
||||||
4. Module scripts are loaded dynamically in parallel
|
|
||||||
5. Each module registers itself with the registry
|
|
||||||
6. Modules are initialized with dependency checking
|
|
||||||
7. Progress is reported and visualized throughout
|
|
||||||
8. When all modules reach FINISHED state, loading overlay is hidden
|
|
||||||
|
|
||||||
## Module Lifecycle
|
|
||||||
|
|
||||||
1. **PENDING**: Initial state before loading begins
|
|
||||||
2. **LOADING**: Module is loading dependencies
|
|
||||||
3. **WAITING**: Module is waiting for dependencies to be ready
|
|
||||||
4. **INITIALIZING**: Module's initialize() method is executing
|
|
||||||
5. **FINISHED**: Module is fully initialized and ready
|
|
||||||
6. **ERROR**: Module encountered an error during initialization
|
|
||||||
|
|
||||||
## Integration Pattern
|
|
||||||
|
|
||||||
Modules follow a consistent registration pattern:
|
|
||||||
```javascript
|
|
||||||
// Create the singleton instance
|
|
||||||
const ModuleName = new ModuleNameClass();
|
|
||||||
|
|
||||||
// Register with the module registry
|
|
||||||
moduleRegistry.register(ModuleName);
|
|
||||||
|
|
||||||
// Export the module
|
|
||||||
export { ModuleName };
|
|
||||||
|
|
||||||
// Keep a reference in window for loader system
|
|
||||||
window.ModuleName = ModuleName;
|
|
||||||
```
|
|
||||||
|
|
||||||
This design creates a flexible, maintainable system for loading complex applications with multiple interdependent components, prioritizing both user experience and performance.
|
|
||||||
|
|
||||||
|
|
||||||
# TTS System Structure & Kokoro Loading
|
|
||||||
|
|
||||||
After reviewing our chat history, here's a summary of the TTS system structure and how we decided to load the Kokoro TTS engine:
|
|
||||||
|
|
||||||
## Overall TTS System Architecture
|
|
||||||
|
|
||||||
1. **Modular Design**: The TTS system uses a modular architecture with multiple handler classes, each implementing a different TTS approach.
|
|
||||||
|
|
||||||
2. **Three TTS Providers**:
|
|
||||||
- `BrowserTTSHandler` - Uses the built-in Web Speech API
|
|
||||||
- `KokoroHandler` - Uses Kokoro.js neural TTS for high-quality voices
|
|
||||||
- `ApiTTSHandler` - Uses external TTS services like ElevenLabs
|
|
||||||
|
|
||||||
3. **Factory Pattern**: `TTSFactory` manages the handlers, provides a unified interface, and handles provider switching.
|
|
||||||
|
|
||||||
4. **Module System**: `TTSPlayer` module is registered with the `moduleRegistry` as part of the modular loading system.
|
|
||||||
|
|
||||||
## Loading Sequence
|
|
||||||
|
|
||||||
1. The module loader first loads `tts-player.js`, which in turn loads the `tts-factory.js`.
|
|
||||||
|
|
||||||
2. The factory initializes providers in order of preference:
|
|
||||||
- First loads the `BrowserTTSHandler` for immediate low-quality TTS
|
|
||||||
- Then loads the `ApiTTSHandler` if configured
|
|
||||||
- Finally attempts to load `KokoroHandler` in the background with low priority
|
|
||||||
|
|
||||||
3. The system uses the best available provider, with a preference for Kokoro when available.
|
|
||||||
|
|
||||||
## Kokoro TTS Loading Strategy
|
|
||||||
|
|
||||||
After consulting the documentation (https://www.npmjs.com/package/kokoro-js), we made these decisions:
|
|
||||||
|
|
||||||
1. **Low-Priority Loading**: Kokoro is loaded with `requestIdleCallback` to avoid impacting page performance.
|
|
||||||
|
|
||||||
2. **Kokoro npm package integration**: Load Kokoro directly from the local server:
|
|
||||||
'/js/kokoro-js.js' contains the minified complete code of the kokoro npm package copied from the node_modules folder to the public directory. Do not try to read or change it, it is too big!
|
|
||||||
|
|
||||||
3. **Pipeline Creation**: Per documentation, we use the pipeline pattern:
|
|
||||||
```javascript
|
|
||||||
this.kokoro = await window.kokoroTTS.pipeline('text-to-speech', {
|
|
||||||
quantized: true,
|
|
||||||
progress_callback: this.progressCallback
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Voice List**: We hardcoded the available voices rather than querying them dynamically.
|
|
||||||
|
|
||||||
5. **Audio Playback**: Synthesis returns an audio element which we play:
|
|
||||||
```javascript
|
|
||||||
const audio = await this.kokoro(processedText, {
|
|
||||||
voice: this.voiceOptions.voice,
|
|
||||||
speed: this.voiceOptions.speed
|
|
||||||
});
|
|
||||||
audio.play();
|
|
||||||
```
|
|
||||||
|
|
||||||
## User Experience Flow
|
|
||||||
|
|
||||||
1. User sees page immediately with browser TTS enabled (fast startup)
|
|
||||||
2. Kokoro loads in background without blocking the interface
|
|
||||||
3. Once Kokoro is ready, TTS switches to higher quality neural TTS
|
|
||||||
4. User can manually switch between providers via the UI if desired
|
|
||||||
|
|
||||||
This design prioritizes performance and user experience, making the TTS system both flexible and resource-efficient.
|
|
||||||
|
|
||||||
# Important practices
|
|
||||||
|
|
||||||
- Always import the following error, when debugging console output: onpage-dialog.preload.js:121 Uncaught ReferenceError: browser is not defined. This is producced by the installed adblocker and has nothing to do with our project.
|
|
||||||
|
|
||||||
# Text-to-Speech Synchronization Architecture
|
|
||||||
|
|
||||||
The TTS system needs to be synchronized with text animations to create a cohesive user experience. This section outlines the requirements and implementation approach.
|
|
||||||
|
|
||||||
## Transition to Game
|
|
||||||
|
|
||||||
The overlay fades away as the first scheduled animation.
|
|
||||||
- This fade animation is handled by the animation scheduler module
|
|
||||||
- Only after successful fade-out does the game loop start
|
|
||||||
- Socket connection is established and begins receiving text
|
|
||||||
|
|
||||||
## Text Buffering & Sentence Processing
|
|
||||||
|
|
||||||
1. **Text Buffer Collection**: Incoming text from sockets is collected in a buffer.
|
|
||||||
- System can receive fragments of any size (single letters, words, sentences, paragraphs)
|
|
||||||
- All text is accumulated in the buffer regardless of fragment size
|
|
||||||
- Buffer handles partial/incomplete text gracefully
|
|
||||||
|
|
||||||
2. **Sentence Detection**: The buffer identifies complete sentences.
|
|
||||||
- When full sentences are detected, they are extracted from the buffer
|
|
||||||
- If multiple sentences arrive simultaneously, they are split and processed individually
|
|
||||||
- Remaining partial sentences stay in the buffer until completion
|
|
||||||
|
|
||||||
## Synchronized Playback
|
|
||||||
|
|
||||||
1. **TTS Generation Queue**: Complete sentences enter the TTS generation queue.
|
|
||||||
- Generation begins immediately if no other sentence is being processed
|
|
||||||
- Results are cached for immediate playback when needed
|
|
||||||
|
|
||||||
2. **Animation Timing**: Animation speed is synchronized with audio duration.
|
|
||||||
- The system calculates animation duration to match TTS audio length exactly
|
|
||||||
- Both animation and audio start simultaneously
|
|
||||||
- Animation completes at the same time as audio playback
|
|
||||||
|
|
||||||
3. **Playback Pipeline**: Continuous processing of sentences.
|
|
||||||
- As soon as one sentence completes playback, the next begins
|
|
||||||
- Next sentence generation starts during current sentence playback
|
|
||||||
- This creates a seamless reading experience
|
|
||||||
|
|
||||||
## Fast-Forward & Control Flow
|
|
||||||
|
|
||||||
1. **Fast-Forward Behavior**: User can skip current sentence.
|
|
||||||
- Pressing the designated fast-forward key completes current animation immediately
|
|
||||||
- TTS audio is faded out and stopped
|
|
||||||
- System advances to next sentence
|
|
||||||
|
|
||||||
2. **Resource Management**: TTS generation is resource-conscious.
|
|
||||||
- Uses only CPU/GPU resources not needed for animation
|
|
||||||
- Generation process can be cancelled by fast-forward
|
|
||||||
- System prioritizes smooth animation over TTS preparation
|
|
||||||
|
|
||||||
3. **Loading States**: Animation waits for TTS when necessary.
|
|
||||||
- If next sentence TTS generation isn't ready when needed, animation pauses
|
|
||||||
- Fast-forward key can skip incomplete generation
|
|
||||||
- User is never blocked completely by TTS generation
|
|
||||||
|
|
||||||
## Persistent Configuration
|
|
||||||
|
|
||||||
1. **Options Storage**: The persistence-manager stores TTS settings.
|
|
||||||
- Speech on/off state is remembered
|
|
||||||
- Speed settings are preserved between sessions
|
|
||||||
- Voice preferences are stored
|
|
||||||
|
|
||||||
2. **Options UI**: Add an options button and modal dialog.
|
|
||||||
- Show additional options in a modal window
|
|
||||||
- Include volume sliders:
|
|
||||||
- Master volume control
|
|
||||||
- TTS volume control
|
|
||||||
- Music volume control
|
|
||||||
- Sound effects volume control
|
|
||||||
- Include manual TTS system selection
|
|
||||||
- All settings are persisted via the persistence-manager
|
|
||||||
|
|
||||||
This synchronized approach ensures that text animations and speech work together seamlessly, creating a more immersive storytelling experience while maintaining smooth performance.
|
|
||||||
|
|
||||||
# Text Output Pipeline Architecture
|
|
||||||
|
|
||||||
The text output pipeline manages the flow of text from server reception to visual display and audio playback, with a focus on performance and synchronization.
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
1. **Socket Client**: Receives raw text fragments from the server.
|
|
||||||
|
|
||||||
2. **TextBuffer**: Accumulates fragments and identifies complete sentences.
|
|
||||||
- Collects all incoming text regardless of fragment size
|
|
||||||
- Identifies and extracts complete sentences
|
|
||||||
- Maintains partial sentences until completion
|
|
||||||
|
|
||||||
3. **SentenceQueue**: Manages the preparation pipeline for sentences.
|
|
||||||
- Receives complete sentences from TextBuffer
|
|
||||||
- Orchestrates parallel processing of TTS generation and text layout
|
|
||||||
- Ensures sentences are fully prepared before playback
|
|
||||||
- Maintains a queue of sentences ready for playback
|
|
||||||
|
|
||||||
4. **TTS Generation System**: Prepares audio for sentences.
|
|
||||||
- Generates audio in the background without blocking UI
|
|
||||||
- Provides audio duration information for synchronization
|
|
||||||
- Can be cancelled for fast-forward operations
|
|
||||||
- Falls back to character count duration calculation when disabled
|
|
||||||
|
|
||||||
5. **Typography Processor**: Enhances text presentation quality.
|
|
||||||
- Applies smart typography (quotes, em-dashes, etc.)
|
|
||||||
- Handles hyphenation for line breaks
|
|
||||||
- Preserves special formatting
|
|
||||||
|
|
||||||
6. **ParagraphLayout**: Calculates optimal text presentation.
|
|
||||||
- Computes line breaks using Knuth-Plass algorithm
|
|
||||||
- Determines word positioning and timing
|
|
||||||
- Adjusts animation duration to match audio length
|
|
||||||
|
|
||||||
7. **AnimationPlayerQueue**: Manages the playback pipeline.
|
|
||||||
- Maintains a playlist of ready-to-play sentences
|
|
||||||
- Inserts DOM elements for prepared sentences
|
|
||||||
- Coordinates CSS-based animations
|
|
||||||
- Monitors animation completion
|
|
||||||
- Automatically advances to next sentence
|
|
||||||
|
|
||||||
## Process Flow
|
|
||||||
|
|
||||||
1. **Preparation Pipeline**:
|
|
||||||
- Socket client receives text and feeds it to TextBuffer
|
|
||||||
- TextBuffer identifies complete sentences
|
|
||||||
- SentenceQueue receives complete sentences
|
|
||||||
- TTS generation and layout processing happen in parallel
|
|
||||||
- When both TTS and layout are complete, sentence is marked "ready"
|
|
||||||
- Ready sentences are added to AnimationPlayerQueue
|
|
||||||
|
|
||||||
2. **Playback Pipeline**:
|
|
||||||
- AnimationPlayerQueue plays the first ready sentence
|
|
||||||
- DOM elements are inserted and CSS animations begin
|
|
||||||
- TTS audio plays simultaneously with animations
|
|
||||||
- AnimationPlayerQueue monitors complete animation duration
|
|
||||||
- When playback completes, the next ready sentence immediately begins
|
|
||||||
|
|
||||||
3. **Fast-Forward Handling**:
|
|
||||||
- Can interrupt at any stage of the pipeline
|
|
||||||
- Currently playing animations are immediately completed
|
|
||||||
- Currently playing audio is faded out and stopped
|
|
||||||
- Any in-progress sentence preparation is cancelled
|
|
||||||
- System advances to the next sentence in queue
|
|
||||||
|
|
||||||
## Speed Synchronization
|
|
||||||
|
|
||||||
1. **Audio-Driven Timing**: Animation speed is determined by audio duration
|
|
||||||
- TTS audio length dictates animation duration
|
|
||||||
- Without TTS, duration is calculated from character count and speed setting
|
|
||||||
|
|
||||||
2. **Seamless Transitions**: Next sentence begins immediately after current completes
|
|
||||||
- No gap between sentence playbacks
|
|
||||||
- Preparation happens during playback of previous sentence
|
|
||||||
|
|
||||||
3. **Feedback Loop**: Animation system provides timing data back to preparation pipeline
|
|
||||||
- Helps optimize future sentence preparation
|
|
||||||
- Allows runtime adjustment of timing parameters
|
|
||||||
|
|
||||||
This architecture separates preparation from playback, creating a buffer of ready content that enables smooth presentation while handling the computational overhead of text processing and TTS generation in the background.
|
|
||||||
|
|
||||||
# Text Processing & Layout Architecture
|
|
||||||
|
|
||||||
The text processing and layout system transforms raw text input into visually appealing, typographically correct, and elegantly animated content through several specialized components.
|
|
||||||
|
|
||||||
## Component Interactions
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
1. **text-processor.js**: Enhances typography and applies hyphenation
|
|
||||||
- Entry point for text processing pipeline
|
|
||||||
- Manages SmartyPants for typographic enhancements
|
|
||||||
- Controls Hyphenopoly for language-aware hyphenation
|
|
||||||
- Serves as the central coordinator for text transformation
|
|
||||||
|
|
||||||
2. **smartypants.js**: Provides typographic punctuation conversion
|
|
||||||
- Transforms straight quotes to curly quotes
|
|
||||||
- Converts hyphens to em-dashes and en-dashes
|
|
||||||
- Handles ellipses and other typographic niceties
|
|
||||||
- Operates as a pure function with no dependencies
|
|
||||||
|
|
||||||
3. **paragraph-layout.js**: Manages paragraph structure and word metrics
|
|
||||||
- Breaks text into words and calculates their dimensions
|
|
||||||
- Manages paragraph-level styling and layout properties
|
|
||||||
- Prepares text for the line-breaking algorithm
|
|
||||||
- Connects text-processor output to the layout engine
|
|
||||||
|
|
||||||
4. **knuth-plass.js**: Implementation of the optimal line-breaking algorithm
|
|
||||||
- Calculates aesthetically pleasing line breaks
|
|
||||||
- Minimizes "raggedness" across paragraph lines
|
|
||||||
- Implements the core Knuth-Plass algorithm
|
|
||||||
- Uses linked-list.js for internal data structures
|
|
||||||
|
|
||||||
5. **linked-list.js**: Provides data structures for the line-breaking algorithm
|
|
||||||
- Implements doubly-linked list for efficient node insertion/removal
|
|
||||||
- Supports the complex data relationships in the Knuth-Plass algorithm
|
|
||||||
- Pure utility with no direct interaction with other components
|
|
||||||
|
|
||||||
6. **hyphenopoly.module.js**: Performs language-aware hyphenation
|
|
||||||
- Contains language-specific hyphenation patterns
|
|
||||||
- Provides functions to insert soft hyphens at valid breaking points
|
|
||||||
- Loaded dynamically when needed by text-processor.js
|
|
||||||
|
|
||||||
7. **layout-renderer.js**: Translates calculated layout into DOM elements
|
|
||||||
- Takes the output from paragraph-layout.js
|
|
||||||
- Generates DOM structure for the text display
|
|
||||||
- Creates CSS classes and styles for animations
|
|
||||||
- Prepares text for display and animation
|
|
||||||
|
|
||||||
## Process Flow
|
|
||||||
|
|
||||||
1. **Text Input → Typography Enhancement**
|
|
||||||
```
|
|
||||||
Raw Text → text-processor.js → smartypants.js → Enhanced Text
|
|
||||||
```
|
|
||||||
- Raw text enters the text-processor
|
|
||||||
- SmartyPants functions transform quotation marks, dashes, etc.
|
|
||||||
- Typography-enhanced text is produced
|
|
||||||
|
|
||||||
2. **Typography-Enhanced Text → Hyphenation**
|
|
||||||
```
|
|
||||||
Enhanced Text → text-processor.js → hyphenopoly.module.js → Hyphenated Text
|
|
||||||
```
|
|
||||||
- Enhanced text is passed to the hyphenation system
|
|
||||||
- Language-specific rules determine valid hyphenation points
|
|
||||||
- Soft hyphens are inserted at appropriate positions
|
|
||||||
|
|
||||||
3. **Hyphenated Text → Layout Calculation**
|
|
||||||
```
|
|
||||||
Hyphenated Text → paragraph-layout.js → knuth-plass.js → Optimized Layout
|
|
||||||
```
|
|
||||||
- Paragraph layout breaks text into words and calculates metrics
|
|
||||||
- Knuth-Plass algorithm calculates optimal line breaks
|
|
||||||
- linked-list.js provides the data structures for this process
|
|
||||||
- An optimized layout structure is produced
|
|
||||||
|
|
||||||
4. **Layout → Rendering**
|
|
||||||
```
|
|
||||||
Optimized Layout → layout-renderer.js → DOM Elements
|
|
||||||
```
|
|
||||||
- Layout renderer converts the abstract layout to concrete DOM
|
|
||||||
- CSS classes and styles are applied for animation
|
|
||||||
- Words are positioned according to the calculated layout
|
|
||||||
|
|
||||||
5. **Rendering → Animation**
|
|
||||||
```
|
|
||||||
DOM Elements → AnimationQueue → Visual Display
|
|
||||||
```
|
|
||||||
- The rendered DOM elements are passed to the animation system
|
|
||||||
- Words are animated according to timing and styling parameters
|
|
||||||
- Visual presentation occurs synchronized with audio if applicable
|
|
||||||
|
|
||||||
## Implementation Dependencies
|
|
||||||
|
|
||||||
```
|
|
||||||
text-processor.js
|
|
||||||
├── smartypants.js
|
|
||||||
└── hyphenopoly.module.js
|
|
||||||
└── [language pattern files]
|
|
||||||
|
|
||||||
paragraph-layout.js
|
|
||||||
├── knuth-plass.js
|
|
||||||
│ └── linked-list.js
|
|
||||||
└── [font metrics]
|
|
||||||
|
|
||||||
layout-renderer.js
|
|
||||||
└── [CSS styling]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Points
|
|
||||||
|
|
||||||
1. **Text Buffer → Text Processor**
|
|
||||||
- Text buffer passes complete sentences to the processing pipeline
|
|
||||||
- Text processor enhances typography and applies hyphenation
|
|
||||||
|
|
||||||
2. **Text Processor → Paragraph Layout**
|
|
||||||
- Enhanced text flows to paragraph layout for structure analysis
|
|
||||||
- Word metrics and paragraph properties are calculated
|
|
||||||
|
|
||||||
3. **Paragraph Layout → Layout Renderer**
|
|
||||||
- Optimized layout information is passed to the renderer
|
|
||||||
- Renderer creates DOM elements with appropriate styling
|
|
||||||
|
|
||||||
4. **Layout Renderer → Animation Queue**
|
|
||||||
- Rendered elements are scheduled for animation
|
|
||||||
- Animation timing is synchronized with TTS if enabled
|
|
||||||
|
|
||||||
This architecture ensures typographically beautiful text with optimal line breaks, proper hyphenation, and smooth animation, creating a professional reading experience.
|
|
||||||
|
|
||||||
# TTS Integration with Localization
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
The Text-to-Speech (TTS) system has been refactored to seamlessly integrate with the localization module, ensuring a cohesive user experience across different languages. This integration follows these key architectural principles:
|
|
||||||
|
|
||||||
1. **Base TTS Handler Pattern**: All TTS handlers extend a common `TTSHandler` class that inherits from `BaseModule`, ensuring consistent interface and behavior.
|
|
||||||
|
|
||||||
2. **Dependency Injection**: TTS handlers access the localization and persistence modules through the dependency system rather than direct global references.
|
|
||||||
|
|
||||||
3. **Locale-Aware Voice Selection**: TTS handlers automatically select appropriate voices based on the current locale.
|
|
||||||
|
|
||||||
4. **Preference Persistence**: User preferences for TTS settings are stored and retrieved through the persistence manager.
|
|
||||||
|
|
||||||
5. **Optional Functionality**: TTS is treated as an optional feature that can be unavailable without breaking the application.
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
1. **TTSFactory**: Central coordinator for TTS functionality
|
|
||||||
- Manages initialization of all TTS handlers
|
|
||||||
- Implements fallback mechanisms when preferred TTS systems are unavailable
|
|
||||||
- Provides access to the active TTS handler
|
|
||||||
- Integrates with localization module for language-aware voice selection
|
|
||||||
- Reports TTS availability to the UI
|
|
||||||
|
|
||||||
2. **TTSHandler**: Abstract base class for all TTS handlers
|
|
||||||
- Defines common interface methods (speak, stop, getVoices, etc.)
|
|
||||||
- Provides shared utility functions for voice selection and preference handling
|
|
||||||
- Extends BaseModule for dependency management and event handling
|
|
||||||
|
|
||||||
3. **TTS Handlers**: Concrete implementations for different TTS approaches
|
|
||||||
- **BrowserTTSHandler**: Uses the Web Speech API
|
|
||||||
- **ApiTTSHandler**: Communicates with a remote TTS API
|
|
||||||
- **KokoroHandler**: Provides neural TTS via Kokoro.js
|
|
||||||
|
|
||||||
4. **OptionsUI**: User interface for TTS configuration
|
|
||||||
- Allows selection of TTS system (Browser, API, Kokoro)
|
|
||||||
- Provides voice selection based on available voices for current locale
|
|
||||||
- Includes controls for volume, rate, and pitch
|
|
||||||
- Persists user preferences via PersistenceManager
|
|
||||||
|
|
||||||
## Localization Integration
|
|
||||||
|
|
||||||
1. **Locale-Based Voice Selection**:
|
|
||||||
- Each TTS handler implements `setupVoiceFromPreferences()` to select voices based on:
|
|
||||||
- User's explicitly saved voice preference
|
|
||||||
- Current locale from the localization module
|
|
||||||
- Fallback to language-matching voice if exact locale match not found
|
|
||||||
- Default voice (typically English) as final fallback
|
|
||||||
|
|
||||||
2. **Voice Filtering**:
|
|
||||||
- TTS handlers filter available voices to prioritize those matching the current locale
|
|
||||||
- Voice lists in the UI are sorted to show locale-matching voices first
|
|
||||||
|
|
||||||
3. **Preference Persistence**:
|
|
||||||
- TTS settings (system, voice, volume, rate) are saved per-user
|
|
||||||
- Settings are automatically applied when the application loads
|
|
||||||
- Changes in the localization settings trigger voice re-selection
|
|
||||||
|
|
||||||
## Initialization Flow
|
|
||||||
|
|
||||||
1. **TTSFactory Initialization**:
|
|
||||||
```
|
|
||||||
TTSFactory.initialize()
|
|
||||||
├── Loads user preferences via PersistenceManager
|
|
||||||
├── Initializes all available TTS handlers
|
|
||||||
│ ├── KokoroHandler.initialize()
|
|
||||||
│ └── BrowserTTSHandler.initialize()
|
|
||||||
├── Selects active TTS handler based on preferences and availability
|
|
||||||
├── Sets up event listeners for locale changes
|
|
||||||
└── Dispatches TTS availability event
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **TTS Handler Initialization**:
|
|
||||||
```
|
|
||||||
TTSHandler.initialize()
|
|
||||||
├── Loads system-specific resources
|
|
||||||
├── Retrieves available voices
|
|
||||||
├── Gets dependencies (localization, persistenceManager)
|
|
||||||
└── Sets up voice based on preferences and locale
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Voice Setup Process**:
|
|
||||||
```
|
|
||||||
setupVoiceFromPreferences()
|
|
||||||
├── Gets user's preferred voice from persistenceManager
|
|
||||||
├── If preferred voice exists and is available:
|
|
||||||
│ └── Use preferred voice
|
|
||||||
├── Otherwise:
|
|
||||||
│ ├── Get current locale from localization module
|
|
||||||
│ ├── Find voice matching current locale
|
|
||||||
│ ├── If no match, find voice matching language part
|
|
||||||
│ └── If still no match, use default voice
|
|
||||||
└── Update preference with selected voice
|
|
||||||
```
|
|
||||||
|
|
||||||
## Event Handling
|
|
||||||
|
|
||||||
1. **Locale Change Events**:
|
|
||||||
- When user changes locale in the UI, the localization module emits a 'locale-changed' event
|
|
||||||
- TTSFactory listens for this event and triggers voice re-selection in the active TTS handler
|
|
||||||
|
|
||||||
2. **TTS Preference Events**:
|
|
||||||
- Changes to TTS settings in the options UI trigger preference updates
|
|
||||||
- These updates are persisted and immediately applied to the active TTS handler
|
|
||||||
|
|
||||||
3. **TTS Availability Events**:
|
|
||||||
- TTSFactory dispatches 'tts:availability' events to notify the UI about TTS availability
|
|
||||||
- UI Controller listens for these events and updates the speech toggle button accordingly
|
|
||||||
|
|
||||||
## Error Handling and Fallbacks
|
|
||||||
|
|
||||||
1. **TTS System Fallbacks**:
|
|
||||||
- If the preferred TTS system fails to initialize, TTSFactory falls back to the next available system
|
|
||||||
- Priority order: Kokoro > Browser > None (with None being acceptable)
|
|
||||||
- API TTS is not used as a fallback as it requires manual configuration
|
|
||||||
|
|
||||||
2. **Voice Selection Fallbacks**:
|
|
||||||
- If preferred voice is unavailable, fall back to locale-matching voice
|
|
||||||
- If no locale match, fall back to language match
|
|
||||||
- If no language match, fall back to default (typically English)
|
|
||||||
|
|
||||||
3. **TTS Unavailability Handling**:
|
|
||||||
- If no TTS handlers are available, the system continues to function without TTS
|
|
||||||
- The speech toggle button is disabled in the UI
|
|
||||||
- The application remains fully functional for text-only interaction
|
|
||||||
|
|
||||||
This architecture ensures that the TTS system seamlessly adapts to the user's language preferences while maintaining a consistent and intuitive user experience across different locales, even when TTS is unavailable.
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
# Story Markup
|
|
||||||
|
|
||||||
The story stream supports plain paragraphs plus a small set of structural and media cues.
|
|
||||||
|
|
||||||
## Inline Text
|
|
||||||
|
|
||||||
Markdown emphasis is supported inside paragraphs and headings:
|
|
||||||
|
|
||||||
- `*italic*` or `_italic_`
|
|
||||||
- `**bold**` or `__bold__`
|
|
||||||
- `***bold italic***` or `___bold italic___`
|
|
||||||
|
|
||||||
SmartyPants processing still runs after markup parsing, so straight quotes, apostrophes, and dashes are converted typographically before layout and hyphenation.
|
|
||||||
|
|
||||||
## Paragraphs And Sections
|
|
||||||
|
|
||||||
Plain narrative is rendered paragraph by paragraph. Each ordinary paragraph uses the normal first-line indent, and adjacent paragraphs are not separated by a blank line.
|
|
||||||
|
|
||||||
Use an explicit section marker when the next paragraph should begin a new text block:
|
|
||||||
|
|
||||||
```text
|
|
||||||
::section
|
|
||||||
|
|
||||||
The first paragraph of the section begins here.
|
|
||||||
|
|
||||||
The second paragraph begins here.
|
|
||||||
```
|
|
||||||
|
|
||||||
- The first paragraph after `::section` is not horizontally indented.
|
|
||||||
- It gets one line of vertical separation from previous content.
|
|
||||||
- Following paragraphs use the normal first-line indent.
|
|
||||||
|
|
||||||
Paragraphs are separated by one or more blank lines. Single newlines inside a paragraph are normalized to spaces.
|
|
||||||
|
|
||||||
## Chapters
|
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
```text
|
|
||||||
::chapter[The House on the Hill]
|
|
||||||
|
|
||||||
The first paragraph begins here.
|
|
||||||
|
|
||||||
The second paragraph begins here.
|
|
||||||
```
|
|
||||||
|
|
||||||
The chapter heading is rendered in the same font and size as block text, centered and italic. The first following paragraph is unindented and marked for a three-line drop cap. Following paragraphs are indented normally.
|
|
||||||
|
|
||||||
## Images
|
|
||||||
|
|
||||||
Image blocks are parsed, queued, and emitted as future media blocks, but image rendering is not implemented yet.
|
|
||||||
|
|
||||||
```text
|
|
||||||
::image[widescreen](mansion-rain.jpg)
|
|
||||||
::image[portrait](portrait-letter.jpg)
|
|
||||||
```
|
|
||||||
|
|
||||||
`widescreen` means 100% page width and 50% page height. `portrait` means 100% page width and 100% page height. Filenames are relative to the future image directory.
|
|
||||||
|
|
||||||
## Sound Effects
|
|
||||||
|
|
||||||
Sound effects can appear inside regular paragraphs:
|
|
||||||
|
|
||||||
```text
|
|
||||||
The door opens {{sfx:door-creak.ogg}} and the hall exhales.
|
|
||||||
```
|
|
||||||
|
|
||||||
The marker is removed from displayed text and TTS text. It is preserved as a timed cue and emitted when the word animation reaches the marker position. Actual preload and playback are not implemented yet.
|
|
||||||
|
|
||||||
The animation engine emits `story:media-cue` with `type: "sfx"`, `filename`, `wordIndex`, and `delay`.
|
|
||||||
|
|
||||||
## Music
|
|
||||||
|
|
||||||
Music can be queued as a block:
|
|
||||||
|
|
||||||
```text
|
|
||||||
::music[crossfade, loop, lead=4](rain-theme.ogg)
|
|
||||||
```
|
|
||||||
|
|
||||||
It can also be placed inline:
|
|
||||||
|
|
||||||
```text
|
|
||||||
The candles gutter. {{music:cut:danger.ogg}} Something moves upstairs.
|
|
||||||
```
|
|
||||||
|
|
||||||
Supported modes are:
|
|
||||||
|
|
||||||
- `queue`: play after the current track finishes.
|
|
||||||
- `crossfade`: crossfade from the current track to the new one.
|
|
||||||
- `cut`: stop the current track and start the new one immediately.
|
|
||||||
- `loop` or `once`: whether the track repeats.
|
|
||||||
- `lead=<seconds>`: how long the music plays alone before the next text/TTS paragraph starts.
|
|
||||||
|
|
||||||
Inline music markers emit `story:media-cue`. Block music directives emit `story:media-block`; block directives also delay the following paragraph when `lead` is set.
|
|
||||||
File diff suppressed because it is too large
Load Diff
-1034
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
|
||||||
<!-- meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'blob'; style-src 'self' 'unsafe-inline'" -->
|
|
||||||
<title>ai-fiction Book Runtime (Modular Version)</title>
|
|
||||||
<link rel="stylesheet" href="css/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p id="versions">We are using Node.js <span id="node-version"></span>,
|
|
||||||
Chromium <span id="chrome-version"></span>,
|
|
||||||
and Electron <span id="electron-version"></span>.</p>
|
|
||||||
<div id="book">
|
|
||||||
<div id="page_left">
|
|
||||||
<div class="header">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div id="controls" class="buttons">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div id="choices" class="container">
|
|
||||||
<div id="command_history">
|
|
||||||
<!-- Previous commands and responses will be displayed here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="command_input">
|
|
||||||
<div class="input-wrapper">
|
|
||||||
<textarea id="player_input" placeholder="Enter your command..." rows="1" autofocus></textarea>
|
|
||||||
<span id="cursor"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="l10n-remark" id="remark"><i><sup>*</sup>click on page or press spacebar to fast forward text animation</i></div>
|
|
||||||
</div>
|
|
||||||
<div id="page_right">
|
|
||||||
<div id="story" class="container">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ruler"></div>
|
|
||||||
<div class="l10n-prompt" id="indent">What do you want to do next?</div>
|
|
||||||
<div id="lighting" />
|
|
||||||
|
|
||||||
<!-- Socket.io library for client-server communication -->
|
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
|
||||||
|
|
||||||
<!-- Core libraries -->
|
|
||||||
<script src="js/smartypants.js"></script>
|
|
||||||
<script src="js/linked-list.js"></script>
|
|
||||||
<script src="js/linebreak.js"></script>
|
|
||||||
<script src="js/knuth-and-plass.js"></script>
|
|
||||||
<script src="js/Hyphenopoly_Loader.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var locale = "en";
|
|
||||||
// Create global variables needed by the modules
|
|
||||||
window.rstack = [];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Kokoro TTS library - load as module -->
|
|
||||||
<script type="module">
|
|
||||||
try {
|
|
||||||
// Import KokoroTTS class from the module
|
|
||||||
const kokoroModule = await import('./js/kokoro-js.js');
|
|
||||||
|
|
||||||
// Expose the KokoroTTS class globally
|
|
||||||
window.KokoroTTS = kokoroModule.KokoroTTS;
|
|
||||||
console.log('KokoroTTS class loaded and exposed to window');
|
|
||||||
|
|
||||||
// Dispatch an event to signal that the class is ready
|
|
||||||
const event = new CustomEvent('kokoro-class-loaded');
|
|
||||||
window.dispatchEvent(event);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load KokoroTTS module:', error);
|
|
||||||
// Dispatch an event even on failure so handlers don't wait forever
|
|
||||||
const event = new CustomEvent('kokoro-class-load-failed');
|
|
||||||
window.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Main application script - imports all needed modules -->
|
|
||||||
<script type="module" src="js/ai-fiction.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
/**
|
|
||||||
* Input Handler Module
|
|
||||||
* Manages the multi-line text input field with a custom cursor.
|
|
||||||
*/
|
|
||||||
export class InputHandler {
|
|
||||||
constructor(inputId = 'player_input', cursorId = 'cursor') {
|
|
||||||
this.playerInput = document.getElementById(inputId);
|
|
||||||
this.cursor = document.getElementById(cursorId);
|
|
||||||
this.commandInputContainer = document.getElementById('command_input'); // Assuming this container exists
|
|
||||||
|
|
||||||
if (!this.playerInput || !this.cursor || !this.commandInputContainer) {
|
|
||||||
console.error('InputHandler: Required DOM elements not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.commandSubmitCallback = null; // Callback for when a command is submitted
|
|
||||||
|
|
||||||
this.bindEvents();
|
|
||||||
this.adjustTextareaHeight(); // Initial adjustment
|
|
||||||
this.updateCursorPosition(); // Initial position
|
|
||||||
|
|
||||||
// Setup handler for window load event to ensure proper initialization
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
console.log('InputHandler: Window loaded, adjusting text area height and cursor position');
|
|
||||||
this.adjustTextareaHeight();
|
|
||||||
this.updateCursorPosition();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a callback function to be called when a command is submitted.
|
|
||||||
* @param {function(string)} callback - The function to call with the command text.
|
|
||||||
*/
|
|
||||||
onCommandSubmit(callback) {
|
|
||||||
this.commandSubmitCallback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind event handlers to the input element.
|
|
||||||
*/
|
|
||||||
bindEvents() {
|
|
||||||
// Submit command on Enter key without Shift
|
|
||||||
this.playerInput.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault(); // Prevent default to avoid newline
|
|
||||||
this.submitCommand();
|
|
||||||
}
|
|
||||||
// Allow Shift+Enter for new lines (default behavior)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-resize textarea and update cursor on input
|
|
||||||
this.playerInput.addEventListener('input', () => {
|
|
||||||
this.adjustTextareaHeight();
|
|
||||||
this.updateCursorPosition();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update cursor on various events
|
|
||||||
this.playerInput.addEventListener('click', this.updateCursorPosition.bind(this));
|
|
||||||
this.playerInput.addEventListener('keyup', this.updateCursorPosition.bind(this));
|
|
||||||
|
|
||||||
// Show/hide cursor on focus/blur
|
|
||||||
this.playerInput.addEventListener('focus', () => {
|
|
||||||
if (this.cursor) this.cursor.style.opacity = '1';
|
|
||||||
this.updateCursorPosition();
|
|
||||||
});
|
|
||||||
this.playerInput.addEventListener('blur', () => {
|
|
||||||
if (this.cursor) this.cursor.style.opacity = '0';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle paste events
|
|
||||||
this.playerInput.addEventListener('paste', () => {
|
|
||||||
// Use setTimeout to let the paste complete before adjusting
|
|
||||||
setTimeout(() => {
|
|
||||||
this.adjustTextareaHeight();
|
|
||||||
this.updateCursorPosition();
|
|
||||||
}, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
this.adjustTextareaHeight();
|
|
||||||
this.updateCursorPosition();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit the current command.
|
|
||||||
*/
|
|
||||||
submitCommand() {
|
|
||||||
const command = this.playerInput.value.trim();
|
|
||||||
if (command === '' || !this.commandSubmitCallback) return;
|
|
||||||
|
|
||||||
// Fade out the input field container
|
|
||||||
if (this.commandInputContainer) {
|
|
||||||
this.commandInputContainer.classList.add('fading');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable input temporarily
|
|
||||||
this.playerInput.disabled = true;
|
|
||||||
|
|
||||||
// Call the registered callback
|
|
||||||
this.commandSubmitCallback(command);
|
|
||||||
|
|
||||||
// Clear input
|
|
||||||
this.clearInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the input field and resets its state.
|
|
||||||
*/
|
|
||||||
clearInput() {
|
|
||||||
this.playerInput.value = '';
|
|
||||||
this.resetCursorPosition();
|
|
||||||
this.adjustTextareaHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-enables the input field after a command submission or response.
|
|
||||||
*/
|
|
||||||
enableInput() {
|
|
||||||
if (this.commandInputContainer) {
|
|
||||||
// Remove fading class and add fade-in animation
|
|
||||||
this.commandInputContainer.classList.remove('fading');
|
|
||||||
this.commandInputContainer.classList.add('fade-in-input');
|
|
||||||
|
|
||||||
// Remove animation class after it completes
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.commandInputContainer) {
|
|
||||||
this.commandInputContainer.classList.remove('fade-in-input');
|
|
||||||
}
|
|
||||||
}, 500); // Match CSS animation duration
|
|
||||||
}
|
|
||||||
|
|
||||||
this.playerInput.disabled = false;
|
|
||||||
this.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focuses the input field.
|
|
||||||
*/
|
|
||||||
focus() {
|
|
||||||
this.playerInput.focus();
|
|
||||||
// Ensure cursor is visible and positioned correctly after focus
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.cursor) this.cursor.style.opacity = '1';
|
|
||||||
this.updateCursorPosition();
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current value of the input field.
|
|
||||||
* @returns {string} The input text.
|
|
||||||
*/
|
|
||||||
getValue() {
|
|
||||||
return this.playerInput.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the value of the input field.
|
|
||||||
* @param {string} value - The text to set.
|
|
||||||
*/
|
|
||||||
setValue(value) {
|
|
||||||
this.playerInput.value = value;
|
|
||||||
this.adjustTextareaHeight();
|
|
||||||
this.updateCursorPosition();
|
|
||||||
this.focus(); // Focus after setting value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the cursor position to the start.
|
|
||||||
*/
|
|
||||||
resetCursorPosition() {
|
|
||||||
if (this.cursor) {
|
|
||||||
this.cursor.style.left = '0px';
|
|
||||||
// Adjust top based on computed style padding or a default
|
|
||||||
const computedStyle = window.getComputedStyle(this.playerInput);
|
|
||||||
const paddingTop = parseFloat(computedStyle.paddingTop) || 6;
|
|
||||||
this.cursor.style.top = `${paddingTop}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the custom cursor position based on input text and caret position.
|
|
||||||
* Uses a temporary div for accurate measurement.
|
|
||||||
*/
|
|
||||||
updateCursorPosition() {
|
|
||||||
if (!this.cursor || !this.playerInput) return;
|
|
||||||
|
|
||||||
const input = this.playerInput;
|
|
||||||
const cursor = this.cursor;
|
|
||||||
const caretPosition = input.selectionStart || 0;
|
|
||||||
const inputText = input.value;
|
|
||||||
|
|
||||||
// If no text, position cursor at the beginning based on padding
|
|
||||||
if (inputText.length === 0 && caretPosition === 0) {
|
|
||||||
this.resetCursorPosition();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a temporary measurement div
|
|
||||||
const div = document.createElement('div');
|
|
||||||
const style = getComputedStyle(input);
|
|
||||||
|
|
||||||
// Apply relevant styles from the textarea to the div
|
|
||||||
div.style.position = 'absolute';
|
|
||||||
div.style.top = '-9999px';
|
|
||||||
div.style.left = '-9999px';
|
|
||||||
div.style.width = style.width;
|
|
||||||
div.style.height = 'auto';
|
|
||||||
div.style.padding = style.padding;
|
|
||||||
div.style.border = style.border;
|
|
||||||
div.style.fontFamily = style.fontFamily;
|
|
||||||
div.style.fontSize = style.fontSize;
|
|
||||||
div.style.fontWeight = style.fontWeight;
|
|
||||||
div.style.lineHeight = style.lineHeight;
|
|
||||||
div.style.whiteSpace = 'pre-wrap';
|
|
||||||
div.style.wordWrap = 'break-word';
|
|
||||||
div.style.boxSizing = style.boxSizing;
|
|
||||||
|
|
||||||
// Create spans for text before and after the caret, and a marker span
|
|
||||||
const preCaretText = document.createTextNode(inputText.substring(0, caretPosition));
|
|
||||||
const caretMarker = document.createElement('span');
|
|
||||||
caretMarker.innerHTML = ' '; // Use non-breaking space for measurement
|
|
||||||
const postCaretText = document.createTextNode(inputText.substring(caretPosition));
|
|
||||||
|
|
||||||
// Append spans to the div
|
|
||||||
div.appendChild(preCaretText);
|
|
||||||
div.appendChild(caretMarker);
|
|
||||||
div.appendChild(postCaretText);
|
|
||||||
|
|
||||||
// Append div to body for measurement
|
|
||||||
document.body.appendChild(div);
|
|
||||||
|
|
||||||
// Get position relative to the div's content box
|
|
||||||
const markerRect = caretMarker.getBoundingClientRect();
|
|
||||||
const divRect = div.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Calculate position relative to the input's top-left, considering scroll
|
|
||||||
const cursorLeft = markerRect.left - divRect.left;
|
|
||||||
const cursorTop = markerRect.top - divRect.top - input.scrollTop;
|
|
||||||
|
|
||||||
// Set cursor position
|
|
||||||
cursor.style.left = `${cursorLeft}px`;
|
|
||||||
cursor.style.top = `${cursorTop}px`;
|
|
||||||
|
|
||||||
// Clean up the temporary div
|
|
||||||
document.body.removeChild(div);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust textarea height based on its content.
|
|
||||||
*/
|
|
||||||
adjustTextareaHeight() {
|
|
||||||
if (!this.playerInput) return;
|
|
||||||
const textarea = this.playerInput;
|
|
||||||
// Temporarily reset height to accurately measure scrollHeight
|
|
||||||
textarea.style.height = 'auto';
|
|
||||||
// Set height to scrollHeight to fit content, adding a small buffer if needed
|
|
||||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up focus management to keep the input field focused.
|
|
||||||
* Note: Some parts might be better handled by the main application logic
|
|
||||||
* depending on overall focus requirements (e.g., clicking outside input).
|
|
||||||
*/
|
|
||||||
setupFocusManagement() {
|
|
||||||
// Focus input field when the handler is initialized
|
|
||||||
this.focus();
|
|
||||||
|
|
||||||
// Re-focus input when user returns to this browser tab/window
|
|
||||||
window.addEventListener('focus', () => this.focus());
|
|
||||||
window.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
setTimeout(() => this.focus(), 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optional: Add a listener to the document to refocus if needed,
|
|
||||||
// but be careful not to interfere with other interactive elements.
|
|
||||||
/*
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
// Example: Refocus if click is not on specific elements
|
|
||||||
if (!e.target.closest('button, a, .interactive-ui-element')) {
|
|
||||||
this.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
onpage-dialog.preload.js:121 Uncaught ReferenceError: browser is not defined
|
|
||||||
at start (onpage-dialog.preload.js:121:5)
|
|
||||||
at onpage-dialog.preload.js:135:1
|
|
||||||
at onpage-dialog.preload.js:393:12
|
|
||||||
start @ onpage-dialog.preload.js:121
|
|
||||||
(anonymous) @ onpage-dialog.preload.js:135
|
|
||||||
(anonymous) @ onpage-dialog.preload.js:393
|
|
||||||
loader.js:12 Module registry initialized and assigned to window.moduleRegistry
|
|
||||||
loader.js:51 Module Loader: Initialization started
|
|
||||||
module-registry.js:111 Module Registry: Tracking potential circular dependency with unregistered module: localization
|
|
||||||
module-registry.js:111 Module Registry: Tracking potential circular dependency with unregistered module: text-processor
|
|
||||||
module-registry.js:111 Module Registry: Tracking potential circular dependency with unregistered module: tts-factory
|
|
||||||
ui-input-handler.js:35 UIInputHandler: Constructor initialized
|
|
||||||
module-registry.js:111 Module Registry: Tracking potential circular dependency with unregistered module: ui-display-handler
|
|
||||||
ui-input-handler.js:398 UIInputHandler: Registering with window
|
|
||||||
ui-effects.js:48 UIEffects: Constructor initialized
|
|
||||||
ui-effects.js:310 UIEffects: Registering with window
|
|
||||||
ui-display-handler.js:61 UIDisplayHandler: Constructor initialized
|
|
||||||
module-registry.js:111 Module Registry: Tracking potential circular dependency with unregistered module: text-processor
|
|
||||||
ui-display-handler.js:581 UIDisplayHandler: Registering with window
|
|
||||||
module-registry.js:111 Module Registry: Tracking potential circular dependency with unregistered module: text-processor
|
|
||||||
module-registry.js:111 Module Registry: Tracking potential circular dependency with unregistered module: socket-client
|
|
||||||
module-registry.js:111 Module Registry: Tracking potential circular dependency with unregistered module: text-processor
|
|
||||||
module-registry.js:111 Module Registry: Tracking potential circular dependency with unregistered module: tts
|
|
||||||
loader.js:168 Module dependencies:
|
|
||||||
loader.js:171 persistence-manager depends on: localization
|
|
||||||
loader.js:171 localization depends on: none
|
|
||||||
loader.js:171 paragraph-layout depends on: text-processor
|
|
||||||
loader.js:171 animation-queue depends on: none
|
|
||||||
loader.js:171 layout-renderer depends on: animation-queue
|
|
||||||
loader.js:171 audio-manager depends on: none
|
|
||||||
loader.js:171 text-buffer depends on: none
|
|
||||||
loader.js:171 tts-player depends on: tts-factory
|
|
||||||
loader.js:171 ui-input-handler depends on: ui-display-handler
|
|
||||||
loader.js:171 ui-effects depends on: none
|
|
||||||
loader.js:171 ui-display-handler depends on: paragraph-layout, layout-renderer, animation-queue
|
|
||||||
loader.js:171 ui-controller depends on: animation-queue, ui-display-handler, ui-input-handler, ui-effects, text-buffer, socket-client
|
|
||||||
socket-client depends on: text-buffer
|
|
||||||
options-ui depends on: persistence-manager, localization
|
|
||||||
game-loop depends on: ui-controller, socket-client, tts, text-buffer
|
|
||||||
text-processor depends on: localization
|
|
||||||
tts-factory depends on: persistence-manager, localization
|
|
||||||
Starting initialization of module: persistence-manager
|
|
||||||
Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (PENDING)', 'paragraph-layout (PENDING)', 'animation-queue (PENDING)', 'layout-renderer (PENDING)', 'audio-manager (PENDING)', 'text-buffer (PENDING)', 'tts-player (PENDING)', 'ui-input-handler (PENDING)', 'ui-effects (PENDING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
Starting initialization of module: localization
|
|
||||||
Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (PENDING)', 'animation-queue (PENDING)', 'layout-renderer (PENDING)', 'audio-manager (PENDING)', 'text-buffer (PENDING)', 'tts-player (PENDING)', 'ui-input-handler (PENDING)', 'ui-effects (PENDING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
Starting initialization of module: paragraph-layout
|
|
||||||
Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (PENDING)', 'layout-renderer (PENDING)', 'audio-manager (PENDING)', 'text-buffer (PENDING)', 'tts-player (PENDING)', 'ui-input-handler (PENDING)', 'ui-effects (PENDING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: animation-queue
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (PENDING)', 'audio-manager (PENDING)', 'text-buffer (PENDING)', 'tts-player (PENDING)', 'ui-input-handler (PENDING)', 'ui-effects (PENDING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: layout-renderer
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (PENDING)', 'text-buffer (PENDING)', 'tts-player (PENDING)', 'ui-input-handler (PENDING)', 'ui-effects (PENDING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: audio-manager
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (PENDING)', 'tts-player (PENDING)', 'ui-input-handler (PENDING)', 'ui-effects (PENDING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: text-buffer
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (PENDING)', 'ui-input-handler (PENDING)', 'ui-effects (PENDING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: tts-player
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (PENDING)', 'ui-effects (PENDING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: ui-input-handler
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (PENDING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: ui-effects
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (PENDING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: ui-display-handler
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (PENDING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: ui-controller
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (PENDING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: socket-client
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (PENDING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: options-ui
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (PENDING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: game-loop
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (PENDING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: text-processor
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (PENDING)']
|
|
||||||
loader.js:197 Starting initialization of module: tts-factory
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (LOADING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'animation-queue (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'animation-queue (INITIALIZING)', 'layout-renderer (LOADING)', 'audio-manager (LOADING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'animation-queue (INITIALIZING)', 'layout-renderer (LOADING)', 'audio-manager (INITIALIZING)', 'text-buffer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'animation-queue (INITIALIZING)', 'layout-renderer (LOADING)', 'audio-manager (INITIALIZING)', 'text-buffer (INITIALIZING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (17) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'animation-queue (INITIALIZING)', 'layout-renderer (LOADING)', 'audio-manager (INITIALIZING)', 'text-buffer (INITIALIZING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (INITIALIZING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
ui-effects.js:79 UIEffects: Setting up effect elements
|
|
||||||
loader.js:354 Modules still pending: (16) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'layout-renderer (LOADING)', 'audio-manager (INITIALIZING)', 'text-buffer (INITIALIZING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (INITIALIZING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (15) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'layout-renderer (LOADING)', 'text-buffer (INITIALIZING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (INITIALIZING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (14) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'layout-renderer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-effects (INITIALIZING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (13) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'layout-renderer (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:203 Completed initialization of module: animation-queue
|
|
||||||
loader.js:203 Completed initialization of module: audio-manager
|
|
||||||
loader.js:203 Completed initialization of module: text-buffer
|
|
||||||
loader.js:203 Completed initialization of module: ui-effects
|
|
||||||
loader.js:354 Modules still pending: (13) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'layout-renderer (INITIALIZING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (LOADING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (13) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'layout-renderer (INITIALIZING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (INITIALIZING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (12) ['persistence-manager (LOADING)', 'localization (INITIALIZING)', 'paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (INITIALIZING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:203 Completed initialization of module: layout-renderer
|
|
||||||
localization.js:102
|
|
||||||
|
|
||||||
|
|
||||||
GET http://localhost:3001/locales/en-us.json 404 (Not Found)
|
|
||||||
loadTranslations @ localization.js:102
|
|
||||||
initialize @ localization.js:61
|
|
||||||
initializeInterface @ base-module.js:55
|
|
||||||
await in initializeInterface
|
|
||||||
(anonymous) @ loader.js:200
|
|
||||||
initializeModules @ loader.js:175
|
|
||||||
(anonymous) @ loader.js:75
|
|
||||||
Promise.then
|
|
||||||
init @ loader.js:73
|
|
||||||
(anonymous) @ loader.js:586
|
|
||||||
localization.js:111
|
|
||||||
|
|
||||||
|
|
||||||
GET http://localhost:3001/locales/en.json 404 (Not Found)
|
|
||||||
loadTranslations @ localization.js:111
|
|
||||||
await in loadTranslations
|
|
||||||
initialize @ localization.js:61
|
|
||||||
initializeInterface @ base-module.js:55
|
|
||||||
await in initializeInterface
|
|
||||||
(anonymous) @ loader.js:200
|
|
||||||
initializeModules @ loader.js:175
|
|
||||||
(anonymous) @ loader.js:75
|
|
||||||
Promise.then
|
|
||||||
init @ loader.js:73
|
|
||||||
(anonymous) @ loader.js:586
|
|
||||||
localization.js:122 English translations not found, using empty set
|
|
||||||
loadTranslations @ localization.js:122
|
|
||||||
await in loadTranslations
|
|
||||||
initialize @ localization.js:61
|
|
||||||
initializeInterface @ base-module.js:55
|
|
||||||
await in initializeInterface
|
|
||||||
(anonymous) @ loader.js:200
|
|
||||||
initializeModules @ loader.js:175
|
|
||||||
(anonymous) @ loader.js:75
|
|
||||||
Promise.then
|
|
||||||
init @ loader.js:73
|
|
||||||
(anonymous) @ loader.js:586
|
|
||||||
localization.js:102
|
|
||||||
|
|
||||||
|
|
||||||
GET http://localhost:3001/locales/de.json 404 (Not Found)
|
|
||||||
loadTranslations @ localization.js:102
|
|
||||||
initialize @ localization.js:68
|
|
||||||
await in initialize
|
|
||||||
initializeInterface @ base-module.js:55
|
|
||||||
await in initializeInterface
|
|
||||||
(anonymous) @ loader.js:200
|
|
||||||
initializeModules @ loader.js:175
|
|
||||||
(anonymous) @ loader.js:75
|
|
||||||
Promise.then
|
|
||||||
init @ loader.js:73
|
|
||||||
(anonymous) @ loader.js:586
|
|
||||||
loader.js:354 Modules still pending: (11) ['persistence-manager (LOADING)', 'paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (INITIALIZING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:203 Completed initialization of module: localization
|
|
||||||
loader.js:354 Modules still pending: (11) ['persistence-manager (INITIALIZING)', 'paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (INITIALIZING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (LOADING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (11) ['persistence-manager (INITIALIZING)', 'paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (INITIALIZING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (INITIALIZING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (10) ['paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (INITIALIZING)', 'options-ui (LOADING)', 'game-loop (LOADING)', 'text-processor (INITIALIZING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:203 Completed initialization of module: persistence-manager
|
|
||||||
loader.js:354 Modules still pending: (10) ['paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (INITIALIZING)', 'options-ui (INITIALIZING)', 'game-loop (LOADING)', 'text-processor (INITIALIZING)', 'tts-factory (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (10) ['paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (INITIALIZING)', 'options-ui (INITIALIZING)', 'game-loop (LOADING)', 'text-processor (INITIALIZING)', 'tts-factory (INITIALIZING)']
|
|
||||||
loader.js:354 Modules still pending: (9) ['paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'socket-client (INITIALIZING)', 'game-loop (LOADING)', 'text-processor (INITIALIZING)', 'tts-factory (INITIALIZING)']
|
|
||||||
loader.js:203 Completed initialization of module: options-ui
|
|
||||||
socket-client.js:81 Socket Client: Using origin for connection: http://localhost:3001
|
|
||||||
loader.js:354 Modules still pending: (8) ['paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'game-loop (LOADING)', 'text-processor (INITIALIZING)', 'tts-factory (INITIALIZING)']
|
|
||||||
loader.js:203 Completed initialization of module: socket-client
|
|
||||||
browser-tts-handler.js:227 Browser TTS: Loaded 23 voices from event
|
|
||||||
browser-tts-handler.js:269 Browser TTS: Using de voice: Microsoft Hedda - German (Germany)
|
|
||||||
tts-factory.js:201 TTS Factory: Successfully initialized browser TTS handler
|
|
||||||
text-processor.js:145 SmartyPants loaded successfully
|
|
||||||
text-processor.js:171 Initializing hyphenation with Hyphenopoly module
|
|
||||||
text-processor.js:185 Loading hyphenation pattern: /js/patterns/de.wasm
|
|
||||||
kokoro-handler.js:104 Kokoro worker is ready
|
|
||||||
kokoro-handler.js:202 Kokoro worker initialized successfully
|
|
||||||
kokoro-handler.js:753 Kokoro TTS: Set voice to German (Neural)
|
|
||||||
tts-factory.js:201 TTS Factory: Successfully initialized kokoro TTS handler
|
|
||||||
loader.js:354 Modules still pending: (7) ['paragraph-layout (LOADING)', 'tts-player (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'game-loop (LOADING)', 'text-processor (INITIALIZING)']
|
|
||||||
loader.js:203 Completed initialization of module: tts-factory
|
|
||||||
loader.js:354 Modules still pending: (7) ['paragraph-layout (LOADING)', 'tts-player (INITIALIZING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'game-loop (LOADING)', 'text-processor (INITIALIZING)']
|
|
||||||
loader.js:354 Modules still pending: (6) ['paragraph-layout (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'game-loop (LOADING)', 'text-processor (INITIALIZING)']
|
|
||||||
loader.js:203 Completed initialization of module: tts-player
|
|
||||||
text-processor.js:208 Hyphenopoly engine ready for de
|
|
||||||
text-processor.js:218 Hyphenator ready for de
|
|
||||||
loader.js:354 Modules still pending: (5) ['paragraph-layout (LOADING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'game-loop (LOADING)']
|
|
||||||
loader.js:203 Completed initialization of module: text-processor
|
|
||||||
loader.js:354 Modules still pending: (5) ['paragraph-layout (INITIALIZING)', 'ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'game-loop (LOADING)']
|
|
||||||
loader.js:354 Modules still pending: (4) ['ui-input-handler (LOADING)', 'ui-display-handler (LOADING)', 'ui-controller (LOADING)', 'game-loop (LOADING)']
|
|
||||||
loader.js:203 Completed initialization of module: paragraph-layout
|
|
||||||
loader.js:354 Modules still pending: (4) ['ui-input-handler (LOADING)', 'ui-display-handler (INITIALIZING)', 'ui-controller (LOADING)', 'game-loop (LOADING)']
|
|
||||||
ui-display-handler.js:162 UIDisplayHandler: CSS /css/style.css loaded successfully
|
|
||||||
ui-display-handler.js:212 UIDisplayHandler: Book container not found, creating it
|
|
||||||
ui-display-handler.js:221 UIDisplayHandler: Left page not found, creating it
|
|
||||||
ui-display-handler.js:285 UIDisplayHandler: Right page not found, creating it
|
|
||||||
ui-display-handler.js:294 UIDisplayHandler: Story container not found, creating it
|
|
||||||
ui-display-handler.js:304 UIDisplayHandler: Paragraphs container not found, creating it
|
|
||||||
ui-display-handler.js:326 UIDisplayHandler: All containers initialized
|
|
||||||
loader.js:354 Modules still pending: (3) ['ui-input-handler (LOADING)', 'ui-controller (LOADING)', 'game-loop (LOADING)']
|
|
||||||
loader.js:203 Completed initialization of module: ui-display-handler
|
|
||||||
loader.js:354 Modules still pending: (3) ['ui-input-handler (INITIALIZING)', 'ui-controller (LOADING)', 'game-loop (LOADING)']
|
|
||||||
ui-input-handler.js:83 UIInputHandler: Setting up input elements in document flow
|
|
||||||
ui-input-handler.js:182 UIInputHandler: Input elements setup complete
|
|
||||||
loader.js:354 Modules still pending: (2) ['ui-controller (LOADING)', 'game-loop (LOADING)']
|
|
||||||
loader.js:203 Completed initialization of module: ui-input-handler
|
|
||||||
loader.js:354 Modules still pending: (2) ['ui-controller (INITIALIZING)', 'game-loop (LOADING)']0: "ui-controller (INITIALIZING)"1: "game-loop (LOADING)"length: 2[[Prototype]]: Array(0)
|
|
||||||
ui-controller.js:271 UIController: Setting up text buffer callback
|
|
||||||
text-buffer.js:68 Text Buffer: Sentence ready callback set
|
|
||||||
ui-controller.js:292 UIController: Text buffer callback set up
|
|
||||||
loader.js:354 Modules still pending: ['game-loop (LOADING)']0: "game-loop (LOADING)"length: 1[[Prototype]]: Array(0)
|
|
||||||
loader.js:203 Completed initialization of module: ui-controller
|
|
||||||
loader.js:354 Modules still pending: ['game-loop (WAITING)']0: "game-loop (WAITING)"length: 1[[Prototype]]: Array(0)
|
|
||||||
loader.js:203 Completed initialization of module: game-loop
|
|
||||||
animation-queue.js:52 Animation Queue: TTS module not found yet, will try again when needed
|
|
||||||
layout-renderer.js:55 Layout Renderer: TTS Player module not found yet, will try again when needed
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
/**
|
|
||||||
* UiController Module
|
|
||||||
* Manages user interface interactions and updates UI elements.
|
|
||||||
*/
|
|
||||||
export class UiController {
|
|
||||||
/**
|
|
||||||
* Create a new UiController
|
|
||||||
* @param {Object} config - Configuration options
|
|
||||||
* @param {Object} config.animationQueue - The AnimationQueue instance
|
|
||||||
* @param {Object} config.ttsPlayer - The TtsPlayer instance
|
|
||||||
* @param {Object} config.inputHandler - The InputHandler instance
|
|
||||||
* @param {Object} config.socketClient - The SocketClient instance (or rely on callbacks)
|
|
||||||
* @param {HTMLElement} config.commandHistoryContainerElement - The command history container
|
|
||||||
* @param {HTMLElement} config.storyContainerElement - The story container
|
|
||||||
* @param {HTMLElement} config.speedSliderElement - The speed slider element
|
|
||||||
* @param {HTMLElement} config.rewindButtonElement - The rewind button element
|
|
||||||
* @param {HTMLElement} config.saveButtonElement - The save button element
|
|
||||||
* @param {HTMLElement} config.loadButtonElement - The load button element
|
|
||||||
* @param {HTMLElement} config.speechButtonElement - The speech button element
|
|
||||||
* @param {HTMLElement} config.speedResetElement - The speed reset button element
|
|
||||||
* @param {Object} config.translations - Translations object
|
|
||||||
* @param {string} config.locale - Locale string
|
|
||||||
*/
|
|
||||||
constructor(config = {}) {
|
|
||||||
// Store dependencies
|
|
||||||
this.animationQueue = config.animationQueue;
|
|
||||||
this.ttsPlayer = config.ttsPlayer; // Handles enabling/disabling TTS via its own logic
|
|
||||||
this.inputHandler = config.inputHandler; // Needed for focus, suggestions?
|
|
||||||
this.socketClient = config.socketClient; // Direct access or use callbacks
|
|
||||||
|
|
||||||
// Callbacks for actions (to be set by AnimatedFiction)
|
|
||||||
this.onRestartRequest = null;
|
|
||||||
this.onSaveRequest = null;
|
|
||||||
this.onLoadRequest = null;
|
|
||||||
|
|
||||||
// Active TTS handler (set via setTtsHandler)
|
|
||||||
this.ttsHandler = null;
|
|
||||||
|
|
||||||
// UI elements
|
|
||||||
this.speedSlider = config.speedSliderElement || document.getElementById('speed');
|
|
||||||
this.commandHistoryContainer = config.commandHistoryContainerElement; // Added
|
|
||||||
this.storyContainer = config.storyContainerElement; // Added
|
|
||||||
this.rewindButton = config.rewindButtonElement || document.getElementById('rewind');
|
|
||||||
this.saveButton = config.saveButtonElement || document.getElementById('save');
|
|
||||||
this.loadButton = config.loadButtonElement || document.getElementById('reload');
|
|
||||||
this.speechButton = config.speechButtonElement || document.getElementById('speech');
|
|
||||||
this.speedReset = config.speedResetElement || document.getElementById('speed_reset');
|
|
||||||
|
|
||||||
// Translations
|
|
||||||
this.translations = config.translations || {};
|
|
||||||
this.locale = config.locale || 'en-us';
|
|
||||||
|
|
||||||
// Initial UI state
|
|
||||||
this.updateButtonStates({ started: false, canLoad: false }); // Start with buttons disabled
|
|
||||||
this.updateSpeechButtonAvailability(false); // Start with speech disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up event listeners
|
|
||||||
*/
|
|
||||||
setupEventListeners() {
|
|
||||||
// Speed slider
|
|
||||||
if (this.speedSlider) {
|
|
||||||
this.speedSlider.addEventListener('input', this.handleSpeedChange.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speed reset button
|
|
||||||
if (this.speedReset) {
|
|
||||||
this.speedReset.addEventListener('click', this.handleSpeedReset.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rewind button
|
|
||||||
if (this.rewindButton) {
|
|
||||||
this.rewindButton.addEventListener('click', this.handleRewindClick.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save button
|
|
||||||
if (this.saveButton) {
|
|
||||||
this.saveButton.addEventListener('click', this.handleSaveClick.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load button
|
|
||||||
if (this.loadButton) {
|
|
||||||
this.loadButton.addEventListener('click', this.handleLoadClick.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speech button
|
|
||||||
if (this.speechButton) {
|
|
||||||
this.speechButton.addEventListener('click', this.handleSpeechToggle.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fast forward (spacebar or click on right page)
|
|
||||||
window.addEventListener('keydown', (event) => {
|
|
||||||
if (event.code === 'Space') {
|
|
||||||
this.handleFastForward();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('page_right')?.addEventListener('click', this.handleFastForward.bind(this));
|
|
||||||
|
|
||||||
// Window resize
|
|
||||||
window.addEventListener('resize', this.handleWindowResize.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle speed slider change
|
|
||||||
* @param {Event} event - The input event
|
|
||||||
*/
|
|
||||||
handleSpeedChange(event) {
|
|
||||||
if (!this.animationQueue) return;
|
|
||||||
|
|
||||||
const value = parseFloat(event.target.value);
|
|
||||||
const speed = Math.pow(100.0 - value, 3) / 10000 * 10 + 0.01;
|
|
||||||
this.animationQueue.setSpeed(speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle speed reset button click
|
|
||||||
*/
|
|
||||||
handleSpeedReset() {
|
|
||||||
if (!this.speedSlider || !this.animationQueue) return;
|
|
||||||
|
|
||||||
this.speedSlider.value = 50;
|
|
||||||
const speed = Math.pow(100.0 - 50, 3) / 10000 * 10 + 0.01;
|
|
||||||
this.animationQueue.setSpeed(speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle rewind button click
|
|
||||||
*/
|
|
||||||
handleRewindClick() {
|
|
||||||
if (this.rewindButton.getAttribute('disabled') === 'disabled') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Use localized confirm message if available
|
|
||||||
const confirmMsg = this.translations[this.locale]?.confirm_restart || 'Are you sure you want to restart the game? All progress will be lost.';
|
|
||||||
if (confirm(confirmMsg)) {
|
|
||||||
if (this.onRestartRequest) {
|
|
||||||
this.onRestartRequest();
|
|
||||||
} else {
|
|
||||||
console.warn("UiController: onRestartRequest callback not set.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle save button click
|
|
||||||
*/
|
|
||||||
handleSaveClick() {
|
|
||||||
if (this.saveButton.getAttribute('disabled') === 'disabled') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.onSaveRequest) {
|
|
||||||
this.onSaveRequest();
|
|
||||||
} else {
|
|
||||||
console.warn("UiController: onSaveRequest callback not set.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle load button click
|
|
||||||
*/
|
|
||||||
handleLoadClick() {
|
|
||||||
if (this.loadButton.getAttribute('disabled') === 'disabled') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.onLoadRequest) {
|
|
||||||
this.onLoadRequest();
|
|
||||||
} else {
|
|
||||||
console.warn("UiController: onLoadRequest callback not set.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle speech toggle button click
|
|
||||||
*/
|
|
||||||
handleSpeechToggle() {
|
|
||||||
if (!this.ttsHandler) {
|
|
||||||
console.warn("UiController: ttsHandler not set. Cannot toggle speech.");
|
|
||||||
// Attempt to use ttsPlayer as fallback if needed, but prefer ttsHandler
|
|
||||||
if (this.ttsPlayer && this.speechButton.getAttribute('disabled') !== 'disabled') {
|
|
||||||
const enabled = this.ttsPlayer.toggle();
|
|
||||||
this.updateSpeechButtonStyling(enabled);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.speechButton.getAttribute('disabled') === 'disabled') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure AudioContext is resumed on user interaction if using Kokoro
|
|
||||||
if (window.ttsFactory && window.ttsFactory.usingKokoro && this.ttsHandler.audioContext && this.ttsHandler.audioContext.state === 'suspended') {
|
|
||||||
this.ttsHandler.audioContext.resume().catch(err => console.error('Error resuming AudioContext on click:', err));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set user activation flag for the handler
|
|
||||||
this.ttsHandler.hasUserActivation = true;
|
|
||||||
const enabled = this.ttsHandler.toggle();
|
|
||||||
this.updateSpeechButtonStyling(enabled); // Update visual style
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
// Speak the last narrative if speech was just enabled and story container is available
|
|
||||||
if (this.storyContainer) {
|
|
||||||
const lastNarrative = this.storyContainer.lastElementChild;
|
|
||||||
if (lastNarrative && lastNarrative.classList.contains('narrative')) { // Check if it's narrative text
|
|
||||||
console.log("Speaking last narrative on toggle");
|
|
||||||
// Use a slight delay to ensure audio context is resumed
|
|
||||||
setTimeout(() => this.ttsHandler.speak(lastNarrative.textContent), 50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If disabling, ensure speech stops
|
|
||||||
this.ttsHandler.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle fast forward (spacebar or click)
|
|
||||||
*/
|
|
||||||
handleFastForward() {
|
|
||||||
if (!this.animationQueue) return;
|
|
||||||
|
|
||||||
this.animationQueue.fastForward();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle window resize
|
|
||||||
*/
|
|
||||||
handleWindowResize() {
|
|
||||||
this.updateBookDimensions();
|
|
||||||
this.updateParagraphHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the active TTS handler.
|
|
||||||
* @param {object} handler - The TTS handler instance (e.g., KokoroHandler, BrowserTtsHandler).
|
|
||||||
*/
|
|
||||||
setTtsHandler(handler) {
|
|
||||||
this.ttsHandler = handler;
|
|
||||||
console.log("UiController: TTS Handler set.", handler);
|
|
||||||
// Update button state based on the new handler's status
|
|
||||||
this.updateSpeechButtonStyling(this.ttsHandler ? this.ttsHandler.isEnabled() : false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the book dimensions based on viewport size
|
|
||||||
*/
|
|
||||||
updateBookDimensions() {
|
|
||||||
const vw = window.innerWidth;
|
|
||||||
const vh = window.innerHeight;
|
|
||||||
const viewportAspectRatio = vw / vh;
|
|
||||||
const imageAspectRatio = 2727 / 1691;
|
|
||||||
|
|
||||||
let bookWidth, bookHeight;
|
|
||||||
|
|
||||||
if (viewportAspectRatio > imageAspectRatio) {
|
|
||||||
bookWidth = vh * imageAspectRatio;
|
|
||||||
bookHeight = vh;
|
|
||||||
} else {
|
|
||||||
bookWidth = vw;
|
|
||||||
bookHeight = vw / imageAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--book-width', `${bookWidth}px`);
|
|
||||||
document.documentElement.style.setProperty('--book-height', `${bookHeight}px`);
|
|
||||||
|
|
||||||
// Setting a CSS variable that will be either vw or vh depending on the viewport aspect ratio
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--viewport-dimension",
|
|
||||||
viewportAspectRatio > imageAspectRatio ? 'vw' : 'vh'
|
|
||||||
);
|
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--viewport-aspect-ratio', viewportAspectRatio);
|
|
||||||
|
|
||||||
const story = document.getElementById("story");
|
|
||||||
if (story) {
|
|
||||||
const paddingTop = window.getComputedStyle(story).paddingTop;
|
|
||||||
const paddingBottom = window.getComputedStyle(story).paddingBottom;
|
|
||||||
document.documentElement.style.setProperty('--story-line-height', (story.clientHeight - paddingTop - paddingBottom) / 28);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update paragraph heights based on viewport
|
|
||||||
*/
|
|
||||||
updateParagraphHeight() {
|
|
||||||
document.querySelectorAll("#story p").forEach((element) => {
|
|
||||||
if (element.dataset.vpc) {
|
|
||||||
const pHeight = parseFloat(window.getComputedStyle(document.getElementById('page_right')).height);
|
|
||||||
const newHeight = pHeight * element.dataset.vpc / 100 + 'px';
|
|
||||||
element.style.height = newHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the speech button styling based on enabled state.
|
|
||||||
* @param {boolean} enabled - Whether speech is enabled.
|
|
||||||
*/
|
|
||||||
updateSpeechButtonStyling(enabled = false) {
|
|
||||||
if (!this.speechButton) return;
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
this.speechButton.style.fontWeight = 'bold';
|
|
||||||
this.speechButton.style.color = '#000';
|
|
||||||
this.speechButton.style.backgroundColor = '#eee';
|
|
||||||
} else {
|
|
||||||
this.speechButton.style.fontWeight = 'normal';
|
|
||||||
this.speechButton.style.color = '#333';
|
|
||||||
this.speechButton.style.backgroundColor = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the enabled/disabled state and title of the speech button.
|
|
||||||
* @param {boolean} available - Whether any TTS system is available.
|
|
||||||
* @param {string} [type] - The type of TTS system available ('kokoro', 'browser', etc.).
|
|
||||||
*/
|
|
||||||
updateSpeechButtonAvailability(available, type) {
|
|
||||||
if (!this.speechButton) return;
|
|
||||||
|
|
||||||
if (available) {
|
|
||||||
this.speechButton.removeAttribute('disabled');
|
|
||||||
const ttsName = type === 'kokoro' ? 'Kokoro TTS' : (type === 'browser' ? 'Browser TTS' : 'TTS');
|
|
||||||
const title = this.translations[this.locale]?.title_speech || `Toggle Text-to-Speech (${ttsName})`;
|
|
||||||
this.speechButton.setAttribute('title', title);
|
|
||||||
// Update style based on current handler state if available
|
|
||||||
this.updateSpeechButtonStyling(this.ttsHandler ? this.ttsHandler.isEnabled() : false);
|
|
||||||
} else {
|
|
||||||
this.speechButton.setAttribute('disabled', 'disabled');
|
|
||||||
const title = this.translations[this.locale]?.title_speech_unavailable || 'Text-to-Speech not available';
|
|
||||||
this.speechButton.setAttribute('title', title);
|
|
||||||
this.updateSpeechButtonStyling(false); // Ensure style is off
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the enabled/disabled state of control buttons based on game state.
|
|
||||||
* @param {object} gameState - The current game state from AnimatedFiction.
|
|
||||||
* @param {boolean} gameState.started - Whether the game has started.
|
|
||||||
* @param {boolean} [gameState.canLoad] - Whether a saved game exists to be loaded.
|
|
||||||
*/
|
|
||||||
updateButtonStates(gameState) {
|
|
||||||
if (this.rewindButton) {
|
|
||||||
if (gameState.started) {
|
|
||||||
this.rewindButton.removeAttribute('disabled');
|
|
||||||
} else {
|
|
||||||
this.rewindButton.setAttribute('disabled', 'disabled');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.saveButton) {
|
|
||||||
if (gameState.started) {
|
|
||||||
this.saveButton.removeAttribute('disabled');
|
|
||||||
} else {
|
|
||||||
this.saveButton.setAttribute('disabled', 'disabled');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.loadButton) {
|
|
||||||
// Enable load button if a save exists (indicated by canLoad flag or similar)
|
|
||||||
// We might need a more robust way to check for saved state existence.
|
|
||||||
// For now, enable if game started OR if canLoad is explicitly true.
|
|
||||||
if (gameState.started || gameState.canLoad) {
|
|
||||||
this.loadButton.removeAttribute('disabled');
|
|
||||||
} else {
|
|
||||||
this.loadButton.setAttribute('disabled', 'disabled');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Speech button availability is handled separately by updateSpeechButtonAvailability
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the visual display of the speed slider.
|
|
||||||
* @param {number} value - The speed value (0-100).
|
|
||||||
*/
|
|
||||||
updateSpeedDisplay(value) {
|
|
||||||
if (this.speedSlider) {
|
|
||||||
this.speedSlider.value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert an element after a delay (Helper, potentially move elsewhere or keep if used)
|
|
||||||
* @param {number} delay - The delay in milliseconds
|
|
||||||
* @param {HTMLElement} target - The target element to append to
|
|
||||||
* @param {HTMLElement} el - The element to insert
|
|
||||||
* @param {boolean} fadeIn - Whether to fade in the element
|
|
||||||
*/
|
|
||||||
insertAfter(delay, target, el, fadeIn = true) {
|
|
||||||
if (this.animationQueue) {
|
|
||||||
if (fadeIn) {
|
|
||||||
el.classList.add("fade-in");
|
|
||||||
this.animationQueue.schedule(function() {
|
|
||||||
target.appendChild(el);
|
|
||||||
}, delay);
|
|
||||||
} else {
|
|
||||||
this.animationQueue.schedule(function() {
|
|
||||||
target.appendChild(el);
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback if no animation queue
|
|
||||||
if (fadeIn) {
|
|
||||||
el.classList.add("fade-in");
|
|
||||||
setTimeout(() => {
|
|
||||||
target.appendChild(el);
|
|
||||||
}, delay);
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
target.appendChild(el);
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the locale for translations
|
|
||||||
* @param {string} locale - The locale code
|
|
||||||
*/
|
|
||||||
setLocale(locale) {
|
|
||||||
this.locale = locale;
|
|
||||||
|
|
||||||
if (this.translations[locale]) {
|
|
||||||
Object.keys(this.translations[locale]).forEach(key => {
|
|
||||||
const prefix = key.substring(0, 5);
|
|
||||||
const postfix = key.substring(6, key.length);
|
|
||||||
const elements = document.querySelectorAll(`.l10n-${(prefix === 'title' ? postfix : key)}`);
|
|
||||||
|
|
||||||
elements.forEach(element => {
|
|
||||||
if (prefix === "title") {
|
|
||||||
element.title = this.translations[locale][key];
|
|
||||||
} else {
|
|
||||||
element.innerHTML = this.translations[locale][key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error(`Locale ${locale} is not defined`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user