Prepare spanning-block continuation spread in background (kill post-flip redraw)

For a paragraph that overflows onto the next spread, the continuation page was redrawn
synchronously after the flip (drawSpread on the main thread), so the next page stayed
blank for ~2.7s and then the carried-over lines popped in already ~24% revealed instead
of animating from the start.

Move that work off the critical path: during lookahead, prepare and cache the
continuation spread's reveal plan using the not-yet-committed preview spreads (so per-line
timing is computed across both spreads), then reuse it after the flip instead of redrawing.

- pagination: expose the preview spread layout on the returned preview spread so the owner
  can detect the continuation spread (race-free; each call owns its preparedSpreads).
- renderer: revealSpreadSourceOverride lets region collection use preview spreads during
  lookahead; prepareContinuationRevealPlan draws+caches the continuation plan (publishEvent
  off); takeContinuationRevealPlan reuses it, re-stamped as an activate-phase publish.
- timeline: prepare the continuation plan during background (non-immediate) prepares;
  revealContinuationSpread reuses it, falling back to the redraw when none was prepared.

Verified live on a spanning block: continuation now appears ~0.25s after the flip (was
~2.7s) at ve~3471 = the right line's duration, i.e. it animates from the start (no pop-in),
runs to ~full over the TTS, no fast-forward, no continuation-missing problems.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 00:04:21 +02:00
parent e72594b3ff
commit 47af10d60c
4 changed files with 117 additions and 7 deletions
+9 -2
View File
@@ -233,17 +233,24 @@ class BookPaginationModule extends BaseModule {
if (targetSpread) this.currentSpreadIndex = targetSpread.index;
}
if (options.publish !== false) this.publish({ reason: options.activate === false ? 'prepare-preload' : 'prepare-activate' });
const resultSpread = targetSpread || (options.activate === false ? preparedSpreads[0] : this.getCurrentSpread());
// Expose the full preview layout on the returned (preview) spread so the playback owner
// can detect a spanning block and prepare its continuation spread in the background,
// off the critical path. Each call returns its own preparedSpreads, so this is race-free.
if (options.activate === false && resultSpread) {
resultSpread.previewSpreads = preparedSpreads;
}
document.dispatchEvent(new CustomEvent('book-pagination:block-prepared', {
detail: {
blockId: pendingBlockId,
spread: targetSpread || (options.activate === false ? preparedSpreads[0] : this.getCurrentSpread()),
spread: resultSpread,
spreadIndex: targetSpread?.index ?? this.currentSpreadIndex,
latestBlockId: pendingBlockId,
latestRenderedBlockId,
phase: options.activate === false ? 'prepare' : 'activate'
}
}));
return targetSpread || (options.activate === false ? preparedSpreads[0] : this.getCurrentSpread());
return resultSpread;
}
getPreparedBlockCacheKey(gameId, blockId, historyEndBlockId, latestRenderedBlockId, options = {}) {