0e4d9e89d7
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>