Gate WebGL book texture fonts

This commit is contained in:
2026-06-07 14:35:00 +02:00
parent 9434950826
commit 74ddd1de1c
7 changed files with 74 additions and 31 deletions
+1 -1
View File
@@ -280,6 +280,6 @@
console.log(message);
};
</script>
<script type="module" src="/js/loader.js?v=20260607-webgl-queued-mask-reveal"></script>
<script type="module" src="/js/loader.js?v=20260607-webgl-forced-font-mask"></script>
</body>
</html>
+1 -1
View File
@@ -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;
+26 -15
View File
@@ -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();
+1 -1
View File
@@ -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;
/**
+6
View File
@@ -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);
+35 -12
View File
@@ -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 = []) {
+4 -1
View File
@@ -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);