Stabilize WebGL reveal timing
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
* Defines the canonical page geometry used by the WebGL book renderer.
|
* Defines the canonical page geometry used by the WebGL book renderer.
|
||||||
*/
|
*/
|
||||||
import { BaseModule } from './base-module.js';
|
import { BaseModule } from './base-module.js';
|
||||||
import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-forced-font-mask';
|
import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-reveal-clock';
|
||||||
|
|
||||||
export const BOOK_TEXTURE_WIDTH = 3072;
|
export const BOOK_TEXTURE_WIDTH = 3072;
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
this.lastAnimationFrameAt = 0;
|
this.lastAnimationFrameAt = 0;
|
||||||
this.targetFrameDurationMs = 1000 / 30;
|
this.targetFrameDurationMs = 1000 / 30;
|
||||||
|
this.pipelineTimings = [];
|
||||||
|
|
||||||
this.bindMethods([
|
this.bindMethods([
|
||||||
'initialize',
|
'initialize',
|
||||||
|
'markPipelineTiming',
|
||||||
'waitForTextureFonts',
|
'waitForTextureFonts',
|
||||||
'ensureTextureFontFace',
|
'ensureTextureFontFace',
|
||||||
'createPageCanvases',
|
'createPageCanvases',
|
||||||
@@ -69,6 +71,9 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.pageFormat = this.getModule('book-page-format');
|
this.pageFormat = this.getModule('book-page-format');
|
||||||
this.pagination = this.getModule('book-pagination');
|
this.pagination = this.getModule('book-pagination');
|
||||||
this.localization = this.getModule('localization');
|
this.localization = this.getModule('localization');
|
||||||
|
window.BookTextureRendererDebug = {
|
||||||
|
pipelineTimings: this.pipelineTimings
|
||||||
|
};
|
||||||
this.reportProgress(10, 'Waiting for book fonts');
|
this.reportProgress(10, 'Waiting for book fonts');
|
||||||
await this.waitForTextureFonts();
|
await this.waitForTextureFonts();
|
||||||
this.reportProgress(20, 'Preparing page texture canvases');
|
this.reportProgress(20, 'Preparing page texture canvases');
|
||||||
@@ -104,6 +109,18 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markPipelineTiming(name, detail = {}) {
|
||||||
|
const entry = {
|
||||||
|
name,
|
||||||
|
at: performance.now(),
|
||||||
|
detail
|
||||||
|
};
|
||||||
|
this.pipelineTimings.push(entry);
|
||||||
|
if (this.pipelineTimings.length > 120) this.pipelineTimings.splice(0, this.pipelineTimings.length - 120);
|
||||||
|
document.documentElement.dataset.webglTexturePipeline = JSON.stringify(this.pipelineTimings);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
async waitForTextureFonts() {
|
async waitForTextureFonts() {
|
||||||
if (!document.fonts) return;
|
if (!document.fonts) return;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -140,6 +157,10 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
drawSpread(spread = null, sides = null) {
|
drawSpread(spread = null, sides = null) {
|
||||||
this.currentSpread = spread || { left: [], right: [] };
|
this.currentSpread = spread || { left: [], right: [] };
|
||||||
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||||
|
this.markPipelineTiming('drawSpread:start', {
|
||||||
|
sides: sidesToDraw,
|
||||||
|
revealBlockIds: this.revealPublishBlockIds ? Array.from(this.revealPublishBlockIds) : []
|
||||||
|
});
|
||||||
this.revealBounds = { left: null, right: null };
|
this.revealBounds = { left: null, right: null };
|
||||||
this.revealWords = { left: [], right: [] };
|
this.revealWords = { left: [], right: [] };
|
||||||
sidesToDraw.forEach((side) => {
|
sidesToDraw.forEach((side) => {
|
||||||
@@ -148,6 +169,9 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.drawPageLines(side, this.currentSpread?.[side] || []);
|
this.drawPageLines(side, this.currentSpread?.[side] || []);
|
||||||
});
|
});
|
||||||
this.publishSpread(sidesToDraw);
|
this.publishSpread(sidesToDraw);
|
||||||
|
this.markPipelineTiming('drawSpread:end', {
|
||||||
|
sides: sidesToDraw
|
||||||
|
});
|
||||||
this.revealBounds = null;
|
this.revealBounds = null;
|
||||||
this.revealWords = null;
|
this.revealWords = null;
|
||||||
this.revealPublishBlockIds = null;
|
this.revealPublishBlockIds = null;
|
||||||
@@ -376,6 +400,10 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
if (blockId == null || !Array.isArray(detail.wordTimings)) return;
|
if (blockId == null || !Array.isArray(detail.wordTimings)) return;
|
||||||
const id = String(blockId);
|
const id = String(blockId);
|
||||||
const wordTimings = detail.wordTimings;
|
const wordTimings = detail.wordTimings;
|
||||||
|
this.markPipelineTiming('prepareRevealBlock:start', {
|
||||||
|
blockId: id,
|
||||||
|
wordTimingCount: wordTimings.length
|
||||||
|
});
|
||||||
this.activeAnimations.set(id, {
|
this.activeAnimations.set(id, {
|
||||||
blockId,
|
blockId,
|
||||||
wordTimings,
|
wordTimings,
|
||||||
@@ -390,12 +418,20 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.pendingRevealBlockIds.delete(id);
|
this.pendingRevealBlockIds.delete(id);
|
||||||
this.revealPublishBlockIds = new Set([id]);
|
this.revealPublishBlockIds = new Set([id]);
|
||||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
|
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
|
||||||
|
this.markPipelineTiming('prepareRevealBlock:end', {
|
||||||
|
blockId: id,
|
||||||
|
wordTimingCount: wordTimings.length
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
startPreparedRevealAnimation(blockId) {
|
startPreparedRevealAnimation(blockId) {
|
||||||
const id = String(blockId ?? '');
|
const id = String(blockId ?? '');
|
||||||
const animation = this.activeAnimations.get(id);
|
const animation = this.activeAnimations.get(id);
|
||||||
if (!animation) return false;
|
if (!animation) return false;
|
||||||
|
this.markPipelineTiming('startPreparedRevealAnimation', {
|
||||||
|
blockId: id,
|
||||||
|
wordTimingCount: Array.isArray(animation.wordTimings) ? animation.wordTimings.length : 0
|
||||||
|
});
|
||||||
animation.startedAt = performance.now();
|
animation.startedAt = performance.now();
|
||||||
animation.prepared = false;
|
animation.prepared = false;
|
||||||
animation.completed = false;
|
animation.completed = false;
|
||||||
@@ -503,6 +539,10 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
|
|
||||||
publishSpread(sides = null) {
|
publishSpread(sides = null) {
|
||||||
const sidesToPublish = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
const sidesToPublish = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||||
|
const wordCounts = {
|
||||||
|
left: this.revealWords?.left?.length || 0,
|
||||||
|
right: this.revealWords?.right?.length || 0
|
||||||
|
};
|
||||||
const detail = {
|
const detail = {
|
||||||
metrics: this.metrics,
|
metrics: this.metrics,
|
||||||
hitMaps: this.hitMaps
|
hitMaps: this.hitMaps
|
||||||
@@ -537,6 +577,11 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (Object.keys(reveal).length) detail.reveal = reveal;
|
if (Object.keys(reveal).length) detail.reveal = reveal;
|
||||||
|
this.markPipelineTiming('publishSpread', {
|
||||||
|
sides: sidesToPublish,
|
||||||
|
hasReveal: Object.keys(reveal).length > 0,
|
||||||
|
wordCounts
|
||||||
|
});
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
||||||
detail
|
detail
|
||||||
}));
|
}));
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260607-webgl-forced-font-mask';
|
const MODULE_CACHE_BUSTER = '20260607-webgl-reveal-clock';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
|||||||
super('ui-display-handler', 'UI Display Handler');
|
super('ui-display-handler', 'UI Display Handler');
|
||||||
|
|
||||||
// Module dependencies
|
// Module dependencies
|
||||||
this.dependencies = ['layout-renderer', 'webgl-book-scene', 'playback-coordinator', 'game-config', 'localization', 'story-history', 'sentence-queue', 'persistence-manager', 'markup-parser'];
|
this.dependencies = ['layout-renderer', 'webgl-book-scene', 'playback-coordinator', 'game-config', 'localization', 'story-history', 'sentence-queue', 'persistence-manager', 'markup-parser', 'book-pagination', 'book-texture-renderer'];
|
||||||
|
|
||||||
// DOM elements
|
// DOM elements
|
||||||
this.container = null;
|
this.container = null;
|
||||||
|
|||||||
+109
-9
@@ -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 { 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 { 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 { 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-forced-font-mask';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-reveal-clock';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@@ -161,6 +161,26 @@ let fpsDisplay = null;
|
|||||||
let fpsWindowStartedAt = performance.now();
|
let fpsWindowStartedAt = performance.now();
|
||||||
let fpsWindowFrames = 0;
|
let fpsWindowFrames = 0;
|
||||||
const lastFrameTiming = {};
|
const lastFrameTiming = {};
|
||||||
|
const loaderTimings = {};
|
||||||
|
const pageTextureTimings = [];
|
||||||
|
|
||||||
|
function markLoaderTiming(name) {
|
||||||
|
loaderTimings[name] = performance.now();
|
||||||
|
document.documentElement.dataset.webglLoaderTimings = JSON.stringify(loaderTimings);
|
||||||
|
}
|
||||||
|
|
||||||
|
function markPageTextureTiming(name, detail = {}) {
|
||||||
|
const entry = {
|
||||||
|
name,
|
||||||
|
at: performance.now(),
|
||||||
|
detail
|
||||||
|
};
|
||||||
|
pageTextureTimings.push(entry);
|
||||||
|
if (pageTextureTimings.length > 120) pageTextureTimings.splice(0, pageTextureTimings.length - 120);
|
||||||
|
document.documentElement.dataset.webglPageTextureTimings = JSON.stringify(pageTextureTimings);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
const book = new THREE.Group();
|
const book = new THREE.Group();
|
||||||
scene.add(book);
|
scene.add(book);
|
||||||
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0.28'), 0, 1);
|
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0.28'), 0, 1);
|
||||||
@@ -203,6 +223,7 @@ const pageRevealState = {
|
|||||||
left: null,
|
left: null,
|
||||||
right: null
|
right: null
|
||||||
};
|
};
|
||||||
|
const pageRevealClearLog = [];
|
||||||
await reportLabStep(52, 'Generating leather texture set');
|
await reportLabStep(52, 'Generating leather texture set');
|
||||||
const leatherTextures = createLeatherTextures();
|
const leatherTextures = createLeatherTextures();
|
||||||
await reportLabStep(56, 'Generating spine cloth texture set');
|
await reportLabStep(56, 'Generating spine cloth texture set');
|
||||||
@@ -389,6 +410,9 @@ window.BookLabDebug = {
|
|||||||
textures: generatedTextureCanvases,
|
textures: generatedTextureCanvases,
|
||||||
ready: false,
|
ready: false,
|
||||||
renderedFrames: 0,
|
renderedFrames: 0,
|
||||||
|
loaderTimings,
|
||||||
|
pageTextureTimings,
|
||||||
|
pageRevealClearLog,
|
||||||
get sceneAoPass() {
|
get sceneAoPass() {
|
||||||
return sceneAoPass;
|
return sceneAoPass;
|
||||||
},
|
},
|
||||||
@@ -444,6 +468,9 @@ window.BookLabDebug = {
|
|||||||
window.BookTextureRenderer?.publishSpread?.();
|
window.BookTextureRenderer?.publishSpread?.();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
getRevealDebugState() {
|
||||||
|
return getRevealDebugState();
|
||||||
|
},
|
||||||
getTextureInfo() {
|
getTextureInfo() {
|
||||||
return {
|
return {
|
||||||
pageTextureWidth,
|
pageTextureWidth,
|
||||||
@@ -1619,6 +1646,11 @@ function syncBookControls() {
|
|||||||
|
|
||||||
function handlePageCanvases(event) {
|
function handlePageCanvases(event) {
|
||||||
const detail = event.detail || {};
|
const detail = event.detail || {};
|
||||||
|
markPageTextureTiming('handlePageCanvases:start', {
|
||||||
|
hasLeft: Boolean(detail.left),
|
||||||
|
hasRight: Boolean(detail.right),
|
||||||
|
revealSides: Object.keys(detail.reveal || {})
|
||||||
|
});
|
||||||
if (detail.left) {
|
if (detail.left) {
|
||||||
if (detail.reveal?.left) {
|
if (detail.reveal?.left) {
|
||||||
beginPageReveal('left', detail.left, detail.reveal.left);
|
beginPageReveal('left', detail.left, detail.reveal.left);
|
||||||
@@ -1639,14 +1671,17 @@ function handlePageCanvases(event) {
|
|||||||
height: leftCanvas.height,
|
height: leftCanvas.height,
|
||||||
source: 'book-texture-renderer'
|
source: 'book-texture-renderer'
|
||||||
});
|
});
|
||||||
|
markPageTextureTiming('handlePageCanvases:end');
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadPageTextureDirect(side, sourceCanvas) {
|
function uploadPageTextureDirect(side, sourceCanvas) {
|
||||||
const canvas = side === 'left' ? leftCanvas : rightCanvas;
|
const canvas = side === 'left' ? leftCanvas : rightCanvas;
|
||||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||||
clearPageReveal(side);
|
markPageTextureTiming('directUpload:start', { side });
|
||||||
|
clearPageReveal(side, 'direct-upload');
|
||||||
drawCanvasPageTexture(canvas, sourceCanvas, side);
|
drawCanvasPageTexture(canvas, sourceCanvas, side);
|
||||||
texture.needsUpdate = true;
|
texture.needsUpdate = true;
|
||||||
|
markPageTextureTiming('directUpload:end', { side });
|
||||||
}
|
}
|
||||||
|
|
||||||
function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||||
@@ -1654,11 +1689,18 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||||
const shader = getPageRevealShader(side);
|
const shader = getPageRevealShader(side);
|
||||||
|
|
||||||
|
markPageTextureTiming('revealUpload:start', {
|
||||||
|
side,
|
||||||
|
wordCount: Array.isArray(revealDetail.wordRects) ? revealDetail.wordRects.length : 0
|
||||||
|
});
|
||||||
drawCanvasPageTexture(canvas, sourceCanvas, side);
|
drawCanvasPageTexture(canvas, sourceCanvas, side);
|
||||||
texture.needsUpdate = true;
|
texture.needsUpdate = true;
|
||||||
|
|
||||||
pageRevealState[side] = {
|
pageRevealState[side] = {
|
||||||
startedAt: revealDetail.startNow ? performance.now() : null,
|
startedAt: revealDetail.startNow ? performance.now() : null,
|
||||||
|
pendingStart: false,
|
||||||
|
lastRevealFrameAt: null,
|
||||||
|
visualElapsedMs: 0,
|
||||||
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||||
blockIds: Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : []
|
blockIds: Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : []
|
||||||
};
|
};
|
||||||
@@ -1673,6 +1715,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
shaderReady: Boolean(shader?.uniforms),
|
shaderReady: Boolean(shader?.uniforms),
|
||||||
started: pageRevealState[side].startedAt != null
|
started: pageRevealState[side].startedAt != null
|
||||||
});
|
});
|
||||||
|
markPageTextureTiming('revealUpload:end', { side });
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPendingPageReveal(side, shader = getPageRevealShader(side)) {
|
function applyPendingPageReveal(side, shader = getPageRevealShader(side)) {
|
||||||
@@ -1729,7 +1772,39 @@ function getPageRevealShader(side) {
|
|||||||
return material?.userData?.bookRevealShader || null;
|
return material?.userData?.bookRevealShader || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPageReveal(side) {
|
function getRevealDebugState() {
|
||||||
|
return ['left', 'right'].reduce((state, side) => {
|
||||||
|
const shader = getPageRevealShader(side);
|
||||||
|
const uniforms = shader?.uniforms || {};
|
||||||
|
state[side] = {
|
||||||
|
active: Number(uniforms.bookRevealActive?.value || 0),
|
||||||
|
elapsedMs: Number(uniforms.bookRevealElapsedMs?.value || 0),
|
||||||
|
visualElapsedMs: Number(pageRevealState[side]?.visualElapsedMs || 0),
|
||||||
|
wordCount: Number(uniforms.bookRevealWordCount?.value || 0),
|
||||||
|
started: pageRevealState[side]?.startedAt != null,
|
||||||
|
pendingStart: pageRevealState[side]?.pendingStart === true,
|
||||||
|
durationMs: Number(pageRevealState[side]?.durationMs || 0),
|
||||||
|
blockIds: pageRevealState[side]?.blockIds || []
|
||||||
|
};
|
||||||
|
return state;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPageReveal(side, reason = 'clear') {
|
||||||
|
pageRevealClearLog.push({
|
||||||
|
side,
|
||||||
|
reason,
|
||||||
|
at: performance.now(),
|
||||||
|
state: pageRevealState[side] ? {
|
||||||
|
started: pageRevealState[side].startedAt != null,
|
||||||
|
pendingStart: pageRevealState[side].pendingStart === true,
|
||||||
|
visualElapsedMs: pageRevealState[side].visualElapsedMs || 0,
|
||||||
|
durationMs: pageRevealState[side].durationMs,
|
||||||
|
blockIds: pageRevealState[side].blockIds || []
|
||||||
|
} : null
|
||||||
|
});
|
||||||
|
if (pageRevealClearLog.length > 40) pageRevealClearLog.splice(0, pageRevealClearLog.length - 40);
|
||||||
|
document.documentElement.dataset.webglRevealClearLog = JSON.stringify(pageRevealClearLog);
|
||||||
pageRevealState[side] = null;
|
pageRevealState[side] = null;
|
||||||
const shader = getPageRevealShader(side);
|
const shader = getPageRevealShader(side);
|
||||||
if (shader?.uniforms?.bookRevealActive) {
|
if (shader?.uniforms?.bookRevealActive) {
|
||||||
@@ -1745,7 +1820,7 @@ function startPageRevealForBlock(blockId) {
|
|||||||
const state = pageRevealState[side];
|
const state = pageRevealState[side];
|
||||||
if (!state || state.startedAt != null) return;
|
if (!state || state.startedAt != null) return;
|
||||||
if (!state.blockIds.map(value => String(value)).includes(id)) return;
|
if (!state.blockIds.map(value => String(value)).includes(id)) return;
|
||||||
state.startedAt = performance.now();
|
state.pendingStart = true;
|
||||||
const shader = getPageRevealShader(side);
|
const shader = getPageRevealShader(side);
|
||||||
if (shader?.uniforms?.bookRevealElapsedMs) shader.uniforms.bookRevealElapsedMs.value = 0;
|
if (shader?.uniforms?.bookRevealElapsedMs) shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||||
});
|
});
|
||||||
@@ -1758,7 +1833,7 @@ function fastForwardPageReveals(blockIds = []) {
|
|||||||
if (!state) return;
|
if (!state) return;
|
||||||
const matches = ids.size === 0 || state.blockIds.some(blockId => ids.has(String(blockId)));
|
const matches = ids.size === 0 || state.blockIds.some(blockId => ids.has(String(blockId)));
|
||||||
if (!matches) return;
|
if (!matches) return;
|
||||||
clearPageReveal(side);
|
clearPageReveal(side, 'fast-forward');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1768,18 +1843,29 @@ function updatePageRevealAnimations(now) {
|
|||||||
if (!state) return;
|
if (!state) return;
|
||||||
const shader = getPageRevealShader(side);
|
const shader = getPageRevealShader(side);
|
||||||
if (!shader?.uniforms) {
|
if (!shader?.uniforms) {
|
||||||
clearPageReveal(side);
|
clearPageReveal(side, 'missing-shader');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.pendingStart) {
|
||||||
|
state.startedAt = now;
|
||||||
|
state.pendingStart = false;
|
||||||
|
state.lastRevealFrameAt = now;
|
||||||
|
state.visualElapsedMs = 0;
|
||||||
|
shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.startedAt == null) {
|
if (state.startedAt == null) {
|
||||||
shader.uniforms.bookRevealElapsedMs.value = 0;
|
shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const progress = THREE.MathUtils.clamp((now - state.startedAt) / state.durationMs, 0, 1);
|
const revealFrameDeltaMs = state.lastRevealFrameAt == null ? 0 : Math.max(0, now - state.lastRevealFrameAt);
|
||||||
shader.uniforms.bookRevealElapsedMs.value = Math.max(0, now - state.startedAt);
|
state.lastRevealFrameAt = now;
|
||||||
|
state.visualElapsedMs = Math.max(0, Number(state.visualElapsedMs || 0)) + Math.min(revealFrameDeltaMs, targetFrameDurationMs);
|
||||||
|
const progress = THREE.MathUtils.clamp(state.visualElapsedMs / state.durationMs, 0, 1);
|
||||||
|
shader.uniforms.bookRevealElapsedMs.value = state.visualElapsedMs;
|
||||||
if (progress < 1) return;
|
if (progress < 1) return;
|
||||||
|
|
||||||
clearPageReveal(side);
|
clearPageReveal(side, 'duration-complete');
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:reveal-committed', {
|
document.dispatchEvent(new CustomEvent('webgl-book:reveal-committed', {
|
||||||
detail: {
|
detail: {
|
||||||
side,
|
side,
|
||||||
@@ -1790,6 +1876,11 @@ function updatePageRevealAnimations(now) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function drawCanvasPageTexture(canvas, sourceCanvas, side) {
|
function drawCanvasPageTexture(canvas, sourceCanvas, side) {
|
||||||
|
markPageTextureTiming('drawCanvasPageTexture:start', {
|
||||||
|
side,
|
||||||
|
width: canvas?.width || 0,
|
||||||
|
height: canvas?.height || 0
|
||||||
|
});
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.fillStyle = '#f2ead0';
|
ctx.fillStyle = '#f2ead0';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
@@ -1803,6 +1894,7 @@ function drawCanvasPageTexture(canvas, sourceCanvas, side) {
|
|||||||
|
|
||||||
ctx.drawImage(sourceCanvas, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(sourceCanvas, 0, 0, canvas.width, canvas.height);
|
||||||
updatePageTextureDebugState(side, canvas, sourceCanvas, true);
|
updatePageTextureDebugState(side, canvas, sourceCanvas, true);
|
||||||
|
markPageTextureTiming('drawCanvasPageTexture:end', { side });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2858,12 +2950,20 @@ function loadAiRoomReflection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function primeSceneForLoader() {
|
function primeSceneForLoader() {
|
||||||
|
markLoaderTiming('primeSceneForLoader:start');
|
||||||
updateCameraRig(0);
|
updateCameraRig(0);
|
||||||
updateCandleShadowUniforms();
|
updateCandleShadowUniforms();
|
||||||
|
markLoaderTiming('bookShadowMaps:start');
|
||||||
updateBookShadowMaps();
|
updateBookShadowMaps();
|
||||||
|
markLoaderTiming('bookShadowMaps:end');
|
||||||
|
markLoaderTiming('tableReflection:start');
|
||||||
updateTableReflection();
|
updateTableReflection();
|
||||||
|
markLoaderTiming('tableReflection:end');
|
||||||
|
markLoaderTiming('shaderCompile:start');
|
||||||
renderer.compile(scene, camera);
|
renderer.compile(scene, camera);
|
||||||
|
markLoaderTiming('shaderCompile:end');
|
||||||
staticSceneBuffersDirty = false;
|
staticSceneBuffersDirty = false;
|
||||||
|
markLoaderTiming('primeSceneForLoader:end');
|
||||||
}
|
}
|
||||||
|
|
||||||
function tintAmbientFromCanvas(canvas) {
|
function tintAmbientFromCanvas(canvas) {
|
||||||
|
|||||||
@@ -58,12 +58,6 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
|
|
||||||
this.addEventListener(document, 'preference-updated', this.handlePreferenceUpdated);
|
this.addEventListener(document, 'preference-updated', this.handlePreferenceUpdated);
|
||||||
this.addEventListener(document, 'localization:languageChanged', this.updateLocalizedText);
|
this.addEventListener(document, 'localization:languageChanged', this.updateLocalizedText);
|
||||||
this.addEventListener(document, 'story:turn-start', this.triggerTextureRefresh);
|
|
||||||
this.addEventListener(document, 'story:turn-complete', this.triggerTextureRefresh);
|
|
||||||
this.addEventListener(document, 'story:history-updated', this.triggerTextureRefresh);
|
|
||||||
this.addEventListener(document, 'story:process-state', this.handleProcessState);
|
|
||||||
this.addEventListener(document, 'input', this.triggerTextureRefresh, true);
|
|
||||||
this.addEventListener(document, 'change', this.triggerTextureRefresh, true);
|
|
||||||
|
|
||||||
if (this.mode !== '3d') {
|
if (this.mode !== '3d') {
|
||||||
this.reportProgress(100, '2D book UI selected');
|
this.reportProgress(100, '2D book UI selected');
|
||||||
@@ -303,7 +297,6 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
this.reportProgress(96, 'Binding WebGL page controls');
|
this.reportProgress(96, 'Binding WebGL page controls');
|
||||||
this.installTextureEventBridge();
|
this.installTextureEventBridge();
|
||||||
this.triggerTextureRefresh();
|
|
||||||
return this.labImportPromise;
|
return this.labImportPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,12 +461,10 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
if (this.mode === '3d') {
|
if (this.mode === '3d') {
|
||||||
this.createLabHost();
|
this.createLabHost();
|
||||||
this.installPreferenceBridge();
|
this.installPreferenceBridge();
|
||||||
if (this.labImportPromise) this.triggerTextureRefresh();
|
|
||||||
}
|
}
|
||||||
const title = document.getElementById('game_title')?.textContent?.trim();
|
const title = document.getElementById('game_title')?.textContent?.trim();
|
||||||
const label = document.getElementById('lab_title');
|
const label = document.getElementById('lab_title');
|
||||||
if (title && label) label.textContent = title;
|
if (title && label) label.textContent = title;
|
||||||
this.triggerTextureRefresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshModalOverview() {
|
refreshModalOverview() {
|
||||||
|
|||||||
@@ -13,6 +13,48 @@ const uiDisplayHandlerPath = path.join(__dirname, '..', 'public', 'js', 'ui-disp
|
|||||||
const uiDisplayHandlerSource = fs.readFileSync(uiDisplayHandlerPath, 'utf8');
|
const uiDisplayHandlerSource = fs.readFileSync(uiDisplayHandlerPath, 'utf8');
|
||||||
const bookPaginationPath = path.join(__dirname, '..', 'public', 'js', 'book-pagination-module.js');
|
const bookPaginationPath = path.join(__dirname, '..', 'public', 'js', 'book-pagination-module.js');
|
||||||
const bookPaginationSource = fs.readFileSync(bookPaginationPath, 'utf8');
|
const bookPaginationSource = fs.readFileSync(bookPaginationPath, 'utf8');
|
||||||
|
const webglScenePath = path.join(__dirname, '..', 'public', 'js', 'webgl-book-scene-module.js');
|
||||||
|
const webglSceneSource = fs.readFileSync(webglScenePath, 'utf8');
|
||||||
|
const loaderPath = path.join(__dirname, '..', 'public', 'js', 'loader.js');
|
||||||
|
const loaderSource = fs.readFileSync(loaderPath, 'utf8');
|
||||||
|
|
||||||
|
function dependencyList(source, moduleId) {
|
||||||
|
const classStart = source.indexOf(`super('${moduleId}'`);
|
||||||
|
if (classStart < 0) return [];
|
||||||
|
const dependencyMatch = source.slice(classStart).match(/this\.dependencies\s*=\s*\[([^\]]*)\]/);
|
||||||
|
if (!dependencyMatch) return [];
|
||||||
|
return Array.from(dependencyMatch[1].matchAll(/'([^']+)'|"([^"]+)"/g)).map(match => match[1] || match[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function directGetModules(source) {
|
||||||
|
return Array.from(source.matchAll(/getModule\('([^']+)'\)|getModule\("([^"]+)"\)/g)).map(match => match[1] || match[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function undeclaredDirectDependencies(source, moduleId, optional = []) {
|
||||||
|
const declared = new Set(dependencyList(source, moduleId));
|
||||||
|
const allowed = new Set([moduleId, ...declared, ...optional]);
|
||||||
|
return Array.from(new Set(directGetModules(source))).filter(id => !allowed.has(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheBuster(source) {
|
||||||
|
return source.match(/MODULE_CACHE_BUSTER\s*=\s*'([^']+)'/)?.[1] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function methodBody(source, methodName) {
|
||||||
|
const start = source.indexOf(`${methodName}(`);
|
||||||
|
if (start < 0) return '';
|
||||||
|
const braceStart = source.indexOf('{', start);
|
||||||
|
if (braceStart < 0) return '';
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = braceStart; index < source.length; index += 1) {
|
||||||
|
if (source[index] === '{') depth += 1;
|
||||||
|
if (source[index] === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) return source.slice(braceStart + 1, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const checks = [
|
const checks = [
|
||||||
['scene-level SSAO import', /SSAOPass/.test(source)],
|
['scene-level SSAO import', /SSAOPass/.test(source)],
|
||||||
@@ -46,7 +88,24 @@ const checks = [
|
|||||||
['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 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)],
|
['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)]
|
['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)],
|
||||||
|
['ui display handler declares every direct module lookup', undeclaredDirectDependencies(uiDisplayHandlerSource, 'ui-display-handler').length === 0],
|
||||||
|
['webgl scene declares every direct module lookup', undeclaredDirectDependencies(webglSceneSource, 'webgl-book-scene').length === 0],
|
||||||
|
['loader cache key matches webgl procedural imports', cacheBuster(loaderSource) && source.includes(`procedural-book-model.js?v=${cacheBuster(loaderSource)}`) && proceduralBookSource.length > 0],
|
||||||
|
['webgl lab exposes loader timing diagnostics', /loaderTimings/.test(source) && /markLoaderTiming/.test(source) && /primeSceneForLoader/.test(source)],
|
||||||
|
['webgl lab records shader compile timing during loader prime', /markLoaderTiming\('shaderCompile:start'\)/.test(source) && /renderer\.compile\(scene, camera\)/.test(source) && /markLoaderTiming\('shaderCompile:end'\)/.test(source)],
|
||||||
|
['webgl lab exposes reveal uniform diagnostics', /getRevealDebugState/.test(source) && /bookRevealActive/.test(source) && /bookRevealElapsedMs/.test(source) && /bookRevealWordCount/.test(source)],
|
||||||
|
['webgl lab records page reveal clear reasons', /clearPageReveal\(side, reason/.test(source) && /webglRevealClearLog/.test(source)],
|
||||||
|
['webgl reveal clock starts on first render frame', /pendingStart/.test(source) && /state\.pendingStart/.test(source) && /state\.startedAt = now/.test(source)],
|
||||||
|
['webgl reveal visual clock caps missed-frame deltas', /visualElapsedMs/.test(source) && /revealFrameDeltaMs/.test(source) && /Math\.min\(revealFrameDeltaMs/.test(source)],
|
||||||
|
['webgl lab records page texture upload/copy timings', /pageTextureTimings/.test(source) && /markPageTextureTiming/.test(source) && /webglPageTextureTimings/.test(source)],
|
||||||
|
['webgl lab times direct and reveal texture uploads', /markPageTextureTiming\('directUpload:start'/.test(source) && /markPageTextureTiming\('revealUpload:start'/.test(source) && /markPageTextureTiming\('drawCanvasPageTexture:start'/.test(source)],
|
||||||
|
['texture renderer exposes reveal pipeline diagnostics', /pipelineTimings/.test(textureRendererSource) && /markPipelineTiming/.test(textureRendererSource) && /webglTexturePipeline/.test(textureRendererSource)],
|
||||||
|
['texture renderer records prepare draw publish and start reveal timing', /markPipelineTiming\('prepareRevealBlock:start'/.test(textureRendererSource) && /markPipelineTiming\('drawSpread:start'/.test(textureRendererSource) && /markPipelineTiming\('publishSpread'/.test(textureRendererSource) && /markPipelineTiming\('startPreparedRevealAnimation'/.test(textureRendererSource)],
|
||||||
|
['texture renderer diagnostics include reveal word counts', /wordCounts/.test(textureRendererSource) && /revealWords/.test(textureRendererSource) && /wordRects/.test(textureRendererSource)],
|
||||||
|
['webgl scene avoids duplicate initial texture publish', !/this\.triggerTextureRefresh\(\)/.test(methodBody(webglSceneSource, 'initializeScene'))],
|
||||||
|
['webgl scene does not republish 3D page textures from DOM refresh events', !/addEventListener\(document, 'story:turn-start', this\.triggerTextureRefresh\)/.test(webglSceneSource) && !/addEventListener\(document, 'story:turn-complete', this\.triggerTextureRefresh\)/.test(webglSceneSource) && !/addEventListener\(document, 'story:history-updated', this\.triggerTextureRefresh\)/.test(webglSceneSource) && !/addEventListener\(document, 'input', this\.triggerTextureRefresh/.test(webglSceneSource) && !/addEventListener\(document, 'change', this\.triggerTextureRefresh/.test(webglSceneSource)],
|
||||||
|
['webgl scene adoptPageContent does not republish 3D page textures', !/triggerTextureRefresh/.test(methodBody(webglSceneSource, 'adoptPageContent'))]
|
||||||
];
|
];
|
||||||
|
|
||||||
const failures = checks.filter(([, passed]) => !passed).map(([name]) => name);
|
const failures = checks.filter(([, passed]) => !passed).map(([name]) => name);
|
||||||
|
|||||||
Reference in New Issue
Block a user