Fix WebGL timeline startup ordering
This commit is contained in:
@@ -8,12 +8,11 @@ import { BaseModule } from './base-module.js';
|
|||||||
class BookPlaybackTimelineModule extends BaseModule {
|
class BookPlaybackTimelineModule extends BaseModule {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('book-playback-timeline', 'Book Playback Timeline');
|
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.pagination = null;
|
||||||
this.textureRenderer = null;
|
this.textureRenderer = null;
|
||||||
this.pageCache = null;
|
this.pageCache = null;
|
||||||
this.playbackCoordinator = null;
|
this.playbackCoordinator = null;
|
||||||
this.sentenceQueue = null;
|
|
||||||
this.activeSegment = null;
|
this.activeSegment = null;
|
||||||
this.preparedSegments = new Map();
|
this.preparedSegments = new Map();
|
||||||
this.timelineDiagnostics = [];
|
this.timelineDiagnostics = [];
|
||||||
@@ -26,6 +25,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
'prepareSentence',
|
'prepareSentence',
|
||||||
'activatePreparedSegment',
|
'activatePreparedSegment',
|
||||||
'ensureAnimationTimings',
|
'ensureAnimationTimings',
|
||||||
|
'calculateAnimationTiming',
|
||||||
'createPreparedSegment',
|
'createPreparedSegment',
|
||||||
'createRevealDetail',
|
'createRevealDetail',
|
||||||
'applyTexturePlan',
|
'applyTexturePlan',
|
||||||
@@ -55,7 +55,6 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
this.textureRenderer = this.getModule('book-texture-renderer');
|
this.textureRenderer = this.getModule('book-texture-renderer');
|
||||||
this.pageCache = this.getModule('webgl-page-cache');
|
this.pageCache = this.getModule('webgl-page-cache');
|
||||||
this.playbackCoordinator = this.getModule('playback-coordinator');
|
this.playbackCoordinator = this.getModule('playback-coordinator');
|
||||||
this.sentenceQueue = this.getModule('sentence-queue');
|
|
||||||
this.addEventListener(document, 'webgl-book:page-reveal-start', (event) => {
|
this.addEventListener(document, 'webgl-book:page-reveal-start', (event) => {
|
||||||
this.markBenchmark('reveal-start', {
|
this.markBenchmark('reveal-start', {
|
||||||
blockId: event.detail?.blockId ?? null
|
blockId: event.detail?.blockId ?? null
|
||||||
@@ -210,8 +209,50 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
ensureAnimationTimings(sentence = {}) {
|
ensureAnimationTimings(sentence = {}) {
|
||||||
if (Array.isArray(sentence.animation?.wordTimings) && sentence.animation.wordTimings.length > 0) return;
|
if (Array.isArray(sentence.animation?.wordTimings) && sentence.animation.wordTimings.length > 0) return;
|
||||||
const words = String(sentence.layoutText || sentence.text || '').match(/\S+/g) || [];
|
const words = String(sentence.layoutText || sentence.text || '').match(/\S+/g) || [];
|
||||||
sentence.animation = this.sentenceQueue?.calculateAnimationTiming?.(words, sentence.tts?.duration || 0, sentence.cueMarkers || [])
|
sentence.animation = this.calculateAnimationTiming(words, sentence.tts?.duration || 0, sentence.cueMarkers || []);
|
||||||
|| { wordTimings: [], cueTimings: [], totalDuration: 0 };
|
}
|
||||||
|
|
||||||
|
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') {
|
createRevealDetail(sentence = {}, spread = null, phase = 'activate') {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class SentenceQueueModule extends BaseModule {
|
|||||||
super('sentence-queue', 'Sentence Queue');
|
super('sentence-queue', 'Sentence Queue');
|
||||||
|
|
||||||
// Dependencies
|
// 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
|
// Queue state
|
||||||
this.sentenceQueue = [];
|
this.sentenceQueue = [];
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class TextProcessorModule extends BaseModule {
|
|||||||
'hyphenate',
|
'hyphenate',
|
||||||
'setLocale',
|
'setLocale',
|
||||||
'loadHyphenopolyLoader',
|
'loadHyphenopolyLoader',
|
||||||
|
'ensureHyphenopolySeedElements',
|
||||||
'normalizeHyphenationLocale',
|
'normalizeHyphenationLocale',
|
||||||
'applyLocaleTypography',
|
'applyLocaleTypography',
|
||||||
'getTypographyLocale',
|
'getTypographyLocale',
|
||||||
@@ -162,6 +163,7 @@ class TextProcessorModule extends BaseModule {
|
|||||||
this.hyphenatorReady = false;
|
this.hyphenatorReady = false;
|
||||||
|
|
||||||
await this.loadHyphenopolyLoader();
|
await this.loadHyphenopolyLoader();
|
||||||
|
this.ensureHyphenopolySeedElements(locale);
|
||||||
|
|
||||||
window.Hyphenopoly.config({
|
window.Hyphenopoly.config({
|
||||||
require: {
|
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() {
|
loadHyphenopolyLoader() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (window.Hyphenopoly && typeof window.Hyphenopoly.config === 'function') {
|
if (window.Hyphenopoly && typeof window.Hyphenopoly.config === 'function') {
|
||||||
|
|||||||
@@ -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)],
|
['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)],
|
['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 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)],
|
['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)],
|
['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)],
|
['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)],
|
||||||
|
|||||||
Reference in New Issue
Block a user