Fix WebGL reveal timing and flip prewarm
This commit is contained in:
@@ -65,8 +65,10 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'buildRevealRegions',
|
'buildRevealRegions',
|
||||||
'collectRevealRegionCandidates',
|
'collectRevealRegionCandidates',
|
||||||
'createRevealRegionForLine',
|
'createRevealRegionForLine',
|
||||||
|
'assignRevealTiming',
|
||||||
'getLineInkRect',
|
'getLineInkRect',
|
||||||
'getLineNaturalWidth',
|
'getLineNaturalWidth',
|
||||||
|
'getLineWordCount',
|
||||||
'getImageRevealDurationMs',
|
'getImageRevealDurationMs',
|
||||||
'getInlineStyleState',
|
'getInlineStyleState',
|
||||||
'updateInlineStyleState',
|
'updateInlineStyleState',
|
||||||
@@ -76,6 +78,8 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'buildLineSegments',
|
'buildLineSegments',
|
||||||
'startRevealAnimation',
|
'startRevealAnimation',
|
||||||
'prepareRevealBlock',
|
'prepareRevealBlock',
|
||||||
|
'preloadAdditionalRevealSpreads',
|
||||||
|
'spreadContainsBlock',
|
||||||
'hasPreparedRevealBlock',
|
'hasPreparedRevealBlock',
|
||||||
'createAnimationState',
|
'createAnimationState',
|
||||||
'publishPreparedReveal',
|
'publishPreparedReveal',
|
||||||
@@ -118,6 +122,11 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.currentSpread = spread || { left: [], right: [] };
|
this.currentSpread = spread || { left: [], right: [] };
|
||||||
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
||||||
this.markPendingReveal(latestBlockId);
|
this.markPendingReveal(latestBlockId);
|
||||||
|
const id = String(latestBlockId);
|
||||||
|
if (this.activeAnimations.has(id)) {
|
||||||
|
this.revealPublishBlockIds = new Set([id]);
|
||||||
|
this.drawSpread(this.currentSpread, ['left', 'right']);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.drawSpread(this.currentSpread);
|
this.drawSpread(this.currentSpread);
|
||||||
@@ -591,34 +600,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
byBlock.forEach((blockRegions, blockId) => {
|
byBlock.forEach((blockRegions, blockId) => {
|
||||||
const animation = this.activeAnimations.get(blockId);
|
const animation = this.activeAnimations.get(blockId);
|
||||||
if (!animation || animation.completed) return;
|
if (!animation || animation.completed) return;
|
||||||
const fixedRegions = blockRegions.filter(region => region.fixedDurationMs > 0);
|
regions.push(...this.assignRevealTiming(blockRegions, animation));
|
||||||
const textRegions = blockRegions.filter(region => !(region.fixedDurationMs > 0));
|
|
||||||
let delay = 0;
|
|
||||||
const textDuration = Math.max(0, Number(animation.totalDuration || 0));
|
|
||||||
const totalArea = textRegions.reduce((sum, region) => sum + Math.max(1, region.area), 0);
|
|
||||||
textRegions.forEach((region) => {
|
|
||||||
const duration = totalArea > 0
|
|
||||||
? Math.max(1, textDuration * (Math.max(1, region.area) / totalArea))
|
|
||||||
: Math.max(1, textDuration / Math.max(1, textRegions.length));
|
|
||||||
regions.push({
|
|
||||||
...region,
|
|
||||||
timing: {
|
|
||||||
delay,
|
|
||||||
duration
|
|
||||||
}
|
|
||||||
});
|
|
||||||
delay += duration;
|
|
||||||
});
|
|
||||||
fixedRegions.forEach((region) => {
|
|
||||||
regions.push({
|
|
||||||
...region,
|
|
||||||
timing: {
|
|
||||||
delay,
|
|
||||||
duration: Math.max(1, region.fixedDurationMs)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
delay += Math.max(1, region.fixedDurationMs);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
const sideRegions = regions.filter(region => region.side === side);
|
const sideRegions = regions.filter(region => region.side === side);
|
||||||
if (!sideRegions.length) return null;
|
if (!sideRegions.length) return null;
|
||||||
@@ -635,7 +617,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
blockIds: Array.from(byBlock.keys()),
|
blockIds: Array.from(byBlock.keys()),
|
||||||
durationMs: regions.reduce((maxDuration, region) => Math.max(maxDuration, region.timing.delay + region.timing.duration), 0),
|
durationMs: sideRegions.reduce((maxDuration, region) => Math.max(maxDuration, region.timing.delay + region.timing.duration), 0),
|
||||||
baseCanvas: null,
|
baseCanvas: null,
|
||||||
lineRects: sideRegions.map(region => ({
|
lineRects: sideRegions.map(region => ({
|
||||||
blockId: region.blockId,
|
blockId: region.blockId,
|
||||||
@@ -664,6 +646,67 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assignRevealTiming(blockRegions = [], animation = {}) {
|
||||||
|
const wordTimings = Array.isArray(animation.wordTimings) ? animation.wordTimings : [];
|
||||||
|
const totalDuration = Math.max(
|
||||||
|
Number(animation.totalDuration || 0),
|
||||||
|
...wordTimings.map(timing => Number(timing.delay || 0) + Number(timing.duration || 0))
|
||||||
|
);
|
||||||
|
const sortedRegions = [...blockRegions].sort((a, b) => {
|
||||||
|
const aStart = Math.max(0, Number(a.wordStart || 0));
|
||||||
|
const bStart = Math.max(0, Number(b.wordStart || 0));
|
||||||
|
if (aStart !== bStart) return aStart - bStart;
|
||||||
|
const aLine = Math.max(0, Number(a.lineIndex || 0));
|
||||||
|
const bLine = Math.max(0, Number(b.lineIndex || 0));
|
||||||
|
return aLine - bLine;
|
||||||
|
});
|
||||||
|
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.area), 0);
|
||||||
|
|
||||||
|
textRegions.forEach((region) => {
|
||||||
|
const wordStart = Math.max(0, Number(region.wordStart || 0));
|
||||||
|
const wordEnd = Math.max(wordStart + 1, Number(region.wordEnd || wordStart + 1));
|
||||||
|
const firstTiming = wordTimings[wordStart] || null;
|
||||||
|
const lastTiming = wordTimings[Math.min(wordTimings.length - 1, wordEnd - 1)] || firstTiming;
|
||||||
|
let delay = firstTiming ? Math.max(0, Number(firstTiming.delay || 0)) : fallbackDelay;
|
||||||
|
let duration = lastTiming
|
||||||
|
? Math.max(1, (Number(lastTiming.delay || 0) + Number(lastTiming.duration || 0)) - delay)
|
||||||
|
: 0;
|
||||||
|
if (!Number.isFinite(duration) || duration <= 0) {
|
||||||
|
duration = totalArea > 0
|
||||||
|
? Math.max(1, totalDuration * (Math.max(1, region.area) / totalArea))
|
||||||
|
: Math.max(1, totalDuration / Math.max(1, textRegions.length));
|
||||||
|
delay = fallbackDelay;
|
||||||
|
}
|
||||||
|
timedRegions.push({
|
||||||
|
...region,
|
||||||
|
timing: { delay, duration }
|
||||||
|
});
|
||||||
|
fallbackDelay = Math.max(fallbackDelay, delay + duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
fixedRegions.forEach((region) => {
|
||||||
|
timedRegions.push({
|
||||||
|
...region,
|
||||||
|
timing: {
|
||||||
|
delay: fallbackDelay,
|
||||||
|
duration: Math.max(1, region.fixedDurationMs)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fallbackDelay += Math.max(1, region.fixedDurationMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
return timedRegions.sort((a, b) => {
|
||||||
|
const aDelay = Number(a.timing?.delay || 0);
|
||||||
|
const bDelay = Number(b.timing?.delay || 0);
|
||||||
|
if (aDelay !== bDelay) return aDelay - bDelay;
|
||||||
|
return Number(a.lineIndex || 0) - Number(b.lineIndex || 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createRevealRegionForLine(side, lineRecord = {}) {
|
createRevealRegionForLine(side, lineRecord = {}) {
|
||||||
const blockId = String(lineRecord?.blockId ?? '');
|
const blockId = String(lineRecord?.blockId ?? '');
|
||||||
if (!blockId || !this.revealPublishBlockIds.has(blockId)) return null;
|
if (!blockId || !this.revealPublishBlockIds.has(blockId)) return null;
|
||||||
@@ -680,10 +723,12 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
const rect = this.getLineInkRect(side, lineRecord);
|
const rect = this.getLineInkRect(side, lineRecord);
|
||||||
if (!rect) return null;
|
if (!rect) return null;
|
||||||
return this.normalizeRevealRegion(side, blockId, lineRecord, rect.x, rect.y, rect.width, rect.height, 0);
|
const wordStart = Math.max(0, Number(lineRecord.blockWordStart || 0));
|
||||||
|
const wordCount = Math.max(1, this.getLineWordCount(lineRecord.line || {}));
|
||||||
|
return this.normalizeRevealRegion(side, blockId, lineRecord, rect.x, rect.y, rect.width, rect.height, 0, wordStart, wordStart + wordCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeRevealRegion(side, blockId, lineRecord, x, y, width, height, fixedDurationMs = 0) {
|
normalizeRevealRegion(side, blockId, lineRecord, x, y, width, height, fixedDurationMs = 0, wordStart = 0, wordEnd = 0) {
|
||||||
const padding = Math.max(2, Number(lineRecord.fontPx || 18) * 0.12);
|
const padding = Math.max(2, Number(lineRecord.fontPx || 18) * 0.12);
|
||||||
const left = Math.max(0, x - padding);
|
const left = Math.max(0, x - padding);
|
||||||
const top = Math.max(0, y - padding);
|
const top = Math.max(0, y - padding);
|
||||||
@@ -696,6 +741,8 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
blockId,
|
blockId,
|
||||||
lineIndex: Number(lineRecord.lineIndex ?? lineRecord.pageLine ?? 0),
|
lineIndex: Number(lineRecord.lineIndex ?? lineRecord.pageLine ?? 0),
|
||||||
fixedDurationMs,
|
fixedDurationMs,
|
||||||
|
wordStart,
|
||||||
|
wordEnd,
|
||||||
area: rectWidth * rectHeight,
|
area: rectWidth * rectHeight,
|
||||||
pixelRect: { x: left, y: top, right, bottom },
|
pixelRect: { x: left, y: top, right, bottom },
|
||||||
rect: {
|
rect: {
|
||||||
@@ -743,6 +790,25 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLineWordCount(line = {}) {
|
||||||
|
const nodes = Array.isArray(line.nodes) ? line.nodes : [];
|
||||||
|
let count = 0;
|
||||||
|
let previousWasGlue = true;
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (!node) return;
|
||||||
|
if (node.type === 'glue') {
|
||||||
|
previousWasGlue = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.type === 'penalty') return;
|
||||||
|
if (node.type === 'box' && node.value) {
|
||||||
|
if (previousWasGlue) count += 1;
|
||||||
|
previousWasGlue = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
getImageRevealDurationMs(lineRecord = {}) {
|
getImageRevealDurationMs(lineRecord = {}) {
|
||||||
const metadata = lineRecord.metadata || {};
|
const metadata = lineRecord.metadata || {};
|
||||||
const explicit = Number(metadata.animationMs || metadata.revealMs || metadata.imageRevealMs || 0);
|
const explicit = Number(metadata.animationMs || metadata.revealMs || metadata.imageRevealMs || 0);
|
||||||
@@ -822,6 +888,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
const spread = detail.spread || this.currentSpread || this.pagination?.getCurrentSpread?.();
|
const spread = detail.spread || this.currentSpread || this.pagination?.getCurrentSpread?.();
|
||||||
const sides = ['left', 'right'];
|
const sides = ['left', 'right'];
|
||||||
const published = this.drawSpread(spread, sides, { preloadOnly });
|
const published = this.drawSpread(spread, sides, { preloadOnly });
|
||||||
|
if (!preloadOnly) this.preloadAdditionalRevealSpreads(id, spread);
|
||||||
if (preloadOnly && published) {
|
if (preloadOnly && published) {
|
||||||
this.preparedRevealCache.set(id, {
|
this.preparedRevealCache.set(id, {
|
||||||
...published,
|
...published,
|
||||||
@@ -837,6 +904,25 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preloadAdditionalRevealSpreads(blockId, primarySpread = null) {
|
||||||
|
const spreads = Array.isArray(this.pagination?.spreads) ? this.pagination.spreads : [];
|
||||||
|
if (!spreads.length) return;
|
||||||
|
const primaryIndex = Number(primarySpread?.index);
|
||||||
|
spreads.forEach((spread) => {
|
||||||
|
if (!spread || Number(spread.index) === primaryIndex) return;
|
||||||
|
if (!this.spreadContainsBlock(spread, blockId)) return;
|
||||||
|
this.drawSpread(spread, ['left', 'right'], { preloadOnly: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
spreadContainsBlock(spread = {}, blockId = '') {
|
||||||
|
const id = String(blockId ?? '');
|
||||||
|
return ['left', 'right'].some((side) => {
|
||||||
|
const lines = Array.isArray(spread?.[side]) ? spread[side] : [];
|
||||||
|
return lines.some(line => String(line?.blockId ?? '') === id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
hasPreparedRevealBlock(blockId) {
|
hasPreparedRevealBlock(blockId) {
|
||||||
const id = String(blockId ?? '');
|
const id = String(blockId ?? '');
|
||||||
return Boolean(id && this.preparedRevealCache.has(id));
|
return Boolean(id && this.preparedRevealCache.has(id));
|
||||||
|
|||||||
+88
-22
@@ -211,6 +211,7 @@ const fastFlipOverlap = 5;
|
|||||||
let activeFlips = [];
|
let activeFlips = [];
|
||||||
let pendingPageFlips = 0;
|
let pendingPageFlips = 0;
|
||||||
const pendingRevealStartBlockIds = new Set();
|
const pendingRevealStartBlockIds = new Set();
|
||||||
|
const activeRevealBlockStarts = new Map();
|
||||||
|
|
||||||
const paperColor = new THREE.Color(0xece4ca);
|
const paperColor = new THREE.Color(0xece4ca);
|
||||||
const inkColor = '#1a1009';
|
const inkColor = '#1a1009';
|
||||||
@@ -2020,28 +2021,30 @@ function handlePageCanvases(event) {
|
|||||||
preloadOnly: Boolean(detail.preloadOnly),
|
preloadOnly: Boolean(detail.preloadOnly),
|
||||||
pageMeta: currentPageMeta
|
pageMeta: currentPageMeta
|
||||||
});
|
});
|
||||||
|
const leftReveal = attachRevealPageMeta(detail.reveal?.left, detail.pageMeta?.left || currentPageMeta.left || null);
|
||||||
|
const rightReveal = attachRevealPageMeta(detail.reveal?.right, detail.pageMeta?.right || currentPageMeta.right || null);
|
||||||
if (detail.preloadOnly) {
|
if (detail.preloadOnly) {
|
||||||
if (detail.left) {
|
if (detail.left) {
|
||||||
const texture = preloadPageTexture('left', detail.left, detail.reveal?.left);
|
const texture = preloadPageTexture('left', detail.left, leftReveal, detail.pageMeta?.left || null);
|
||||||
rememberResidentPageTexture(currentPageMeta.left, texture, detail.left);
|
rememberResidentPageTexture(detail.pageMeta?.left || null, texture, detail.left);
|
||||||
}
|
}
|
||||||
if (detail.right) {
|
if (detail.right) {
|
||||||
const texture = preloadPageTexture('right', detail.right, detail.reveal?.right);
|
const texture = preloadPageTexture('right', detail.right, rightReveal, detail.pageMeta?.right || null);
|
||||||
rememberResidentPageTexture(currentPageMeta.right, texture, detail.right);
|
rememberResidentPageTexture(detail.pageMeta?.right || null, texture, detail.right);
|
||||||
}
|
}
|
||||||
markPageTextureTiming('handlePageCanvases:preloadOnly:end');
|
markPageTextureTiming('handlePageCanvases:preloadOnly:end');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (detail.left) {
|
if (detail.left) {
|
||||||
if (detail.reveal?.left) {
|
if (leftReveal) {
|
||||||
beginPageReveal('left', detail.left, detail.reveal.left);
|
beginPageReveal('left', detail.left, leftReveal);
|
||||||
} else {
|
} else {
|
||||||
uploadPageTextureDirect('left', detail.left, currentPageMeta.left);
|
uploadPageTextureDirect('left', detail.left, currentPageMeta.left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (detail.right) {
|
if (detail.right) {
|
||||||
if (detail.reveal?.right) {
|
if (rightReveal) {
|
||||||
beginPageReveal('right', detail.right, detail.reveal.right);
|
beginPageReveal('right', detail.right, rightReveal);
|
||||||
} else {
|
} else {
|
||||||
uploadPageTextureDirect('right', detail.right, currentPageMeta.right);
|
uploadPageTextureDirect('right', detail.right, currentPageMeta.right);
|
||||||
}
|
}
|
||||||
@@ -2055,16 +2058,26 @@ function handlePageCanvases(event) {
|
|||||||
markPageTextureTiming('handlePageCanvases:end');
|
markPageTextureTiming('handlePageCanvases:end');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRevealCacheKey(revealDetail = {}) {
|
function attachRevealPageMeta(revealDetail = null, pageMeta = null) {
|
||||||
const ids = Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : [];
|
if (!revealDetail) return null;
|
||||||
return ids.map(id => String(id)).join('|') || 'direct';
|
return {
|
||||||
|
...revealDetail,
|
||||||
|
pageMeta: pageMeta ? { ...pageMeta } : null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadPageTexture(side, sourceCanvas, revealDetail = {}) {
|
function getRevealCacheKey(revealDetail = {}) {
|
||||||
|
const ids = Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : [];
|
||||||
|
const pageIndex = Number(revealDetail.pageMeta?.pageIndex);
|
||||||
|
const pageKey = Number.isFinite(pageIndex) ? `page:${Math.max(0, Math.round(pageIndex))}` : 'page:unknown';
|
||||||
|
return `${pageKey}:${ids.map(id => String(id)).join('|') || 'direct'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadPageTexture(side, sourceCanvas, revealDetail = {}, pageMeta = null) {
|
||||||
if (!sourceCanvas) return null;
|
if (!sourceCanvas) return null;
|
||||||
const texture = createPageCanvasTexture(sourceCanvas);
|
const texture = createPageCanvasTexture(sourceCanvas);
|
||||||
const baseTexture = revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null;
|
const baseTexture = revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null;
|
||||||
const key = getRevealCacheKey(revealDetail);
|
const key = getRevealCacheKey({ ...(revealDetail || {}), pageMeta: revealDetail?.pageMeta || pageMeta || null });
|
||||||
markPageTextureTiming('preloadTexture:start', {
|
markPageTextureTiming('preloadTexture:start', {
|
||||||
side,
|
side,
|
||||||
key,
|
key,
|
||||||
@@ -2312,13 +2325,19 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
}
|
}
|
||||||
const baseTexture = prepared?.baseTexture || (revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null);
|
const baseTexture = prepared?.baseTexture || (revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null);
|
||||||
|
|
||||||
|
const revealBlockIds = Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds.map(value => String(value)) : [];
|
||||||
|
const activeStartedAt = revealBlockIds
|
||||||
|
.map(blockId => activeRevealBlockStarts.get(blockId))
|
||||||
|
.filter(value => Number.isFinite(Number(value)))
|
||||||
|
.sort((a, b) => a - b)[0] ?? null;
|
||||||
|
|
||||||
pageRevealState[side] = {
|
pageRevealState[side] = {
|
||||||
startedAt: revealDetail.startNow ? performance.now() : null,
|
startedAt: activeStartedAt ?? (revealDetail.startNow ? performance.now() : null),
|
||||||
pendingStart: false,
|
pendingStart: false,
|
||||||
lastRevealFrameAt: null,
|
lastRevealFrameAt: null,
|
||||||
visualElapsedMs: 0,
|
visualElapsedMs: activeStartedAt ? Math.max(0, performance.now() - activeStartedAt) : 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: revealBlockIds,
|
||||||
baseTexture,
|
baseTexture,
|
||||||
fastForwarding: false,
|
fastForwarding: false,
|
||||||
fastForwardStartedAt: null,
|
fastForwardStartedAt: null,
|
||||||
@@ -2328,6 +2347,21 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
if (material?.userData) material.userData.pendingPageReveal = revealDetail;
|
if (material?.userData) material.userData.pendingPageReveal = revealDetail;
|
||||||
if (shader?.uniforms) applyPendingPageReveal(side, shader);
|
if (shader?.uniforms) applyPendingPageReveal(side, shader);
|
||||||
else if (material) material.needsUpdate = true;
|
else if (material) material.needsUpdate = true;
|
||||||
|
if (shader?.uniforms?.bookRevealElapsedMs) {
|
||||||
|
shader.uniforms.bookRevealElapsedMs.value = pageRevealState[side].visualElapsedMs;
|
||||||
|
}
|
||||||
|
if (side === 'right' && isRightBodyPageComplete()) {
|
||||||
|
const targetSpread = Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + 1);
|
||||||
|
prewarmFlipTextures(1, targetSpread).then(() => {
|
||||||
|
markPageTextureTiming('rightPageReveal:flip-prewarm-ready', { targetSpread });
|
||||||
|
}).catch((error) => {
|
||||||
|
recordPageCacheProblem({
|
||||||
|
type: 'right-page-flip-prewarm-error',
|
||||||
|
targetSpread,
|
||||||
|
message: error?.message || String(error)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
document.documentElement.dataset.webglRevealDebug = JSON.stringify({
|
document.documentElement.dataset.webglRevealDebug = JSON.stringify({
|
||||||
side,
|
side,
|
||||||
blockIds: pageRevealState[side].blockIds,
|
blockIds: pageRevealState[side].blockIds,
|
||||||
@@ -2450,6 +2484,7 @@ function clearPageReveal(side, reason = 'clear') {
|
|||||||
function startPageRevealForBlock(blockId) {
|
function startPageRevealForBlock(blockId) {
|
||||||
const id = String(blockId ?? '');
|
const id = String(blockId ?? '');
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
if (!activeRevealBlockStarts.has(id)) activeRevealBlockStarts.set(id, performance.now());
|
||||||
if (activeFlips.length > 0) {
|
if (activeFlips.length > 0) {
|
||||||
pendingRevealStartBlockIds.add(id);
|
pendingRevealStartBlockIds.add(id);
|
||||||
markPageTextureTiming('revealStart:deferred-for-flip', {
|
markPageTextureTiming('revealStart:deferred-for-flip', {
|
||||||
@@ -2463,6 +2498,7 @@ function startPageRevealForBlock(blockId) {
|
|||||||
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.pendingStart = true;
|
state.pendingStart = true;
|
||||||
|
state.startedAt = activeRevealBlockStarts.get(id) || performance.now();
|
||||||
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;
|
||||||
});
|
});
|
||||||
@@ -2493,18 +2529,17 @@ function updatePageRevealAnimations(now) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.pendingStart) {
|
if (state.pendingStart) {
|
||||||
state.startedAt = now;
|
if (state.startedAt == null) state.startedAt = now;
|
||||||
state.pendingStart = false;
|
state.pendingStart = false;
|
||||||
state.lastRevealFrameAt = now;
|
state.lastRevealFrameAt = now;
|
||||||
state.visualElapsedMs = 0;
|
state.visualElapsedMs = Math.max(0, now - state.startedAt);
|
||||||
shader.uniforms.bookRevealElapsedMs.value = 0;
|
shader.uniforms.bookRevealElapsedMs.value = state.visualElapsedMs;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.startedAt == null) {
|
if (state.startedAt == null) {
|
||||||
shader.uniforms.bookRevealElapsedMs.value = 0;
|
shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const revealFrameDeltaMs = state.lastRevealFrameAt == null ? 0 : Math.max(0, now - state.lastRevealFrameAt);
|
|
||||||
state.lastRevealFrameAt = now;
|
state.lastRevealFrameAt = now;
|
||||||
if (state.fastForwarding) {
|
if (state.fastForwarding) {
|
||||||
const fastElapsed = Math.max(0, now - Number(state.fastForwardStartedAt || now));
|
const fastElapsed = Math.max(0, now - Number(state.fastForwardStartedAt || now));
|
||||||
@@ -2515,7 +2550,7 @@ function updatePageRevealAnimations(now) {
|
|||||||
fastProgress
|
fastProgress
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
state.visualElapsedMs = Math.max(0, Number(state.visualElapsedMs || 0)) + Math.min(revealFrameDeltaMs, targetFrameDurationMs);
|
state.visualElapsedMs = Math.max(0, now - state.startedAt);
|
||||||
}
|
}
|
||||||
const progress = THREE.MathUtils.clamp(state.visualElapsedMs / state.durationMs, 0, 1);
|
const progress = THREE.MathUtils.clamp(state.visualElapsedMs / state.durationMs, 0, 1);
|
||||||
shader.uniforms.bookRevealElapsedMs.value = state.visualElapsedMs;
|
shader.uniforms.bookRevealElapsedMs.value = state.visualElapsedMs;
|
||||||
@@ -2995,8 +3030,8 @@ function lineYAtX(points, x) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setActivePageGeometry(flip, surface) {
|
function setActivePageGeometry(flip, surface) {
|
||||||
const geometry = createFlippingPageGeometry(surface);
|
|
||||||
if (!flip.mesh) {
|
if (!flip.mesh) {
|
||||||
|
const geometry = createFlippingPageGeometry(surface);
|
||||||
flip.mesh = new THREE.Mesh(geometry, [
|
flip.mesh = new THREE.Mesh(geometry, [
|
||||||
materials.flipPageSurface,
|
materials.flipPageSurface,
|
||||||
materials.flipPageBackSurface,
|
materials.flipPageBackSurface,
|
||||||
@@ -3009,9 +3044,12 @@ function setActivePageGeometry(flip, surface) {
|
|||||||
book.add(flip.mesh);
|
book.add(flip.mesh);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!updateFlippingPageGeometry(flip.mesh.geometry, surface)) {
|
||||||
|
const geometry = createFlippingPageGeometry(surface);
|
||||||
flip.mesh.geometry.dispose();
|
flip.mesh.geometry.dispose();
|
||||||
flip.mesh.geometry = geometry;
|
flip.mesh.geometry = geometry;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createFlippingPageGeometry(surface) {
|
function createFlippingPageGeometry(surface) {
|
||||||
const positions = [];
|
const positions = [];
|
||||||
@@ -3085,6 +3123,34 @@ function createFlippingPageGeometry(surface) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateFlippingPageGeometry(geometry, surface) {
|
||||||
|
const position = geometry?.getAttribute?.('position');
|
||||||
|
if (!position || !surface?.length || !surface[0]?.length) return false;
|
||||||
|
const widthSegments = surface.length - 1;
|
||||||
|
const depthSegments = surface[0].length - 1;
|
||||||
|
const expectedVertexCount = (widthSegments + 1) * (depthSegments + 1) * 2;
|
||||||
|
if (position.count !== expectedVertexCount) return false;
|
||||||
|
const pageThickness = Math.max(0.0008, Number(PROCEDURAL_BOOK.SHEET_THICKNESS_MODEL || 0.001));
|
||||||
|
const array = position.array;
|
||||||
|
let offset = 0;
|
||||||
|
surface.forEach((rowPoints) => {
|
||||||
|
rowPoints.forEach((point) => {
|
||||||
|
array[offset] = point.x;
|
||||||
|
array[offset + 1] = point.y + pageThickness;
|
||||||
|
array[offset + 2] = point.z;
|
||||||
|
offset += 3;
|
||||||
|
array[offset] = point.x;
|
||||||
|
array[offset + 1] = point.y;
|
||||||
|
array[offset + 2] = point.z;
|
||||||
|
offset += 3;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
position.needsUpdate = true;
|
||||||
|
geometry.computeVertexNormals();
|
||||||
|
geometry.computeBoundingSphere();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function finishActiveFlip(flip) {
|
function finishActiveFlip(flip) {
|
||||||
removeFlipMesh(flip);
|
removeFlipMesh(flip);
|
||||||
activeFlips = activeFlips.filter((active) => active !== flip);
|
activeFlips = activeFlips.filter((active) => active !== flip);
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ const checks = [
|
|||||||
['webgl lab exposes reveal uniform diagnostics', /getRevealDebugState/.test(source) && /bookRevealActive/.test(source) && /bookRevealElapsedMs/.test(source) && /bookRevealRegionCount/.test(source)],
|
['webgl lab exposes reveal uniform diagnostics', /getRevealDebugState/.test(source) && /bookRevealActive/.test(source) && /bookRevealElapsedMs/.test(source) && /bookRevealRegionCount/.test(source)],
|
||||||
['webgl lab records page reveal clear reasons', /clearPageReveal\(side, reason/.test(source) && /webglRevealClearLog/.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 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 reveal visual clock is derived from absolute playback time', /visualElapsedMs/.test(source) && /activeRevealBlockStarts/.test(source) && /state\.visualElapsedMs = Math\.max\(0, now - state\.startedAt\)/.test(source) && !/revealFrameDeltaMs/.test(source)],
|
||||||
['webgl fast-forward accelerates reveal instead of clearing the mask immediately', /fastForwarding/.test(source) && /fastForwardDurationMs/.test(source) && !/clearPageReveal\(side, 'fast-forward'\)/.test(source)],
|
['webgl fast-forward accelerates reveal instead of clearing the mask immediately', /fastForwarding/.test(source) && /fastForwardDurationMs/.test(source) && !/clearPageReveal\(side, 'fast-forward'\)/.test(source)],
|
||||||
['webgl lab records page texture binding timings', /pageTextureTimings/.test(source) && /markPageTextureTiming/.test(source) && /webglPageTextureTimings/.test(source)],
|
['webgl lab records page texture binding timings', /pageTextureTimings/.test(source) && /markPageTextureTiming/.test(source) && /webglPageTextureTimings/.test(source)],
|
||||||
['webgl lab binds source canvases directly instead of copying whole page textures', /bindPageTextureSource/.test(source) && /texture\.image = sourceCanvas/.test(source) && !/drawCanvasPageTexture/.test(methodBody(source, 'uploadPageTextureDirect')) && !/drawCanvasPageTexture/.test(methodBody(source, 'beginPageReveal'))],
|
['webgl lab binds source canvases directly instead of copying whole page textures', /bindPageTextureSource/.test(source) && /texture\.image = sourceCanvas/.test(source) && !/drawCanvasPageTexture/.test(methodBody(source, 'uploadPageTextureDirect')) && !/drawCanvasPageTexture/.test(methodBody(source, 'beginPageReveal'))],
|
||||||
@@ -148,7 +148,7 @@ const checks = [
|
|||||||
['webgl cache is non-optional with a 5gb persistent budget and large memory cache', /maxCacheSizeBytes = 5 \* 1024 \* 1024 \* 1024/.test(webglPageCacheSource) && /maxMemoryCanvasCount = 256/.test(webglPageCacheSource) && /persistent page caching is in a problem state/.test(webglPageCacheSource) && !/if \(this\.memoryCanvasCache\.has\(key\)\) return true/.test(webglPageCacheSource)],
|
['webgl cache is non-optional with a 5gb persistent budget and large memory cache', /maxCacheSizeBytes = 5 \* 1024 \* 1024 \* 1024/.test(webglPageCacheSource) && /maxMemoryCanvasCount = 256/.test(webglPageCacheSource) && /persistent page caching is in a problem state/.test(webglPageCacheSource) && !/if \(this\.memoryCanvasCache\.has\(key\)\) return true/.test(webglPageCacheSource)],
|
||||||
['webgl lab prewarms cached page textures into generous vram before flips', /residentPageTextures/.test(source) && /const maxResidentPageTextures = 192/.test(source) && /preloadCachedPageTexture/.test(source) && /prewarmFlipTextures/.test(source) && /await prewarmFlipTextures\(direction, targetSpread\)/.test(source) && /getResidentPageTexture\(targetBackPageIndex\)/.test(source)],
|
['webgl lab prewarms cached page textures into generous vram before flips', /residentPageTextures/.test(source) && /const maxResidentPageTextures = 192/.test(source) && /preloadCachedPageTexture/.test(source) && /prewarmFlipTextures/.test(source) && /await prewarmFlipTextures\(direction, targetSpread\)/.test(source) && /getResidentPageTexture\(targetBackPageIndex\)/.test(source)],
|
||||||
['webgl lab records cache misses as problem states', /pageCacheProblemLog/.test(source) && /recordPageCacheProblem/.test(source) && /db-cache-miss/.test(source) && /webglPageCacheProblems/.test(source)],
|
['webgl lab records cache misses as problem states', /pageCacheProblemLog/.test(source) && /recordPageCacheProblem/.test(source) && /db-cache-miss/.test(source) && /webglPageCacheProblems/.test(source)],
|
||||||
['webgl lab makes preload-only page canvases resident in vram immediately', /rememberResidentPageTexture/.test(source) && /if \(detail\.preloadOnly\) \{[\s\S]*rememberResidentPageTexture\(currentPageMeta\.left, texture, detail\.left\)[\s\S]*rememberResidentPageTexture\(currentPageMeta\.right, texture, detail\.right\)/.test(source)],
|
['webgl lab makes preload-only page canvases resident by explicit page metadata', /rememberResidentPageTexture/.test(source) && /attachRevealPageMeta/.test(source) && /rememberResidentPageTexture\(detail\.pageMeta\?\.left \|\| null, texture, detail\.left\)/.test(source) && /rememberResidentPageTexture\(detail\.pageMeta\?\.right \|\| null, texture, detail\.right\)/.test(source)],
|
||||||
['webgl lab keeps current visible page textures resident without disposing shared maps', /rememberResidentPageTexture\(pageMeta, texture, sourceCanvas, false\)/.test(source) && /ownsTexture/.test(source) && /if \(oldest\?\.ownsTexture\) oldest\.texture\?\.dispose\?\.\(\)/.test(source)],
|
['webgl lab keeps current visible page textures resident without disposing shared maps', /rememberResidentPageTexture\(pageMeta, texture, sourceCanvas, false\)/.test(source) && /ownsTexture/.test(source) && /if \(oldest\?\.ownsTexture\) oldest\.texture\?\.dispose\?\.\(\)/.test(source)],
|
||||||
['webgl lab reuses current-enough resident cached page textures for direct stack switches', /uploadPageTextureDirect\(side, sourceCanvas, pageMeta = null\)/.test(source) && /getResidentPageTextureForMeta\(pageMeta\)/.test(source) && /usedResidentTexture/.test(source) && /uploadPageTextureDirect\('left', detail\.left, currentPageMeta\.left\)/.test(source) && /uploadPageTextureDirect\('right', detail\.right, currentPageMeta\.right\)/.test(source)],
|
['webgl lab reuses current-enough resident cached page textures for direct stack switches', /uploadPageTextureDirect\(side, sourceCanvas, pageMeta = null\)/.test(source) && /getResidentPageTextureForMeta\(pageMeta\)/.test(source) && /usedResidentTexture/.test(source) && /uploadPageTextureDirect\('left', detail\.left, currentPageMeta\.left\)/.test(source) && /uploadPageTextureDirect\('right', detail\.right, currentPageMeta\.right\)/.test(source)],
|
||||||
['webgl page cache preserves explicit cache keys across writes and reads', /cacheKey: pageMeta\.cacheKey/.test(webglPageCacheSource) && /makePageKey\(pageMeta\)/.test(webglPageCacheSource)],
|
['webgl page cache preserves explicit cache keys across writes and reads', /cacheKey: pageMeta\.cacheKey/.test(webglPageCacheSource) && /makePageKey\(pageMeta\)/.test(webglPageCacheSource)],
|
||||||
@@ -163,7 +163,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 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 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 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 are precomputed from final layout regions', /area: rectWidth \* rectHeight/.test(textureRendererSource) && /textDuration \* \(Math\.max\(1, region\.area\) \/ totalArea\)/.test(textureRendererSource) && /getImageRevealDurationMs/.test(textureRendererSource)],
|
['webgl reveal line timings use absolute block word timing across split pages', /assignRevealTiming/.test(textureRendererSource) && /wordStart/.test(textureRendererSource) && /blockWordStart/.test(textureRendererSource) && /wordTimings\[wordStart\]/.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 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 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)],
|
['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)],
|
||||||
@@ -189,6 +189,7 @@ const checks = [
|
|||||||
['webgl flip page is a two-sided body using paper-thickness constants', /flipPageBackSurface/.test(source) && /flipPageEdge/.test(source) && /new THREE\.Mesh\(geometry, \[\s*materials\.flipPageSurface,\s*materials\.flipPageBackSurface,\s*materials\.flipPageEdge\s*\]\)/.test(source) && /PROCEDURAL_BOOK\.SHEET_THICKNESS_MODEL/.test(source) && /geometry\.addGroup\(0, topIndices\.length, 0\)/.test(source)],
|
['webgl flip page is a two-sided body using paper-thickness constants', /flipPageBackSurface/.test(source) && /flipPageEdge/.test(source) && /new THREE\.Mesh\(geometry, \[\s*materials\.flipPageSurface,\s*materials\.flipPageBackSurface,\s*materials\.flipPageEdge\s*\]\)/.test(source) && /PROCEDURAL_BOOK\.SHEET_THICKNESS_MODEL/.test(source) && /geometry\.addGroup\(0, topIndices\.length, 0\)/.test(source)],
|
||||||
['webgl animated page front and back maps are independently switchable before animation starts', /materials\.flipPageBackSurface = materials\.flipPageSurface\.clone\(\)/.test(source) && /materials\.flipPageSurface\.map = sourceTexture/.test(source) && /materials\.flipPageBackSurface\.map = backTexture/.test(source) && /flip\.sourceTexture = sourceTexture/.test(source) && /flip\.backTexture = backTexture/.test(source)],
|
['webgl animated page front and back maps are independently switchable before animation starts', /materials\.flipPageBackSurface = materials\.flipPageSurface\.clone\(\)/.test(source) && /materials\.flipPageSurface\.map = sourceTexture/.test(source) && /materials\.flipPageBackSurface\.map = backTexture/.test(source) && /flip\.sourceTexture = sourceTexture/.test(source) && /flip\.backTexture = backTexture/.test(source)],
|
||||||
['webgl animated page back face uses its own unflipped page orientation', /bottomRow\.push\(push\(point, 0, u, 1 - v\)\)/.test(source)],
|
['webgl animated page back face uses its own unflipped page orientation', /bottomRow\.push\(push\(point, 0, u, 1 - v\)\)/.test(source)],
|
||||||
|
['webgl animated page reuses geometry buffers during flips', /function updateFlippingPageGeometry/.test(source) && /position\.needsUpdate = true/.test(source) && /updateFlippingPageGeometry\(flip\.mesh\.geometry, surface\)/.test(source) && !/flip\.mesh\.geometry\.dispose\(\);\s*flip\.mesh\.geometry = geometry;/.test(methodBody(source, 'setActivePageGeometry'))],
|
||||||
['webgl scene targets 60fps with browser-frame scheduling live mirror and static heavy refresh', /const targetFrameDurationMs = 1000 \/ 60/.test(source) && /this\.targetFrameDurationMs = 1000 \/ 60/.test(textureRendererSource) && /requestAnimationFrame\(animate\)/.test(source) && /const refreshStaticSceneBuffers = staticSceneBuffersDirty \|\| activeFlips\.length > 0/.test(source) && /updateTableReflection\(\);/.test(source) && /mirrorRefreshesEveryFrame: true/.test(source) && !/setTimeout\(animate/.test(source)],
|
['webgl scene targets 60fps with browser-frame scheduling live mirror and static heavy refresh', /const targetFrameDurationMs = 1000 \/ 60/.test(source) && /this\.targetFrameDurationMs = 1000 \/ 60/.test(textureRendererSource) && /requestAnimationFrame\(animate\)/.test(source) && /const refreshStaticSceneBuffers = staticSceneBuffersDirty \|\| activeFlips\.length > 0/.test(source) && /updateTableReflection\(\);/.test(source) && /mirrorRefreshesEveryFrame: true/.test(source) && !/setTimeout\(animate/.test(source)],
|
||||||
['webgl scene lowers mirror target and caps table film maps to 2k', /const reflectionPixelRatio = 1/.test(source) && /const tableReflectionBaseWidth = 2048/.test(source) && /const tableReflectionBaseHeight = 1152/.test(source) && /tableDustTexture = loadUtilityTexture\('\/assets\/webgl\/table_dust_4k\.png', \{ maxSize: 2048 \}\)/.test(source) && /tableGreaseTexture = loadUtilityTexture\('\/assets\/webgl\/table_grease_4k\.png', \{ maxSize: 2048 \}\)/.test(source)],
|
['webgl scene lowers mirror target and caps table film maps to 2k', /const reflectionPixelRatio = 1/.test(source) && /const tableReflectionBaseWidth = 2048/.test(source) && /const tableReflectionBaseHeight = 1152/.test(source) && /tableDustTexture = loadUtilityTexture\('\/assets\/webgl\/table_dust_4k\.png', \{ maxSize: 2048 \}\)/.test(source) && /tableGreaseTexture = loadUtilityTexture\('\/assets\/webgl\/table_grease_4k\.png', \{ maxSize: 2048 \}\)/.test(source)],
|
||||||
['webgl debug exposes runtime invariants for visual regression tests', /getRuntimeInvariants\(\)/.test(source) && /residentPageTextureCount/.test(source) && /flipFrontBackShareMaterial/.test(source) && /mirrorRefreshesEveryFrame/.test(source)],
|
['webgl debug exposes runtime invariants for visual regression tests', /getRuntimeInvariants\(\)/.test(source) && /residentPageTextureCount/.test(source) && /flipFrontBackShareMaterial/.test(source) && /mirrorRefreshesEveryFrame/.test(source)],
|
||||||
@@ -202,7 +203,8 @@ const checks = [
|
|||||||
['markup parser strips and stores pagereserve directives', /parsePageReserveDirective/.test(markupParserSource) && /#pagereserve\\\[/.test(markupParserSource) && /unit: match\[2\] === '%' \? 'percent' : 'pages'/.test(markupParserSource)],
|
['markup parser strips and stores pagereserve directives', /parsePageReserveDirective/.test(markupParserSource) && /#pagereserve\\\[/.test(markupParserSource) && /unit: match\[2\] === '%' \? 'percent' : 'pages'/.test(markupParserSource)],
|
||||||
['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'))],
|
['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 completion arms a durable autoplay-targeted flip without bypassing choices', /handleRevealCommittedForPageFlip/.test(source) && /tryStartPendingRightPageFlip/.test(source) && /pendingRightPageFlipAutoplay/.test(source) && /const targetSpread = Math\.max\(0, Math\.round\(Number\(bookPaginationState\.spreadIndex \|\| 0\)\) \+ 1\)/.test(source) && /force: options\.force === true \|\| pendingRightPageFlipAutoplay/.test(source) && /isChoiceAwaitingPlayer/.test(source) && /pendingRightPageFlip = true/.test(source)],
|
['webgl right-page completion arms a durable autoplay-targeted flip without bypassing choices', /handleRevealCommittedForPageFlip/.test(source) && /tryStartPendingRightPageFlip/.test(source) && /pendingRightPageFlipAutoplay/.test(source) && /const targetSpread = Math\.max\(0, Math\.round\(Number\(bookPaginationState\.spreadIndex \|\| 0\)\) \+ 1\)/.test(source) && /force: options\.force === true \|\| pendingRightPageFlipAutoplay/.test(source) && /isChoiceAwaitingPlayer/.test(source) && /pendingRightPageFlip = true/.test(source)],
|
||||||
['webgl page flips defer and pause all reveal animation until the flip is finished', /pendingRevealStartBlockIds/.test(source) && /revealStart:deferred-for-flip/.test(source) && /flushPendingRevealStarts/.test(source) && /if \(activeFlips\.length > 0\) return;/.test(methodBody(source, 'updatePageRevealAnimations')) && /dataset\.webglPageFlipActive/.test(source) && /isWebGLPageFlipActive/.test(textureRendererSource) && /animation\.startedAt \+= this\.targetFrameDurationMs/.test(textureRendererSource)],
|
['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)],
|
||||||
|
['texture renderer preloads every spread touched by an active reveal block', /preloadAdditionalRevealSpreads/.test(textureRendererSource) && /spreadContainsBlock/.test(textureRendererSource) && /this\.drawSpread\(spread, \['left', 'right'\], \{ preloadOnly: true \}\)/.test(textureRendererSource) && /this\.activeAnimations\.has\(id\)/.test(textureRendererSource)],
|
||||||
['webgl page flips require resident back textures before animation starts', /prepareStaticPageForFlip\(flip, prewarm = null\)/.test(source) && /flip-back-texture-missing/.test(source) && /return false;/.test(methodBody(source, 'prepareStaticPageForFlip')) && /flipTexturePreflight:ready/.test(source) && /if \(!prepareStaticPageForFlip\(flip, options\.prewarm \|\| null\)\) \{[\s\S]*return false;[\s\S]*\}/.test(source)],
|
['webgl page flips require resident back textures before animation starts', /prepareStaticPageForFlip\(flip, prewarm = null\)/.test(source) && /flip-back-texture-missing/.test(source) && /return false;/.test(methodBody(source, 'prepareStaticPageForFlip')) && /flipTexturePreflight:ready/.test(source) && /if \(!prepareStaticPageForFlip\(flip, options\.prewarm \|\| null\)\) \{[\s\S]*return false;[\s\S]*\}/.test(source)],
|
||||||
['markup and 3d pagination accept full-page images', /'full'/.test(markupParserSource) && /size === 'full'/.test(bookPaginationSource)],
|
['markup and 3d pagination accept full-page images', /'full'/.test(markupParserSource) && /size === 'full'/.test(bookPaginationSource)],
|
||||||
['story history can persist 3d pagination decisions', /persistPaginationMetrics/.test(bookPaginationSource) && /collectPaginationMetrics/.test(bookPaginationSource) && /pageStart/.test(storyHistorySource) && /pagination: metrics\.pagination/.test(storyHistorySource)]
|
['story history can persist 3d pagination decisions', /persistPaginationMetrics/.test(bookPaginationSource) && /collectPaginationMetrics/.test(bookPaginationSource) && /pageStart/.test(storyHistorySource) && /pagination: metrics\.pagination/.test(storyHistorySource)]
|
||||||
|
|||||||
Reference in New Issue
Block a user