diff --git a/public/index.html b/public/index.html index 1f4af9a..5ac2f6d 100644 --- a/public/index.html +++ b/public/index.html @@ -280,6 +280,6 @@ console.log(message); }; - + diff --git a/public/js/book-page-format-module.js b/public/js/book-page-format-module.js index 9b5ffd9..86be605 100644 --- a/public/js/book-page-format-module.js +++ b/public/js/book-page-format-module.js @@ -3,7 +3,7 @@ * Defines the canonical page geometry used by the WebGL book renderer. */ import { BaseModule } from './base-module.js'; -import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-queued-mask-reveal'; +import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-forced-font-mask'; export const BOOK_TEXTURE_WIDTH = 3072; diff --git a/public/js/book-texture-renderer-module.js b/public/js/book-texture-renderer-module.js index 9388d90..46631b5 100644 --- a/public/js/book-texture-renderer-module.js +++ b/public/js/book-texture-renderer-module.js @@ -37,8 +37,9 @@ class BookTextureRendererModule extends BaseModule { this.bindMethods([ 'initialize', + 'waitForTextureFonts', + 'ensureTextureFontFace', 'createPageCanvases', - 'drawEmptySpread', 'drawSpread', 'drawPageBase', 'drawPageLines', @@ -60,8 +61,7 @@ class BookTextureRendererModule extends BaseModule { 'publishSpread', 'getPageCanvas', 'getHitMap', - 'handlePageCountChanged', - 'handleSceneReady' + 'handlePageCountChanged' ]); } @@ -70,12 +70,10 @@ class BookTextureRendererModule extends BaseModule { this.pagination = this.getModule('book-pagination'); this.localization = this.getModule('localization'); this.reportProgress(10, 'Waiting for book fonts'); - if (document.fonts?.ready) await document.fonts.ready; + await this.waitForTextureFonts(); this.reportProgress(20, 'Preparing page texture canvases'); this.createPageCanvases(); - this.drawEmptySpread(); this.addEventListener(document, 'webgl-book:page-count-changed', this.handlePageCountChanged); - this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady); this.addEventListener(document, 'book-pagination:spread-updated', (event) => { const spread = event.detail?.spread || this.pagination?.getCurrentSpread?.(); const latestBlockId = event.detail?.latestBlockId; @@ -106,6 +104,28 @@ class BookTextureRendererModule extends BaseModule { return true; } + async waitForTextureFonts() { + if (!document.fonts) return; + await Promise.all([ + this.ensureTextureFontFace('EB Garamond', '/fonts/EBGaramond12-Regular.otf'), + this.ensureTextureFontFace('EB Garamond 12', '/fonts/EBGaramond12/webfonts/EBGaramond-Regular.woff2'), + this.ensureTextureFontFace('EB Garamond Initials', '/fonts/EB-Garamond-Initials/EBGaramond-0.016/otf/EBGaramond-Initials.otf') + ]); + await Promise.all([ + document.fonts.load('24px "EB Garamond"'), + document.fonts.load('24px "EB Garamond 12"'), + document.fonts.load('72px "EB Garamond Initials"') + ]); + await document.fonts.ready; + } + + async ensureTextureFontFace(family, url) { + if (!window.FontFace) return; + const face = new FontFace(family, `url(${url})`); + const loadedFace = await face.load(); + document.fonts.add(loadedFace); + } + createPageCanvases(textureWidth = this.pageFormat?.getTextureWidth?.() || 3072) { this.metrics = this.pageFormat.getTextureMetrics(textureWidth); ['left', 'right'].forEach((side) => { @@ -117,12 +137,6 @@ class BookTextureRendererModule extends BaseModule { }); } - drawEmptySpread() { - this.drawPageBase('left'); - this.drawPageBase('right'); - this.publishSpread(); - } - drawSpread(spread = null, sides = null) { this.currentSpread = spread || { left: [], right: [] }; const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right']; @@ -542,9 +556,6 @@ class BookTextureRendererModule extends BaseModule { this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.()); } - handleSceneReady() { - this.publishSpread(); - } } const bookTextureRenderer = new BookTextureRendererModule(); diff --git a/public/js/loader.js b/public/js/loader.js index 78b4fd1..57e6dfa 100644 --- a/public/js/loader.js +++ b/public/js/loader.js @@ -24,7 +24,7 @@ const ModuleState = { ERROR: 'ERROR' }; -const MODULE_CACHE_BUSTER = '20260607-webgl-queued-mask-reveal'; +const MODULE_CACHE_BUSTER = '20260607-webgl-forced-font-mask'; window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER; /** diff --git a/public/js/ui-display-handler-module.js b/public/js/ui-display-handler-module.js index f09f4db..5f184c3 100644 --- a/public/js/ui-display-handler-module.js +++ b/public/js/ui-display-handler-module.js @@ -1047,6 +1047,12 @@ class UIDisplayHandlerModule extends BaseModule { const bookPagination = this.getModule('book-pagination'); const bookTextureRenderer = this.getModule('book-texture-renderer'); if (!bookPagination || !bookTextureRenderer || sentence.blockId == null) return; + const sentenceQueue = this.getModule('sentence-queue'); + if (!Array.isArray(sentence.animation?.wordTimings) || sentence.animation.wordTimings.length === 0) { + const words = String(sentence.layoutText || sentence.text || '').match(/\S+/g) || []; + sentence.animation = sentenceQueue?.calculateAnimationTiming?.(words, sentence.tts?.duration || 0, sentence.cueMarkers || []) + || { wordTimings: [], cueTimings: [], totalDuration: 0 }; + } if (typeof bookPagination.preparePendingBlock === 'function') { await bookPagination.preparePendingBlock(sentence); diff --git a/public/js/webgl-book-lab.js b/public/js/webgl-book-lab.js index b92e340..2284162 100644 --- a/public/js/webgl-book-lab.js +++ b/public/js/webgl-book-lab.js @@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js'; import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js'; import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js'; -import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-queued-mask-reveal'; +import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-forced-font-mask'; const canvas = document.getElementById('scene'); canvas.style.cursor = 'grab'; @@ -184,7 +184,7 @@ let pendingPageFlips = 0; const paperColor = new THREE.Color(0xece4ca); const inkColor = '#1a1009'; -const maxRevealWords = 128; +const maxRevealWords = 256; const completedRevealElapsedMs = 1000000000; await reportLabStep(48, 'Preparing high-resolution page textures'); @@ -570,6 +570,7 @@ function configureBookShadowReceiver(material, strength) { shader.uniforms.bookRevealPaperColor = { value: paperColor.clone() }; shader.uniforms.bookRevealSoftness = { value: 0.035 }; material.userData.bookRevealShader = shader; + applyPendingPageReveal(pageReveal.side, shader); } shader.vertexShader = shader.vertexShader @@ -607,14 +608,14 @@ function configureBookShadowReceiver(material, strength) { ${pageReveal ? `uniform float bookRevealActive; uniform float bookRevealElapsedMs; uniform int bookRevealWordCount; - uniform vec4 bookRevealWordRects[128]; - uniform vec4 bookRevealWordTimings[128]; + uniform vec4 bookRevealWordRects[256]; + uniform vec4 bookRevealWordTimings[256]; uniform vec3 bookRevealPaperColor; uniform float bookRevealSoftness; float bookRevealVisibleMask(vec2 uv) { float hidden = 0.0; - for (int i = 0; i < 128; i++) { + for (int i = 0; i < 256; i++) { if (i >= bookRevealWordCount) break; vec4 rect = bookRevealWordRects[i]; vec2 local = (uv - rect.xy) / max(rect.zw, vec2(0.0001)); @@ -1652,22 +1653,44 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) { const canvas = side === 'left' ? leftCanvas : rightCanvas; const texture = side === 'left' ? leftTexture : rightTexture; const shader = getPageRevealShader(side); - if (!shader?.uniforms) { - uploadPageTextureDirect(side, sourceCanvas); - return; - } drawCanvasPageTexture(canvas, sourceCanvas, side); texture.needsUpdate = true; - applyPageRevealWords(shader, revealDetail.wordRects || []); - shader.uniforms.bookRevealActive.value = 1; - shader.uniforms.bookRevealElapsedMs.value = 0; pageRevealState[side] = { startedAt: revealDetail.startNow ? performance.now() : null, durationMs: Math.max(1, Number(revealDetail.durationMs || 1)), blockIds: Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : [] }; + const material = side === 'left' ? materials.leftPage : materials.rightPage; + if (material?.userData) material.userData.pendingPageReveal = revealDetail; + if (shader?.uniforms) applyPendingPageReveal(side, shader); + else if (material) material.needsUpdate = true; + document.documentElement.dataset.webglRevealDebug = JSON.stringify({ + side, + blockIds: pageRevealState[side].blockIds, + wordCount: Array.isArray(revealDetail.wordRects) ? revealDetail.wordRects.length : 0, + shaderReady: Boolean(shader?.uniforms), + started: pageRevealState[side].startedAt != null + }); +} + +function applyPendingPageReveal(side, shader = getPageRevealShader(side)) { + const material = side === 'left' ? materials.leftPage : materials.rightPage; + const revealDetail = material?.userData?.pendingPageReveal; + if (!revealDetail || !shader?.uniforms) return false; + applyPageRevealWords(shader, revealDetail.wordRects || []); + shader.uniforms.bookRevealActive.value = 1; + shader.uniforms.bookRevealElapsedMs.value = 0; + document.documentElement.dataset.webglRevealDebug = JSON.stringify({ + side, + blockIds: pageRevealState[side]?.blockIds || revealDetail.blockIds || [], + wordCount: Array.isArray(revealDetail.wordRects) ? revealDetail.wordRects.length : 0, + shaderReady: true, + started: pageRevealState[side]?.startedAt != null + }); + delete material.userData.pendingPageReveal; + return true; } function applyPageRevealWords(shader, words = []) { diff --git a/scripts/check-webgl-book-lab.js b/scripts/check-webgl-book-lab.js index f1adcdd..52869ed 100644 --- a/scripts/check-webgl-book-lab.js +++ b/scripts/check-webgl-book-lab.js @@ -43,7 +43,10 @@ const checks = [ ['pagination can build a pending unrendered 3D block', /preparePendingBlock/.test(bookPaginationSource) && /book-pagination:prepare-block/.test(bookPaginationSource)], ['texture renderer has separate prepare and start reveal phases', /prepareRevealBlock/.test(textureRendererSource) && /startPreparedRevealAnimation/.test(textureRendererSource) && /webgl-book:page-reveal-start/.test(textureRendererSource)], ['texture renderer publishes per-word reveal coordinates', /revealWords/.test(textureRendererSource) && /wordRects/.test(textureRendererSource) && /blockWordStart/.test(textureRendererSource)], - ['page reveal shader uses coordinate mask instead of comparing page textures', /bookRevealWordRects/.test(source) && /bookRevealWordTimings/.test(source) && /bookRevealElapsedMs/.test(source) && !/texture2D\(bookRevealMap/.test(source)] + ['page reveal shader uses coordinate mask instead of comparing page textures', /bookRevealWordRects/.test(source) && /bookRevealWordTimings/.test(source) && /bookRevealElapsedMs/.test(source) && !/texture2D\(bookRevealMap/.test(source)], + ['texture renderer explicitly gates initial font before painting', /waitForTextureFonts/.test(textureRendererSource) && /ensureTextureFontFace/.test(textureRendererSource) && /FontFace\(family/.test(textureRendererSource) && /document\.fonts\.load\('72px "EB Garamond Initials"'\)/.test(textureRendererSource)], + ['texture renderer no longer republishes stale scene-ready textures', !/addEventListener\(document, 'webgl-book:scene-ready'/.test(textureRendererSource) && !/handleSceneReady\(\)\s*{\s*this\.publishSpread\(\)/.test(textureRendererSource) && !/drawEmptySpread/.test(textureRendererSource)], + ['prepared reveal never falls back to unmasked direct upload before shader compile', /pendingPageReveal/.test(source) && /applyPendingPageReveal/.test(source) && !/if \(!shader\?\.uniforms\) {\s*uploadPageTextureDirect\(side, sourceCanvas\)/.test(source)] ]; const failures = checks.filter(([, passed]) => !passed).map(([name]) => name);