Fix WebGL timeline startup ordering

This commit is contained in:
2026-06-10 10:04:06 +02:00
parent ce8147b5b1
commit 623b42caf9
4 changed files with 79 additions and 6 deletions
+46 -5
View File
@@ -8,12 +8,11 @@ import { BaseModule } from './base-module.js';
class BookPlaybackTimelineModule extends BaseModule {
constructor() {
super('book-playback-timeline', 'Book Playback Timeline');
this.dependencies = ['book-pagination', 'book-texture-renderer', 'webgl-page-cache', 'playback-coordinator', 'sentence-queue'];
this.dependencies = ['book-pagination', 'book-texture-renderer', 'webgl-page-cache', 'playback-coordinator'];
this.pagination = null;
this.textureRenderer = null;
this.pageCache = null;
this.playbackCoordinator = null;
this.sentenceQueue = null;
this.activeSegment = null;
this.preparedSegments = new Map();
this.timelineDiagnostics = [];
@@ -26,6 +25,7 @@ class BookPlaybackTimelineModule extends BaseModule {
'prepareSentence',
'activatePreparedSegment',
'ensureAnimationTimings',
'calculateAnimationTiming',
'createPreparedSegment',
'createRevealDetail',
'applyTexturePlan',
@@ -55,7 +55,6 @@ class BookPlaybackTimelineModule extends BaseModule {
this.textureRenderer = this.getModule('book-texture-renderer');
this.pageCache = this.getModule('webgl-page-cache');
this.playbackCoordinator = this.getModule('playback-coordinator');
this.sentenceQueue = this.getModule('sentence-queue');
this.addEventListener(document, 'webgl-book:page-reveal-start', (event) => {
this.markBenchmark('reveal-start', {
blockId: event.detail?.blockId ?? null
@@ -210,8 +209,50 @@ class BookPlaybackTimelineModule extends BaseModule {
ensureAnimationTimings(sentence = {}) {
if (Array.isArray(sentence.animation?.wordTimings) && sentence.animation.wordTimings.length > 0) return;
const words = String(sentence.layoutText || sentence.text || '').match(/\S+/g) || [];
sentence.animation = this.sentenceQueue?.calculateAnimationTiming?.(words, sentence.tts?.duration || 0, sentence.cueMarkers || [])
|| { wordTimings: [], cueTimings: [], totalDuration: 0 };
sentence.animation = this.calculateAnimationTiming(words, sentence.tts?.duration || 0, sentence.cueMarkers || []);
}
calculateAnimationTiming(words = [], totalDuration = 0, cueMarkers = []) {
if (!Array.isArray(words) || words.length === 0) {
return {
wordTimings: [],
cueTimings: [],
totalDuration: 0
};
}
const totalChars = words.reduce((sum, word) => sum + String(word || '').length, 0);
if (totalChars === 0) {
return {
wordTimings: words.map(word => ({ word, delay: 0, duration: 0 })),
cueTimings: [],
totalDuration: 0
};
}
const msPerChar = Number(totalDuration || 0) / totalChars;
let currentDelay = 0;
const wordTimings = words.map(word => {
const duration = String(word || '').length * msPerChar;
const timing = {
word,
delay: currentDelay,
duration
};
currentDelay += duration;
return timing;
});
const cueTimings = (cueMarkers || []).map(cue => {
const wordIndex = Math.max(0, Math.min(cue.wordIndex || 0, wordTimings.length - 1));
const timing = wordTimings[wordIndex] || { delay: currentDelay };
return {
...cue,
delay: timing.delay
};
});
return {
wordTimings,
cueTimings,
totalDuration: Math.round(currentDelay)
};
}
createRevealDetail(sentence = {}, spread = null, phase = 'activate') {
+1 -1
View File
@@ -13,7 +13,7 @@ class SentenceQueueModule extends BaseModule {
super('sentence-queue', 'Sentence Queue');
// Dependencies
this.dependencies = ['text-buffer', 'tts-factory', 'paragraph-layout', 'audio-manager', 'persistence-manager'];
this.dependencies = ['text-buffer', 'tts-factory', 'paragraph-layout', 'audio-manager', 'persistence-manager', 'book-playback-timeline'];
// Queue state
this.sentenceQueue = [];
+31
View File
@@ -24,6 +24,7 @@ class TextProcessorModule extends BaseModule {
'hyphenate',
'setLocale',
'loadHyphenopolyLoader',
'ensureHyphenopolySeedElements',
'normalizeHyphenationLocale',
'applyLocaleTypography',
'getTypographyLocale',
@@ -162,6 +163,7 @@ class TextProcessorModule extends BaseModule {
this.hyphenatorReady = false;
await this.loadHyphenopolyLoader();
this.ensureHyphenopolySeedElements(locale);
window.Hyphenopoly.config({
require: {
@@ -203,6 +205,35 @@ class TextProcessorModule extends BaseModule {
}
}
ensureHyphenopolySeedElements(locale = 'en-us') {
const normalizedLocale = this.normalizeHyphenationLocale(locale);
let container = document.getElementById('hyphenopoly_seed_elements');
if (!container) {
container = document.createElement('div');
container.id = 'hyphenopoly_seed_elements';
container.setAttribute('aria-hidden', 'true');
Object.assign(container.style, {
position: 'absolute',
width: '1px',
height: '1px',
overflow: 'hidden',
opacity: '0',
pointerEvents: 'none',
left: '-9999px',
top: '-9999px'
});
document.body.appendChild(container);
}
container.innerHTML = '';
['hyphenate', 'hyphenatePipe'].forEach((className) => {
const seed = document.createElement('span');
seed.className = className;
seed.lang = normalizedLocale;
seed.textContent = normalizedLocale.startsWith('de') ? 'Silbentrennung' : 'hyphenation';
container.appendChild(seed);
});
}
loadHyphenopolyLoader() {
return new Promise((resolve, reject) => {
if (window.Hyphenopoly && typeof window.Hyphenopoly.config === 'function') {
+1
View File
@@ -213,6 +213,7 @@ const checks = [
['webgl visible spread state ignores future prepared publishes before flip', /spreadUpdate:deferred-future-unrendered/.test(source) && /incomingSpreadIndex > Math\.max\(0, Number\(bookPaginationState\.spreadIndex/.test(source) && /this\.drawSpread\(this\.currentSpread, \['left', 'right'\], \{ phase: 'prepare' \}\)/.test(textureRendererSource)],
['3D overflow reveal preloads target spread before forced page flip', /createRevealDetail\(sentence, previewSpread, 'prepare'\)/.test(bookPlaybackTimelineSource) && /this\.textureRenderer\.prepareRevealBlock\(revealDetail, \{[\s\S]*phase: 'prepare'[\s\S]*publishEvent: false/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /this\.assertSegmentReady\(segment, 'prepare'\)/.test(bookPlaybackTimelineSource) && /reason: 'timeline-preplay-spread-transition'/.test(bookPlaybackTimelineSource)],
['book playback timeline is loaded through module infrastructure', /book-playback-timeline-module\.js/.test(loaderSource) && /super\('book-playback-timeline'/.test(bookPlaybackTimelineSource) && /reportProgress\(100, 'Book playback timeline ready'\)/.test(bookPlaybackTimelineSource)],
['book playback timeline initializes before sentence queue without a dependency cycle', /this\.dependencies = \[[^\]]*'book-playback-timeline'[^\]]*\]/.test(sentenceQueueSource) && !/this\.dependencies = \[[^\]]*'sentence-queue'[^\]]*\]/.test(bookPlaybackTimelineSource) && /calculateAnimationTiming\(words = \[\]/.test(bookPlaybackTimelineSource)],
['3D display playback is owned by book playback timeline', /book-playback-timeline/.test(uiDisplayHandlerSource) && /playWebGLBookSentence/.test(uiDisplayHandlerSource) && /timeline\.playSentence\(sentence\)/.test(uiDisplayHandlerSource) && /if \(useWebGLBookReveal\) \{[\s\S]*await this\.playWebGLBookSentence\(sentence\);[\s\S]*this\.markBlockRendered\(sentence\.blockId\);[\s\S]*return null;/.test(uiDisplayHandlerSource)],
['sentence queue lookahead prepares 3D book timeline segments', /book-playback-timeline/.test(sentenceQueueSource) && /bookPlaybackTimeline\.prepareSentence\(sentence/.test(sentenceQueueSource) && /timelineSegment: segment/.test(sentenceQueueSource)],
['book playback timeline prewarms texture window before prepared playback and flips', /prewarmSegmentTextures/.test(bookPlaybackTimelineSource) && /pageCache\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /await this\.pageCache\?\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource)],