Move page rasterization to an OffscreenCanvas worker
Page text drawing (the bulk of drawSpread cost: layout, fonts, fillText across ~25 lines x 2 pages at 3072px) ran synchronously on the main thread during prepare/lookahead, tanking FPS at load and at flips/word boundaries. New public/js/book-texture-worker.js owns rasterization off-thread: it loads the EB Garamond faces via FontFace, draws base + title + lines + page number into an OffscreenCanvas, and returns a full-page ImageBitmap plus a background-only base ImageBitmap (for the reveal mask) per side. The main thread blits those onto the existing page canvases with one drawImage, so the texture/reveal/scene pipeline downstream is unchanged. The worker also owns image loading (fetch + createImageBitmap) and a DOM-free inline-tag parser (no document in a worker); the renderer marshals the DOM-sourced title data in. drawSpread is now async and serialized through a promise chain so the shared render state (currentSpread, revealPublishBlockIds, spread override, reveal base) stays consistent across the worker round trip even with concurrent lookahead prepares; the reveal context is passed per draw rather than left on the instance. prepareRevealBlock / prepareContinuationRevealPlan / preloadAdditionalRevealSpreads and their timeline callers await accordingly. The old main-thread drawing methods are deleted (single implementation now lives in the worker). Verified live: pages render correctly via the worker (text + drop caps crisp), worker fonts load (probe returns fonts-ready + drawn), idle ~66fps, playback median ~60fps. Remaining non-rasterization main-thread costs (procedural texture generation in the loader; pagination text layout; per-frame reflection/shadow on content change) are separate follow-ups. Suite 166. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -222,12 +222,12 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
: null;
|
||||
|
||||
const revealDetail = this.createRevealDetail(sentence, previewSpread, 'prepare');
|
||||
const texturePlan = this.textureRenderer.prepareRevealBlock(
|
||||
const texturePlan = await this.textureRenderer.prepareRevealBlock(
|
||||
continuationSpread ? { ...revealDetail, previewSpreads } : revealDetail,
|
||||
{ phase: 'prepare', publishEvent: false }
|
||||
);
|
||||
if (continuationSpread) {
|
||||
this.textureRenderer.prepareContinuationRevealPlan({
|
||||
await this.textureRenderer.prepareContinuationRevealPlan({
|
||||
...revealDetail,
|
||||
previewSpreads,
|
||||
continuationSpread
|
||||
@@ -317,7 +317,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
const revealDetail = this.createRevealDetail(sentence, spread, 'activate');
|
||||
// Reuse the spanning-aware plan prepared during lookahead — its timing already spans
|
||||
// both pages. No synchronous redraw on the critical path.
|
||||
const texturePlan = this.textureRenderer.prepareRevealBlock(revealDetail, { publishEvent: false });
|
||||
const texturePlan = await this.textureRenderer.prepareRevealBlock(revealDetail, { publishEvent: false });
|
||||
segment.activeTexturePlan = texturePlan;
|
||||
this.applyTexturePlan(texturePlan, segment, 'activate');
|
||||
await this.assertSegmentReady(segment, 'activate');
|
||||
|
||||
Reference in New Issue
Block a user