diff --git a/public/js/book-texture-renderer-module.js b/public/js/book-texture-renderer-module.js index a83827b..0535700 100644 --- a/public/js/book-texture-renderer-module.js +++ b/public/js/book-texture-renderer-module.js @@ -679,13 +679,24 @@ class BookTextureRendererModule extends BaseModule { const timedRegions = []; 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.timingArea || region.area), 0); const lineHeight = Math.max(1, Number(this.metrics?.typographyLineHeightPx || 1)); const estimatedTextWidth = totalArea / lineHeight; - const totalDuration = requestedTotalDuration > 1 + const baseDuration = requestedTotalDuration > 1 ? requestedTotalDuration : Math.max(800, estimatedTextWidth * 16); + // Word-proportional scaling: these regions may cover only part of the block (the + // rest is on another spread this reveal does not include). Reveal only this portion's + // share of the block TTS, offset by the words before it, so the page reveals at + // normal pace and flips when its words are spoken — the continuation then resumes on + // the next spread instead of the page absorbing the whole TTS. When the regions cover + // the whole block (unified plan or single-page block) this is a no-op. + const totalBlockWords = Array.isArray(animation.wordTimings) ? animation.wordTimings.length : 0; + const collectedWords = textRegions.reduce((sum, region) => sum + Math.max(0, Number(region.blockWordCount || 0)), 0); + const wordsBefore = textRegions.reduce((min, region) => Math.min(min, Math.max(0, Number(region.blockWordStart || 0))), Number.POSITIVE_INFINITY); + const useWordShare = totalBlockWords > 0 && collectedWords > 0 && collectedWords < totalBlockWords; + const totalDuration = useWordShare ? baseDuration * (collectedWords / totalBlockWords) : baseDuration; + let fallbackDelay = useWordShare && Number.isFinite(wordsBefore) ? baseDuration * (wordsBefore / totalBlockWords) : 0; textRegions.forEach((region) => { const duration = totalArea > 0 ? Math.max(1, totalDuration * (Math.max(1, region.timingArea || region.area) / totalArea)) diff --git a/public/js/webgl-book-lab.js b/public/js/webgl-book-lab.js index b192b2e..7074792 100644 --- a/public/js/webgl-book-lab.js +++ b/public/js/webgl-book-lab.js @@ -3114,20 +3114,21 @@ function prepareStaticPageForFlip(flip, prewarm = null) { hasBackTexture: Boolean(backTexture || getBlankPageTexture()), sourceTextureMatchesBackTexture: sourceTexture === (backTexture || getBlankPageTexture()) }; - if (flip.direction > 0) { - const blankTexture = getBlankPageTexture(); - if (blankTexture && materials.rightPage.map !== blankTexture) { - clearPageReveal('right', 'page-flip-start', { preserveBaseTexture: sourceSide === 'right' }); - materials.rightPage.map = blankTexture; - materials.rightPage.needsUpdate = true; - } - } else if (flip.direction < 0) { - const blankTexture = getBlankPageTexture(); - if (blankTexture && materials.leftPage.map !== blankTexture) { - clearPageReveal('left', 'page-flip-start', { preserveBaseTexture: sourceSide === 'left' }); - materials.leftPage.map = blankTexture; - materials.leftPage.needsUpdate = true; - } + // The page lifts from the source side, uncovering the target spread's same-side page + // beneath it. Show that target page now (hidden under the lifting page at t=0, then + // revealed as it turns away) instead of a blank that pops in at the end. If that side + // has a pending reveal (playback), keep it blank so activate lands the masked reveal. + const revealedSide = sourceSide; + const revealedMaterial = revealedSide === 'left' ? materials.leftPage : materials.rightPage; + const revealedDeferred = Array.isArray(flip.deferRevealSides) && flip.deferRevealSides.includes(revealedSide); + const revealedMeta = getPaginationPageMeta(targetPages[revealedSide]) || makeBlankPageMeta(targetPages[revealedSide]); + const revealedTexture = (revealedDeferred || revealedMeta.kind === 'blank') + ? getBlankPageTexture() + : (pageTextureStore?.getResidentTextureForMeta?.(revealedMeta) || getBlankPageTexture()); + clearPageReveal(revealedSide, 'page-flip-start', { preserveBaseTexture: true }); + if (revealedTexture && revealedMaterial.map !== revealedTexture) { + revealedMaterial.map = revealedTexture; + revealedMaterial.needsUpdate = true; } markPageTextureTiming('flipTexturePreflight:ready', { ...lastFlipTexturePreflight,