A spanning block stalled ~2.1s before it began playing: activate ran prepareRevealBlock
with forceRebuild, synchronously redrawing the start spread (and preloading the
continuation spread) on the main thread, because the lookahead plan had been built with
right-only timing before pagination committed the overflow.
Build the start-spread plan spanning-aware during lookahead instead: when the preview
layout shows the block overflows, derive its timing across both preview spreads (via the
revealSpreadSourceOverride) and cache it. activate then reuses that plan — the same fast
cached-plan path non-spanning blocks already use — with no synchronous redraw. forceRebuild
is kept only as a fallback when a block spans but was not prepared spanning-aware (e.g. an
immediate prepare with no preview layout), and an evicted plan still rebuilds correctly
because pagination is committed by then.
Verified live: the spanning block's pre-playback gap dropped from ~2088ms to 139ms (equal
to non-spanning blocks), while the right line still reveals over its area share (~3.3s),
the continuation still animates from the start, and there are no fast-forwards or problems.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
A paragraph that overflows the right page onto the next spread revealed its single
right-page line over the entire TTS, then timed out (timeline-reveal-commit-timeout)
and only flipped after the whole narration. Two root causes:
- At activate the reused lookahead segment played a sentence instance whose animation
word-timings were lost (wordTimings=[], totalDuration=0), so reveal timing fell back
to an area estimate spanning the full TTS. Snapshot the timings at prepare and restore
them at activate.
- Reveal duration was distributed by ink area, but just-paginated continuation lines
have ~0 area, so the one right-page line received the whole duration. Distribute by
word count (reliable) with area as fallback.
Now the right page reveals only its word share (~2.7s for a 6/55-word line), commits,
and flips while TTS continues; the continuation animates on the next spread. Also
rewrote the right-reveal wait to a single timer + commit/fast-forward listeners with
cleanup, removing the stray timeline-reveal-commit-timeout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Starting a new game reuses block ids (1,2,3...). The reveal clock's per-block
start times (activeRevealBlockStarts in the lab) and the renderer's animation/
revealed sets are keyed by block id and were never cleared on a client reset, so
a new game over already-cached content inherited the previous run's start times.
beginPageReveal then computed a huge elapsed and the shader treated the reveal as
already complete — showing everything at once instead of animating.
resetClientPlaybackAndDisplay (run on new game and restore) now emits
story:client-reset; the lab clears activeRevealBlockStarts/pending reveal state,
the texture renderer clears active animations and revealed-block ids, and the
timeline invalidates prepared segments. So each game starts with a clean reveal
clock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Establish book-playback-timeline as the sole playback owner driving the
scene through formal webgl-book:* events (not the BookLabDebug surface),
with a single reveal clock in the scene render loop and webgl-page-cache as
the only texture cache. Remove the legacy dual playback path and the
ownsPageFlipCommit gating.
Fixes:
- Flip page detached/folded at the spine: restore the raw page-cap line for
flip geometry (matches the prototype/pre-regression), removing
normalizeFlipLineToVisiblePage which moved the pivot off the spine arc.
- Flip textures: distance-based UVs (no horizontal compression),
direction-aware face material (source on the camera-facing side), source
meta derived from the visible spread (manual flips), prewarm shape fix.
- Reveal: flash removed on the static page and the flip back surface;
spanning blocks rebuild the reveal plan at activate and continue the
reveal on the next spread after the fill flip.
- Cache staleness is contentVersion-primary; nav clamps to spreadCount.
Docs updated to describe the intended single-owner architecture. Regression
checks updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>