Stabilize WebGL title and timeline texture flow

This commit is contained in:
2026-06-17 08:31:46 +02:00
parent ef358c5cfd
commit c19ebe3089
5 changed files with 211 additions and 76 deletions
+58 -35
View File
@@ -7,10 +7,11 @@ import { BaseModule } from './base-module.js';
class BookTextureRendererModule extends BaseModule {
constructor() {
super('book-texture-renderer', 'Book Texture Renderer');
this.dependencies = ['book-page-format', 'book-pagination', 'localization', 'webgl-page-cache'];
this.dependencies = ['book-page-format', 'book-pagination', 'localization', 'game-config', 'webgl-page-cache'];
this.pageFormat = null;
this.pagination = null;
this.localization = null;
this.gameConfig = null;
this.pageCache = null;
this.metrics = null;
this.canvases = {
@@ -103,6 +104,7 @@ class BookTextureRendererModule extends BaseModule {
this.pageFormat = this.getModule('book-page-format');
this.pagination = this.getModule('book-pagination');
this.localization = this.getModule('localization');
this.gameConfig = this.getModule('game-config');
this.pageCache = this.getModule('webgl-page-cache');
window.BookTextureRendererDebug = {
pipelineTimings: this.pipelineTimings
@@ -119,6 +121,7 @@ class BookTextureRendererModule extends BaseModule {
const latestRenderedBlockId = Math.max(0, Number(event.detail?.latestRenderedBlockId || 0));
const visibility = event.detail?.visibility || 'current';
this.currentSpread = spread || { left: [], right: [] };
const timelineOwnsPlayback = window.BookPlaybackTimeline?.ownsPageFlipCommit === true;
if (document.documentElement.dataset.webglPageFlipActive === 'true' && this.activeAnimations.size === 0) {
this.markPipelineTiming('spreadUpdate:skip-during-flip', {
spreadIndex,
@@ -129,8 +132,19 @@ class BookTextureRendererModule extends BaseModule {
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
this.markPendingReveal(latestBlockId);
const id = String(latestBlockId);
if (timelineOwnsPlayback && visibility !== 'future-ready') {
this.markPipelineTiming('spreadUpdate:skip-timeline-owned-reveal', {
spreadIndex,
latestBlockId: id,
visibility
});
return;
}
if (visibility === 'future-ready' && !this.activeAnimations.has(id)) {
this.drawSpread(this.stripUnrenderedLines(this.currentSpread, latestRenderedBlockId), ['left', 'right']);
this.drawSpread(this.stripUnrenderedLines(this.currentSpread, latestRenderedBlockId), ['left', 'right'], {
phase: 'prepare',
publishEvent: !timelineOwnsPlayback
});
return;
}
if (this.activeAnimations.has(id)) {
@@ -145,6 +159,15 @@ class BookTextureRendererModule extends BaseModule {
}
return;
}
if (timelineOwnsPlayback && visibility !== 'future-ready' && latestBlockId) {
this.markPipelineTiming('spreadUpdate:skip-timeline-owned-commit', {
spreadIndex,
latestBlockId,
latestRenderedBlockId,
visibility
});
return;
}
this.drawSpread(this.currentSpread);
});
this.addEventListener(document, 'book-texture:fast-forward', this.fastForwardAnimations);
@@ -230,7 +253,7 @@ class BookTextureRendererModule extends BaseModule {
const hasReveal = this.revealPublishBlockIds && this.revealPublishBlockIds.size > 0;
const phase = this.getDrawPhase(options);
const drawSignature = this.getDrawSignature(this.currentSpread, sidesToDraw);
if (phase !== 'prepare' && !hasReveal && drawSignature === this.lastDrawSignature) {
if (options.force !== true && phase !== 'prepare' && !hasReveal && drawSignature === this.lastDrawSignature) {
const now = performance.now();
if (now - this.lastDrawSkipLoggedAt > 1000) {
this.lastDrawSkipLoggedAt = now;
@@ -326,11 +349,17 @@ class BookTextureRendererModule extends BaseModule {
const ctx = this.contexts[side];
if (!ctx || !this.metrics) return;
const content = this.getPageContent(side);
const titleText = document.getElementById('game_title')?.textContent?.trim() || '';
const authorText = document.getElementById('game_author')?.textContent?.trim() || '';
const subtitleText = document.getElementById('game_subtitle')?.textContent?.trim() || '';
const metadata = this.gameConfig?.getMetadata?.() || {};
const titleText = document.getElementById('game_title')?.textContent?.trim() || metadata.title || '';
const authorText = document.getElementById('game_author')?.textContent?.trim()
|| (metadata.author ? this.localization?.t?.('title.byAuthor', { author: metadata.author }) : '')
|| '';
const subtitleText = document.getElementById('game_subtitle')?.textContent?.trim() || metadata.subtitle || '';
const ornamentText = document.querySelector('#start_prompt .separator, #start_prompt .ornament, #start_prompt [class*="separator"]')?.textContent?.trim() || '';
const legalText = document.getElementById('game_legal_text')?.textContent?.trim() || '';
const legalText = document.getElementById('game_legal_text')?.textContent?.trim() || [
metadata.version ? this.localization?.t?.('title.version', { version: metadata.version }) : '',
metadata.copyright || ''
].filter(Boolean).join(' | ');
const centerX = content.x + content.width * 0.5;
const font = this.metrics.typography.fontFamily;
@@ -658,7 +687,8 @@ class BookTextureRendererModule extends BaseModule {
blockId: region.blockId,
lineIndex: region.lineIndex,
rect: region.rect,
timing: region.timing
timing: region.timing,
timingArea: region.timingArea || region.area || 0
})),
bounds: {
x: bounds.x / this.metrics.width,
@@ -707,7 +737,7 @@ class BookTextureRendererModule extends BaseModule {
}
assignRevealTiming(blockRegions = [], animation = {}) {
const totalDuration = Math.max(
const requestedTotalDuration = Math.max(
Number(animation.totalDuration || 0),
...((Array.isArray(animation.wordTimings) ? animation.wordTimings : []).map(timing => Number(timing.delay || 0) + Number(timing.duration || 0)))
);
@@ -723,32 +753,22 @@ class BookTextureRendererModule extends BaseModule {
const textRegions = sortedRegions.filter(region => !(region.fixedDurationMs > 0));
const fixedRegions = sortedRegions.filter(region => region.fixedDurationMs > 0);
let fallbackDelay = 0;
const totalArea = textRegions.reduce((sum, region) => sum + Math.max(1, region.area), 0);
const wordTimings = Array.isArray(animation.wordTimings) ? animation.wordTimings : [];
const canUseLineWordSpans = wordTimings.length > 0
&& textRegions.every(region => Number.isFinite(Number(region.blockWordStart)) && Number(region.blockWordCount) > 0);
if (canUseLineWordSpans) {
textRegions.forEach((region) => {
const timing = this.getLineTimingFromWords(region, wordTimings);
timedRegions.push({
...region,
timing
});
fallbackDelay = Math.max(fallbackDelay, timing.delay + timing.duration);
const totalArea = textRegions.reduce((sum, region) => sum + Math.max(1, region.timingArea || region.area), 0);
const lineHeight = Math.max(1, Number(this.metrics?.typographyLineHeightPx || 1));
const estimatedTextWidth = totalArea / lineHeight;
const totalDuration = requestedTotalDuration > 1
? requestedTotalDuration
: Math.max(800, estimatedTextWidth * 16);
textRegions.forEach((region) => {
const duration = totalArea > 0
? Math.max(1, totalDuration * (Math.max(1, region.timingArea || region.area) / totalArea))
: Math.max(1, totalDuration / Math.max(1, textRegions.length));
timedRegions.push({
...region,
timing: { delay: fallbackDelay, duration }
});
} else {
textRegions.forEach((region) => {
const duration = totalArea > 0
? Math.max(1, totalDuration * (Math.max(1, region.area) / totalArea))
: Math.max(1, totalDuration / Math.max(1, textRegions.length));
timedRegions.push({
...region,
timing: { delay: fallbackDelay, duration }
});
fallbackDelay += duration;
});
}
fallbackDelay += duration;
});
fixedRegions.forEach((region) => {
timedRegions.push({
@@ -813,6 +833,8 @@ class BookTextureRendererModule extends BaseModule {
const bottom = Math.min(this.metrics.height, y + height + padding);
const rectWidth = Math.max(1, right - left);
const rectHeight = Math.max(1, bottom - top);
const timingWidth = Math.max(1, Number(lineRecord.timingWidthPx || width || rectWidth));
const timingHeight = Math.max(1, Number(lineRecord.timingHeightPx || height || rectHeight));
return {
side,
spreadIndex: Math.max(0, Number((spreadIndex ?? Math.floor(Number(lineRecord.pageIndex || 0) / 2)) || 0)),
@@ -822,6 +844,7 @@ class BookTextureRendererModule extends BaseModule {
blockWordCount: Number(lineRecord.lineWordCount ?? 0),
fixedDurationMs,
area: rectWidth * rectHeight,
timingArea: timingWidth * timingHeight,
pixelRect: { x: left, y: top, right, bottom },
rect: {
x: left / this.metrics.width,
@@ -975,7 +998,7 @@ class BookTextureRendererModule extends BaseModule {
phase,
publishEvent: options.publishEvent !== false
});
if (phase !== 'prepare') this.preloadAdditionalRevealSpreads(id, spread);
this.preloadAdditionalRevealSpreads(id, spread);
if (phase === 'prepare' && published) {
this.pageCache?.rememberPreparedRevealPlan?.(id, {
...published,