From e72594b3ff7d74c8c19635ec816c0af1164db5a3 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Thu, 18 Jun 2026 23:45:26 +0200 Subject: [PATCH] Revert per-line reveal timing to area-weighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit changed per-line reveal-duration distribution from ink-area to word-count. That dropped a deliberate precision decision (area gives sub-line granularity) and, verified live on a spanning paragraph, it was what made the continuation page fail to animate. Restore area-weighting for the per-line split. The word-share scaling of the *total* duration for partial (spanning) blocks and the timeline-module timing snapshot/restore are kept — they only preserve existing word-timings, they do not change the area-based per-line distribution. Verified: on a real spanning block the right line reveals over its area share (~3.3s), the page flips, and the continuation animates progressively across the next spread over the full TTS (no fast-forward, no reveal-all-at-once). Co-Authored-By: Claude Opus 4.8 --- public/js/book-texture-renderer-module.js | 13 ++----------- scripts/check-webgl-book-lab.js | 4 ++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/public/js/book-texture-renderer-module.js b/public/js/book-texture-renderer-module.js index 517d825..44b2a90 100644 --- a/public/js/book-texture-renderer-module.js +++ b/public/js/book-texture-renderer-module.js @@ -698,18 +698,9 @@ class BookTextureRendererModule extends BaseModule { 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; - // Weight each line by its word count when available, falling back to ink area. - // Word counts are reliable even for just-paginated continuation lines whose rect - // area can be ~0; area-weighting there would hand the whole duration to the one - // line on the current page and stretch it across the entire TTS. - const useWordWeights = collectedWords > 0; - const totalWeight = useWordWeights ? collectedWords : totalArea; textRegions.forEach((region) => { - const weight = useWordWeights - ? Math.max(0, Number(region.blockWordCount || 0)) - : Math.max(1, region.timingArea || region.area); - const duration = totalWeight > 0 - ? Math.max(1, totalDuration * (weight / totalWeight)) + const duration = totalArea > 0 + ? Math.max(1, totalDuration * (Math.max(1, region.timingArea || region.area) / totalArea)) : Math.max(1, totalDuration / Math.max(1, textRegions.length)); timedRegions.push({ ...region, diff --git a/scripts/check-webgl-book-lab.js b/scripts/check-webgl-book-lab.js index a7e85c5..391f2b2 100644 --- a/scripts/check-webgl-book-lab.js +++ b/scripts/check-webgl-book-lab.js @@ -165,7 +165,7 @@ const checks = [ ['webgl page text textures avoid mipmap generation', /function configurePageCanvasTexture/.test(source) && /texture\.minFilter = THREE\.LinearFilter/.test(source) && /texture\.generateMipmaps = false/.test(source)], ['webgl reveal shader masks against a base-page texture instead of flat color blocks', /bookRevealBaseMap/.test(source) && /bookRevealUseBaseMap/.test(source) && /revealBaseColor/.test(source) && /baseCanvas/.test(textureRendererSource)], ['webgl reveal shader masks antialiased ink and uses smooth line-dominant scan', /smoothstep\(0\.52, 0\.9, luminance\)/.test(source) && /local\.x \* 0\.96/.test(source) && /bookRevealSoftness = \{ value: 0\.025 \}/.test(source)], - ['webgl reveal line timings use global word-weighted timing across split-page spreads', /assignRevealTiming/.test(textureRendererSource) && /sourceSpreads/.test(textureRendererSource) && /this\.pagination\?\.spreads/.test(textureRendererSource) && /spreadIndex/.test(textureRendererSource) && /totalDuration \* \(weight \/ totalWeight\)/.test(textureRendererSource) && /durationMs: sideRegions\.reduce/.test(textureRendererSource)], + ['webgl reveal line timings use global area-weighted timing across split-page spreads', /assignRevealTiming/.test(textureRendererSource) && /sourceSpreads/.test(textureRendererSource) && /this\.pagination\?\.spreads/.test(textureRendererSource) && /spreadIndex/.test(textureRendererSource) && /totalDuration \* \(Math\.max\(1, region\.timingArea \|\| region\.area\) \/ totalArea\)/.test(textureRendererSource) && /durationMs: sideRegions\.reduce/.test(textureRendererSource)], ['webgl page format reduces only outer margins from previous value', /outerBaseIn: 0\.27/.test(pageFormatSource) && /outerThicknessFactor: 0\.015/.test(pageFormatSource) && /outerMaxIn: 0\.315/.test(pageFormatSource) && /innerBaseIn: 0\.42/.test(pageFormatSource)], ['webgl mode enlarges and inverts DOM overlay text without touching 2D mode', /body\.webgl-mode \{[\s\S]*font-size: 18px;/.test(styleSource) && /body\.webgl-mode \.choice-list \.choice-button/.test(styleSource) && /rgba\(246, 231, 201/.test(styleSource)], ['webgl choice overlay hides title clutter and prevents horizontal scrollbar', /body\.webgl-mode #page_left #game_title/.test(styleSource) && /body\.webgl-mode #page_left #start_prompt/.test(styleSource) && /overflow-x: hidden/.test(styleSource) && /book\.style\.width = 'min\(44rem/.test(webglSceneSource)], @@ -213,7 +213,7 @@ const checks = [ ['game loop persists webgl book state in save slots', /webglBookState: this\.getWebGLBookState\(\)/.test(fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'game-loop-module.js'), 'utf8')) && /applyWebGLBookState\(browserSave\.webglBookState\)/.test(fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'game-loop-module.js'), 'utf8'))], ['webgl right-page reveal flips are owned by the timeline, not the scene', !/pendingRightPageFlip/.test(source) && !/handleRevealCommittedForPageFlip/.test(source) && /waitForVisualCompletion/.test(bookPlaybackTimelineSource) && /reason: 'timeline-right-page-filled'/.test(bookPlaybackTimelineSource) && /requiresRightPageFlipAfterReveal/.test(bookPlaybackTimelineSource) && /isChoiceAwaitingPlayer/.test(bookPlaybackTimelineSource)], ['webgl reveal clock follows absolute playback time and continues across page flips', /activeRevealBlockStarts/.test(source) && /state\.visualElapsedMs = Math\.max\(0, now - state\.startedAt\)/.test(source) && !/Math\.min\(revealFrameDeltaMs, targetFrameDurationMs\)/.test(source) && /prewarmFlipTextures\(1, targetSpread\)/.test(source)], - ['webgl line reveal timing is word-weighted (reliable for 0-area continuation lines) with area fallback', /lineWordCount/.test(bookPaginationSource) && /blockWordStart/.test(textureRendererSource) && /blockWordCount/.test(textureRendererSource) && /timingArea/.test(textureRendererSource) && /const useWordWeights = collectedWords > 0/.test(textureRendererSource) && /totalDuration \* \(weight \/ totalWeight\)/.test(textureRendererSource) && !/const canUseLineWordSpans/.test(textureRendererSource)], + ['webgl line reveal timing scales total by word-share for partial blocks and splits per-line by area', /lineWordCount/.test(bookPaginationSource) && /blockWordStart/.test(textureRendererSource) && /blockWordCount/.test(textureRendererSource) && /timingArea/.test(textureRendererSource) && /const useWordShare = totalBlockWords > 0 && collectedWords > 0 && collectedWords < totalBlockWords/.test(textureRendererSource) && /totalDuration \* \(Math\.max\(1, region\.timingArea \|\| region\.area\) \/ totalArea\)/.test(textureRendererSource) && !/const canUseLineWordSpans/.test(textureRendererSource)], ['webgl flip completion defers book rebuild out of the final animation frame', /scheduledBookRebuildFrame/.test(source) && /function scheduleBookRebuild/.test(source) && /syncReadingProgressToCurrentPage\(\{[\s\S]*rebuild: 'defer'[\s\S]*reason: 'page-flip-finished'/.test(source)], ['webgl ordinary flip near-end uses resident target textures and defers revealing sides', /applyResidentSpreadTextures\(targetSpread, 'page-flip-near-end', \{ skipSides: flip\.deferRevealSides \}\)/.test(source) && /function applyResidentSpreadTextures\(spreadIndex, reason = 'resident-spread', options = \{\}\)/.test(source) && /const skipSides = Array\.isArray\(options\.skipSides\)/.test(source) && /residentSpreadTextures:applied/.test(source) && /spreadUpdate:state-only/.test(source)], ['webgl autoplay flip source prefers currently revealing visible material over resident cache', /if \(revealStateMatchesPage\(side, pageMeta\)\) return material\?\.map \|\| null/.test(source) && /revealStateMatchesPage\(sourceSide, sourcePageMeta\) \? sourceSide : null/.test(source)],