Fix WebGL line reveal renderer
This commit is contained in:
@@ -30,8 +30,6 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.revealedBlockIds = new Set();
|
this.revealedBlockIds = new Set();
|
||||||
this.pendingRevealBlockIds = new Set();
|
this.pendingRevealBlockIds = new Set();
|
||||||
this.preparedRevealCache = new Map();
|
this.preparedRevealCache = new Map();
|
||||||
this.revealBounds = null;
|
|
||||||
this.revealWords = null;
|
|
||||||
this.revealBaseCanvases = null;
|
this.revealBaseCanvases = null;
|
||||||
this.revealPublishBlockIds = null;
|
this.revealPublishBlockIds = null;
|
||||||
this.lastDrawSignature = null;
|
this.lastDrawSignature = null;
|
||||||
@@ -64,7 +62,12 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'drawImageFitted',
|
'drawImageFitted',
|
||||||
'drawLine',
|
'drawLine',
|
||||||
'drawWord',
|
'drawWord',
|
||||||
'recordRevealRect',
|
'buildRevealRegions',
|
||||||
|
'collectRevealRegionCandidates',
|
||||||
|
'createRevealRegionForLine',
|
||||||
|
'getLineInkRect',
|
||||||
|
'getLineNaturalWidth',
|
||||||
|
'getImageRevealDurationMs',
|
||||||
'getInlineStyleState',
|
'getInlineStyleState',
|
||||||
'updateInlineStyleState',
|
'updateInlineStyleState',
|
||||||
'getCanvasFont',
|
'getCanvasFont',
|
||||||
@@ -204,8 +207,6 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
revealBlockIds: this.revealPublishBlockIds ? Array.from(this.revealPublishBlockIds) : [],
|
revealBlockIds: this.revealPublishBlockIds ? Array.from(this.revealPublishBlockIds) : [],
|
||||||
preloadOnly: Boolean(options.preloadOnly)
|
preloadOnly: Boolean(options.preloadOnly)
|
||||||
});
|
});
|
||||||
this.revealBounds = { left: null, right: null };
|
|
||||||
this.revealWords = { left: [], right: [] };
|
|
||||||
this.revealBaseCanvases = { left: null, right: null };
|
this.revealBaseCanvases = { left: null, right: null };
|
||||||
sidesToDraw.forEach((side) => {
|
sidesToDraw.forEach((side) => {
|
||||||
if (!this.canvases[side]) return;
|
if (!this.canvases[side]) return;
|
||||||
@@ -220,8 +221,6 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
sides: sidesToDraw,
|
sides: sidesToDraw,
|
||||||
preloadOnly: Boolean(options.preloadOnly)
|
preloadOnly: Boolean(options.preloadOnly)
|
||||||
});
|
});
|
||||||
this.revealBounds = null;
|
|
||||||
this.revealWords = null;
|
|
||||||
this.revealBaseCanvases = null;
|
this.revealBaseCanvases = null;
|
||||||
this.revealPublishBlockIds = null;
|
this.revealPublishBlockIds = null;
|
||||||
if (!options.preloadOnly && !hasReveal) this.lastDrawSignature = drawSignature;
|
if (!options.preloadOnly && !hasReveal) this.lastDrawSignature = drawSignature;
|
||||||
@@ -448,7 +447,6 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
ctx.font = `${dropCapFontPx}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
ctx.font = `${dropCapFontPx}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
ctx.fillText(String(lineRecord.dropCapText), dropCapX, dropCapY);
|
ctx.fillText(String(lineRecord.dropCapText), dropCapX, dropCapY);
|
||||||
this.recordRevealRect(side, lineRecord, dropCapX, dropCapY, fontPx * 2.9, dropCapFontPx * 0.9, 0);
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = smallCaps ? 'all-small-caps' : 'normal';
|
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = smallCaps ? 'all-small-caps' : 'normal';
|
||||||
if ('letterSpacing' in ctx) ctx.letterSpacing = smallCaps ? `${fontPx * 0.012}px` : '0px';
|
if ('letterSpacing' in ctx) ctx.letterSpacing = smallCaps ? `${fontPx * 0.012}px` : '0px';
|
||||||
@@ -578,51 +576,177 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
const value = segment?.value || '';
|
const value = segment?.value || '';
|
||||||
this.applyTextStyle(ctx, fontPx, smallCaps, segment?.style || {});
|
this.applyTextStyle(ctx, fontPx, smallCaps, segment?.style || {});
|
||||||
ctx.fillText(value, x, baseY);
|
ctx.fillText(value, x, baseY);
|
||||||
const width = Number(segment?.width || 0) || ctx.measureText(value).width || fontPx;
|
|
||||||
this.recordRevealRect(side, lineRecord, x, baseY - fontPx, width, lineHeightPx, localWordIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recordRevealRect(side, lineRecord, x, y, width, height, localWordIndex = 0) {
|
buildRevealRegions(side) {
|
||||||
if (!this.revealBounds || !this.revealPublishBlockIds) return;
|
if (!this.revealPublishBlockIds || !this.metrics) return null;
|
||||||
const blockId = String(lineRecord?.blockId ?? '');
|
const candidates = this.collectRevealRegionCandidates();
|
||||||
if (!blockId || !this.revealPublishBlockIds.has(blockId)) return;
|
if (!candidates.length) return null;
|
||||||
const animation = this.activeAnimations.get(blockId);
|
const byBlock = candidates.reduce((map, region) => {
|
||||||
if (!animation || animation.completed) return;
|
if (!map.has(region.blockId)) map.set(region.blockId, []);
|
||||||
const padding = Math.max(2, Number(lineRecord.fontPx || 18) * 0.12);
|
map.get(region.blockId).push(region);
|
||||||
const nextRect = {
|
return map;
|
||||||
x: Math.max(0, x - padding),
|
}, new Map());
|
||||||
y: Math.max(0, y - padding),
|
const regions = [];
|
||||||
right: Math.min(this.metrics.width, x + width + padding),
|
byBlock.forEach((blockRegions, blockId) => {
|
||||||
bottom: Math.min(this.metrics.height, y + height + padding)
|
const animation = this.activeAnimations.get(blockId);
|
||||||
};
|
if (!animation || animation.completed) return;
|
||||||
const current = this.revealBounds[side];
|
const fixedRegions = blockRegions.filter(region => region.fixedDurationMs > 0);
|
||||||
this.revealBounds[side] = current ? {
|
const textRegions = blockRegions.filter(region => !(region.fixedDurationMs > 0));
|
||||||
x: Math.min(current.x, nextRect.x),
|
let delay = 0;
|
||||||
y: Math.min(current.y, nextRect.y),
|
const textDuration = Math.max(0, Number(animation.totalDuration || 0));
|
||||||
right: Math.max(current.right, nextRect.right),
|
const totalArea = textRegions.reduce((sum, region) => sum + Math.max(1, region.area), 0);
|
||||||
bottom: Math.max(current.bottom, nextRect.bottom),
|
textRegions.forEach((region) => {
|
||||||
blockIds: current.blockIds.add(blockId)
|
const duration = totalArea > 0
|
||||||
} : {
|
? Math.max(1, textDuration * (Math.max(1, region.area) / totalArea))
|
||||||
...nextRect,
|
: Math.max(1, textDuration / Math.max(1, textRegions.length));
|
||||||
blockIds: new Set([blockId])
|
regions.push({
|
||||||
};
|
...region,
|
||||||
const globalWordIndex = Math.max(0, Number(lineRecord.blockWordStart || 0) + Number(localWordIndex || 0));
|
timing: {
|
||||||
const timing = Array.isArray(animation.wordTimings) ? animation.wordTimings[globalWordIndex] : null;
|
delay,
|
||||||
if (!timing || !this.revealWords?.[side]) return;
|
duration
|
||||||
this.revealWords[side].push({
|
}
|
||||||
blockId,
|
});
|
||||||
wordIndex: globalWordIndex,
|
delay += duration;
|
||||||
rect: {
|
});
|
||||||
x: nextRect.x / this.metrics.width,
|
fixedRegions.forEach((region) => {
|
||||||
y: nextRect.y / this.metrics.height,
|
regions.push({
|
||||||
width: Math.max(0.001, (nextRect.right - nextRect.x) / this.metrics.width),
|
...region,
|
||||||
height: Math.max(0.001, (nextRect.bottom - nextRect.y) / this.metrics.height)
|
timing: {
|
||||||
},
|
delay,
|
||||||
timing: {
|
duration: Math.max(1, region.fixedDurationMs)
|
||||||
delay: Math.max(0, Number(timing.delay || 0)),
|
}
|
||||||
duration: Math.max(1, Number(timing.duration || 1))
|
});
|
||||||
}
|
delay += Math.max(1, region.fixedDurationMs);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
const sideRegions = regions.filter(region => region.side === side);
|
||||||
|
if (!sideRegions.length) return null;
|
||||||
|
const bounds = sideRegions.reduce((box, region) => ({
|
||||||
|
x: Math.min(box.x, region.pixelRect.x),
|
||||||
|
y: Math.min(box.y, region.pixelRect.y),
|
||||||
|
right: Math.max(box.right, region.pixelRect.right),
|
||||||
|
bottom: Math.max(box.bottom, region.pixelRect.bottom)
|
||||||
|
}), {
|
||||||
|
x: this.metrics.width,
|
||||||
|
y: this.metrics.height,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
blockIds: Array.from(byBlock.keys()),
|
||||||
|
durationMs: regions.reduce((maxDuration, region) => Math.max(maxDuration, region.timing.delay + region.timing.duration), 0),
|
||||||
|
baseCanvas: null,
|
||||||
|
lineRects: sideRegions.map(region => ({
|
||||||
|
blockId: region.blockId,
|
||||||
|
lineIndex: region.lineIndex,
|
||||||
|
rect: region.rect,
|
||||||
|
timing: region.timing
|
||||||
|
})),
|
||||||
|
bounds: {
|
||||||
|
x: bounds.x / this.metrics.width,
|
||||||
|
y: bounds.y / this.metrics.height,
|
||||||
|
width: Math.max(0.001, (bounds.right - bounds.x) / this.metrics.width),
|
||||||
|
height: Math.max(0.001, (bounds.bottom - bounds.y) / this.metrics.height)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
collectRevealRegionCandidates() {
|
||||||
|
const candidates = [];
|
||||||
|
['left', 'right'].forEach((side) => {
|
||||||
|
const spreadLines = Array.isArray(this.currentSpread?.[side]) ? this.currentSpread[side] : [];
|
||||||
|
spreadLines.forEach((lineRecord) => {
|
||||||
|
const region = this.createRevealRegionForLine(side, lineRecord);
|
||||||
|
if (region) candidates.push(region);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRevealRegionForLine(side, lineRecord = {}) {
|
||||||
|
const blockId = String(lineRecord?.blockId ?? '');
|
||||||
|
if (!blockId || !this.revealPublishBlockIds.has(blockId)) return null;
|
||||||
|
const animation = this.activeAnimations.get(blockId);
|
||||||
|
if (!animation || animation.completed) return null;
|
||||||
|
if (lineRecord.type === 'image' || lineRecord.kind === 'image') {
|
||||||
|
const content = this.getPageContent(side);
|
||||||
|
const rect = lineRecord.metadata?.imageLayout?.textureRect || {};
|
||||||
|
const x = content.x + Number(rect.x || 0);
|
||||||
|
const y = content.y + Number(rect.y || 0);
|
||||||
|
const width = Math.max(1, Number(rect.width || content.width));
|
||||||
|
const height = Math.max(1, Number(rect.height || this.metrics.typographyLineHeightPx));
|
||||||
|
return this.normalizeRevealRegion(side, blockId, lineRecord, x, y, width, height, this.getImageRevealDurationMs(lineRecord));
|
||||||
|
}
|
||||||
|
const rect = this.getLineInkRect(side, lineRecord);
|
||||||
|
if (!rect) return null;
|
||||||
|
return this.normalizeRevealRegion(side, blockId, lineRecord, rect.x, rect.y, rect.width, rect.height, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeRevealRegion(side, blockId, lineRecord, x, y, width, height, fixedDurationMs = 0) {
|
||||||
|
const padding = Math.max(2, Number(lineRecord.fontPx || 18) * 0.12);
|
||||||
|
const left = Math.max(0, x - padding);
|
||||||
|
const top = Math.max(0, y - padding);
|
||||||
|
const right = Math.min(this.metrics.width, x + width + padding);
|
||||||
|
const bottom = Math.min(this.metrics.height, y + height + padding);
|
||||||
|
const rectWidth = Math.max(1, right - left);
|
||||||
|
const rectHeight = Math.max(1, bottom - top);
|
||||||
|
return {
|
||||||
|
side,
|
||||||
|
blockId,
|
||||||
|
lineIndex: Number(lineRecord.lineIndex ?? lineRecord.pageLine ?? 0),
|
||||||
|
fixedDurationMs,
|
||||||
|
area: rectWidth * rectHeight,
|
||||||
|
pixelRect: { x: left, y: top, right, bottom },
|
||||||
|
rect: {
|
||||||
|
x: left / this.metrics.width,
|
||||||
|
y: top / this.metrics.height,
|
||||||
|
width: Math.max(0.001, rectWidth / this.metrics.width),
|
||||||
|
height: Math.max(0.001, rectHeight / this.metrics.height)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getLineInkRect(side, lineRecord = {}) {
|
||||||
|
const content = this.getPageContent(side);
|
||||||
|
const fontPx = Math.max(1, Number(lineRecord.fontPx || 22));
|
||||||
|
const lineHeightPx = Math.max(fontPx + 2, Number(lineRecord.lineHeightPx || this.metrics.typographyLineHeightPx || 30));
|
||||||
|
const line = lineRecord.line || {};
|
||||||
|
const naturalWidth = this.getLineNaturalWidth(line);
|
||||||
|
const centerOffset = line.align === 'center'
|
||||||
|
? Math.max(0, (content.width - naturalWidth) / 2)
|
||||||
|
: Number(line.offset || 0);
|
||||||
|
const measuredWidth = Number(line.measure || lineRecord.measure || 0);
|
||||||
|
const isJustified = line.align !== 'center' && !line.isFinal;
|
||||||
|
let x = content.x + centerOffset;
|
||||||
|
let y = content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx);
|
||||||
|
let width = Math.max(1, Math.min(content.width - centerOffset, isJustified ? (measuredWidth || content.width - centerOffset) : (naturalWidth || measuredWidth || content.width - centerOffset)));
|
||||||
|
let height = lineHeightPx;
|
||||||
|
if (lineRecord.dropCapText) {
|
||||||
|
const dropCapFontPx = Math.round(fontPx * 2.68);
|
||||||
|
const dropCapY = content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) + (fontPx * 0.25);
|
||||||
|
const dropCapWidth = fontPx * 2.9;
|
||||||
|
const normalRight = x + width;
|
||||||
|
x = Math.min(content.x, x);
|
||||||
|
y = Math.min(y, dropCapY);
|
||||||
|
width = Math.max(normalRight, content.x + dropCapWidth) - x;
|
||||||
|
height = Math.max((content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx)) + lineHeightPx, dropCapY + (dropCapFontPx * 0.9)) - y;
|
||||||
|
}
|
||||||
|
return { x, y, width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
getLineNaturalWidth(line = {}) {
|
||||||
|
const nodes = Array.isArray(line.nodes) ? line.nodes : [];
|
||||||
|
return nodes.reduce((sum, node) => {
|
||||||
|
if (node?.type === 'box' || node?.type === 'glue') return sum + Number(node.width || 0);
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getImageRevealDurationMs(lineRecord = {}) {
|
||||||
|
const metadata = lineRecord.metadata || {};
|
||||||
|
const explicit = Number(metadata.animationMs || metadata.revealMs || metadata.imageRevealMs || 0);
|
||||||
|
return Number.isFinite(explicit) && explicit > 0 ? explicit : 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
startRevealAnimation(detail = {}) {
|
startRevealAnimation(detail = {}) {
|
||||||
@@ -822,6 +946,10 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.animationFrameId = window.setTimeout(() => this.tickAnimations(performance.now()), this.targetFrameDurationMs);
|
this.animationFrameId = window.setTimeout(() => this.tickAnimations(performance.now()), this.targetFrameDurationMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isWebGLPageFlipActive() {
|
||||||
|
return document.documentElement.dataset.webglPageFlipActive === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
tickAnimations(now) {
|
tickAnimations(now) {
|
||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
if (now - this.lastAnimationFrameAt < this.targetFrameDurationMs) {
|
if (now - this.lastAnimationFrameAt < this.targetFrameDurationMs) {
|
||||||
@@ -832,6 +960,18 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
|
|
||||||
let hasActive = false;
|
let hasActive = false;
|
||||||
const currentNow = performance.now();
|
const currentNow = performance.now();
|
||||||
|
if (this.isWebGLPageFlipActive()) {
|
||||||
|
this.activeAnimations.forEach((animation) => {
|
||||||
|
if (animation.completed) return;
|
||||||
|
if (!Array.isArray(animation.wordTimings) || animation.wordTimings.length === 0) return;
|
||||||
|
hasActive = true;
|
||||||
|
if (animation.startedAt != null) {
|
||||||
|
animation.startedAt += this.targetFrameDurationMs;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hasActive) this.requestAnimationFrame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.activeAnimations.forEach((animation) => {
|
this.activeAnimations.forEach((animation) => {
|
||||||
if (animation.completed) return;
|
if (animation.completed) return;
|
||||||
if (!Array.isArray(animation.wordTimings) || animation.wordTimings.length === 0) return;
|
if (!Array.isArray(animation.wordTimings) || animation.wordTimings.length === 0) return;
|
||||||
@@ -853,9 +993,9 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
|
|
||||||
publishSpread(sides = null, options = {}) {
|
publishSpread(sides = null, options = {}) {
|
||||||
const sidesToPublish = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
const sidesToPublish = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||||
const wordCounts = {
|
const regionCounts = {
|
||||||
left: this.revealWords?.left?.length || 0,
|
left: 0,
|
||||||
right: this.revealWords?.right?.length || 0
|
right: 0
|
||||||
};
|
};
|
||||||
const detail = {
|
const detail = {
|
||||||
metrics: this.metrics,
|
metrics: this.metrics,
|
||||||
@@ -872,38 +1012,20 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
const reveal = {};
|
const reveal = {};
|
||||||
sidesToPublish.forEach((side) => {
|
sidesToPublish.forEach((side) => {
|
||||||
const bounds = this.revealBounds?.[side];
|
const sideReveal = this.buildRevealRegions(side);
|
||||||
if (!bounds) return;
|
if (!sideReveal) return;
|
||||||
const blockIds = Array.from(bounds.blockIds || []);
|
sideReveal.baseCanvas = options.preloadOnly
|
||||||
const durationMs = blockIds.reduce((maxDuration, blockId) => {
|
? this.cloneCanvas(this.revealBaseCanvases?.[side])
|
||||||
const animation = this.activeAnimations.get(String(blockId));
|
: this.revealBaseCanvases?.[side] || null;
|
||||||
return Math.max(maxDuration, Number(animation?.totalDuration || 0));
|
regionCounts[side] = sideReveal.lineRects.length;
|
||||||
}, 0);
|
reveal[side] = sideReveal;
|
||||||
if (durationMs <= 0) return;
|
|
||||||
reveal[side] = {
|
|
||||||
blockIds,
|
|
||||||
durationMs,
|
|
||||||
baseCanvas: options.preloadOnly ? this.cloneCanvas(this.revealBaseCanvases?.[side]) : this.revealBaseCanvases?.[side] || null,
|
|
||||||
wordRects: (this.revealWords?.[side] || []).map(word => ({
|
|
||||||
blockId: word.blockId,
|
|
||||||
wordIndex: word.wordIndex,
|
|
||||||
rect: word.rect,
|
|
||||||
timing: word.timing
|
|
||||||
})),
|
|
||||||
bounds: {
|
|
||||||
x: bounds.x / this.metrics.width,
|
|
||||||
y: bounds.y / this.metrics.height,
|
|
||||||
width: Math.max(0.001, (bounds.right - bounds.x) / this.metrics.width),
|
|
||||||
height: Math.max(0.001, (bounds.bottom - bounds.y) / this.metrics.height)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
if (Object.keys(reveal).length) detail.reveal = reveal;
|
if (Object.keys(reveal).length) detail.reveal = reveal;
|
||||||
this.cachePublishedPages(sidesToPublish, detail);
|
this.cachePublishedPages(sidesToPublish, detail);
|
||||||
this.markPipelineTiming('publishSpread', {
|
this.markPipelineTiming('publishSpread', {
|
||||||
sides: sidesToPublish,
|
sides: sidesToPublish,
|
||||||
hasReveal: Object.keys(reveal).length > 0,
|
hasReveal: Object.keys(reveal).length > 0,
|
||||||
wordCounts,
|
regionCounts,
|
||||||
preloadOnly: Boolean(options.preloadOnly)
|
preloadOnly: Boolean(options.preloadOnly)
|
||||||
});
|
});
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
||||||
|
|||||||
@@ -981,15 +981,26 @@ class UIDisplayHandlerModule extends BaseModule {
|
|||||||
const generation = this.displayGeneration;
|
const generation = this.displayGeneration;
|
||||||
const sentenceGameId = sentence.gameId || null;
|
const sentenceGameId = sentence.gameId || null;
|
||||||
const isCurrent = () => this.isDisplayGenerationCurrent(generation, sentenceGameId);
|
const isCurrent = () => this.isDisplayGenerationCurrent(generation, sentenceGameId);
|
||||||
|
const useWebGLBookReveal = this.isWebGLMode() && (sentence.kind === 'paragraph' || sentence.kind === 'heading');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (useWebGLBookReveal) {
|
||||||
|
await this.prepareWebGLBookReveal(sentence);
|
||||||
|
if (!isCurrent()) return null;
|
||||||
|
await this.playbackCoordinator.play(sentence);
|
||||||
|
if (!isCurrent()) return null;
|
||||||
|
if (sentence.blockId != null) this.markBlockRendered(sentence.blockId);
|
||||||
|
this.dispatchDeferredTagsForBlock(sentence);
|
||||||
|
if (sentence.onComplete) sentence.onComplete();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureLiveTailWindow();
|
await this.ensureLiveTailWindow();
|
||||||
if (!isCurrent()) return null;
|
if (!isCurrent()) return null;
|
||||||
await this.scrollTo(this.getLiveEndLine(), { mode: 'enter-live-tail', smooth: false });
|
await this.scrollTo(this.getLiveEndLine(), { mode: 'enter-live-tail', smooth: false });
|
||||||
if (!isCurrent()) return null;
|
if (!isCurrent()) return null;
|
||||||
this.rebuildLayoutExclusions(this.renderedItems);
|
this.rebuildLayoutExclusions(this.renderedItems);
|
||||||
this.layoutFlowLine = this.getFlowLineFromItems(this.renderedItems);
|
this.layoutFlowLine = this.getFlowLineFromItems(this.renderedItems);
|
||||||
const useWebGLBookReveal = this.isWebGLMode() && (sentence.kind === 'paragraph' || sentence.kind === 'heading');
|
|
||||||
const element = await this.renderStoryBlock(sentence, {
|
const element = await this.renderStoryBlock(sentence, {
|
||||||
animate: true,
|
animate: true,
|
||||||
playback: true,
|
playback: true,
|
||||||
|
|||||||
+134
-59
@@ -40,7 +40,7 @@ renderer.shadowMap.type = THREE.VSMShadowMap;
|
|||||||
|
|
||||||
const generatedTextureCanvases = {};
|
const generatedTextureCanvases = {};
|
||||||
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
|
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||||
const reflectionPixelRatio = 2;
|
const reflectionPixelRatio = 1;
|
||||||
const pageTextureWidth = 3072;
|
const pageTextureWidth = 3072;
|
||||||
const reflectionTargetSize = new THREE.Vector2();
|
const reflectionTargetSize = new THREE.Vector2();
|
||||||
const pageRaycaster = new THREE.Raycaster();
|
const pageRaycaster = new THREE.Raycaster();
|
||||||
@@ -80,8 +80,8 @@ let tableDustTexture = null;
|
|||||||
let tableGreaseTexture = null;
|
let tableGreaseTexture = null;
|
||||||
const tableTopY = -0.02;
|
const tableTopY = -0.02;
|
||||||
const bookTableContactClearance = 0.002;
|
const bookTableContactClearance = 0.002;
|
||||||
const tableReflectionBaseWidth = 4096;
|
const tableReflectionBaseWidth = 2048;
|
||||||
const tableReflectionBaseHeight = 2304;
|
const tableReflectionBaseHeight = 1152;
|
||||||
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
|
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
|
||||||
colorSpace: THREE.SRGBColorSpace,
|
colorSpace: THREE.SRGBColorSpace,
|
||||||
depthBuffer: true,
|
depthBuffer: true,
|
||||||
@@ -210,10 +210,11 @@ const fastFlipCount = 10;
|
|||||||
const fastFlipOverlap = 5;
|
const fastFlipOverlap = 5;
|
||||||
let activeFlips = [];
|
let activeFlips = [];
|
||||||
let pendingPageFlips = 0;
|
let pendingPageFlips = 0;
|
||||||
|
const pendingRevealStartBlockIds = new Set();
|
||||||
|
|
||||||
const paperColor = new THREE.Color(0xece4ca);
|
const paperColor = new THREE.Color(0xece4ca);
|
||||||
const inkColor = '#1a1009';
|
const inkColor = '#1a1009';
|
||||||
const maxRevealWords = 256;
|
const maxRevealRegions = 128;
|
||||||
const completedRevealElapsedMs = 1000000000;
|
const completedRevealElapsedMs = 1000000000;
|
||||||
|
|
||||||
await reportLabStep(48, 'Preparing high-resolution page textures');
|
await reportLabStep(48, 'Preparing high-resolution page textures');
|
||||||
@@ -584,7 +585,8 @@ window.BookLabDebug = {
|
|||||||
maxResidentPageTextures,
|
maxResidentPageTextures,
|
||||||
pageCacheProblemCount: pageCacheProblemLog.length,
|
pageCacheProblemCount: pageCacheProblemLog.length,
|
||||||
flipFrontBackShareMaterial: materials.flipPageSurface === materials.flipPageBackSurface,
|
flipFrontBackShareMaterial: materials.flipPageSurface === materials.flipPageBackSurface,
|
||||||
mirrorRefreshesEveryFrame: true
|
mirrorRefreshesEveryFrame: true,
|
||||||
|
mirrorRefreshesWhenStaticDirty: true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
projectPointerToPage(clientX, clientY) {
|
projectPointerToPage(clientX, clientY) {
|
||||||
@@ -696,11 +698,11 @@ function buildTable() {
|
|||||||
tableNormal.wrapT = THREE.RepeatWrapping;
|
tableNormal.wrapT = THREE.RepeatWrapping;
|
||||||
tableNormal.repeat.set(2.15, 1.45);
|
tableNormal.repeat.set(2.15, 1.45);
|
||||||
tableNormal.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
tableNormal.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||||
tableDustTexture = loadUtilityTexture('/assets/webgl/table_dust_4k.png');
|
tableDustTexture = loadUtilityTexture('/assets/webgl/table_dust_4k.png', { maxSize: 2048 });
|
||||||
tableDustTexture.wrapS = THREE.ClampToEdgeWrapping;
|
tableDustTexture.wrapS = THREE.ClampToEdgeWrapping;
|
||||||
tableDustTexture.wrapT = THREE.ClampToEdgeWrapping;
|
tableDustTexture.wrapT = THREE.ClampToEdgeWrapping;
|
||||||
tableDustTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
tableDustTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||||
tableGreaseTexture = loadUtilityTexture('/assets/webgl/table_grease_4k.png');
|
tableGreaseTexture = loadUtilityTexture('/assets/webgl/table_grease_4k.png', { maxSize: 2048 });
|
||||||
tableGreaseTexture.wrapS = THREE.ClampToEdgeWrapping;
|
tableGreaseTexture.wrapS = THREE.ClampToEdgeWrapping;
|
||||||
tableGreaseTexture.wrapT = THREE.ClampToEdgeWrapping;
|
tableGreaseTexture.wrapT = THREE.ClampToEdgeWrapping;
|
||||||
tableGreaseTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
tableGreaseTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||||
@@ -724,8 +726,21 @@ function buildTable() {
|
|||||||
scene.add(tableMesh);
|
scene.add(tableMesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUtilityTexture(url) {
|
function loadUtilityTexture(url, options = {}) {
|
||||||
const texture = new THREE.TextureLoader().load(url);
|
const texture = new THREE.TextureLoader().load(url, (loadedTexture) => {
|
||||||
|
const maxSize = Math.max(0, Number(options.maxSize || 0));
|
||||||
|
const image = loadedTexture.image;
|
||||||
|
const width = Number(image?.naturalWidth || image?.width || 0);
|
||||||
|
const height = Number(image?.naturalHeight || image?.height || 0);
|
||||||
|
if (!maxSize || !width || !height || (width <= maxSize && height <= maxSize)) return;
|
||||||
|
const scale = Math.min(maxSize / width, maxSize / height);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = Math.max(1, Math.round(width * scale));
|
||||||
|
canvas.height = Math.max(1, Math.round(height * scale));
|
||||||
|
canvas.getContext('2d')?.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||||
|
loadedTexture.image = canvas;
|
||||||
|
loadedTexture.needsUpdate = true;
|
||||||
|
});
|
||||||
texture.colorSpace = THREE.NoColorSpace;
|
texture.colorSpace = THREE.NoColorSpace;
|
||||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
texture.magFilter = THREE.LinearFilter;
|
texture.magFilter = THREE.LinearFilter;
|
||||||
@@ -738,7 +753,7 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
const isHardcoverPaper = material.userData?.isHardcoverPaper === true;
|
const isHardcoverPaper = material.userData?.isHardcoverPaper === true;
|
||||||
const isHeadband = material.userData?.isHeadband === true;
|
const isHeadband = material.userData?.isHeadband === true;
|
||||||
const pageReveal = material.userData?.bookPageReveal || null;
|
const pageReveal = material.userData?.bookPageReveal || null;
|
||||||
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}-${pageReveal ? 'page-reveal-v2' : isHeadband ? 'headband-v1' : isSpineCloth ? 'spine-cloth-v4' : isHardcoverPaper ? 'hardcover-paper-v1' : 'plain'}`;
|
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}-${pageReveal ? 'page-reveal-line-v1' : isHeadband ? 'headband-v1' : isSpineCloth ? 'spine-cloth-v4' : isHardcoverPaper ? 'hardcover-paper-v1' : 'plain'}`;
|
||||||
material.onBeforeCompile = (shader) => {
|
material.onBeforeCompile = (shader) => {
|
||||||
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
||||||
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
||||||
@@ -748,9 +763,9 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
if (pageReveal) {
|
if (pageReveal) {
|
||||||
shader.uniforms.bookRevealActive = { value: 0 };
|
shader.uniforms.bookRevealActive = { value: 0 };
|
||||||
shader.uniforms.bookRevealElapsedMs = { value: completedRevealElapsedMs };
|
shader.uniforms.bookRevealElapsedMs = { value: completedRevealElapsedMs };
|
||||||
shader.uniforms.bookRevealWordCount = { value: 0 };
|
shader.uniforms.bookRevealRegionCount = { value: 0 };
|
||||||
shader.uniforms.bookRevealWordRects = { value: Array.from({ length: maxRevealWords }, () => new THREE.Vector4(0, 0, 0, 0)) };
|
shader.uniforms.bookRevealRegionRects = { value: Array.from({ length: maxRevealRegions }, () => new THREE.Vector4(0, 0, 0, 0)) };
|
||||||
shader.uniforms.bookRevealWordTimings = { value: Array.from({ length: maxRevealWords }, () => new THREE.Vector4(0, 1, 0, 0)) };
|
shader.uniforms.bookRevealRegionTimings = { value: Array.from({ length: maxRevealRegions }, () => new THREE.Vector4(0, 1, 0, 0)) };
|
||||||
shader.uniforms.bookRevealPaperColor = { value: paperColor.clone() };
|
shader.uniforms.bookRevealPaperColor = { value: paperColor.clone() };
|
||||||
shader.uniforms.bookRevealBaseMap = { value: leftTexture };
|
shader.uniforms.bookRevealBaseMap = { value: leftTexture };
|
||||||
shader.uniforms.bookRevealUseBaseMap = { value: 0 };
|
shader.uniforms.bookRevealUseBaseMap = { value: 0 };
|
||||||
@@ -793,9 +808,9 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
uniform float bookTableTopY;
|
uniform float bookTableTopY;
|
||||||
${pageReveal ? `uniform float bookRevealActive;
|
${pageReveal ? `uniform float bookRevealActive;
|
||||||
uniform float bookRevealElapsedMs;
|
uniform float bookRevealElapsedMs;
|
||||||
uniform int bookRevealWordCount;
|
uniform int bookRevealRegionCount;
|
||||||
uniform vec4 bookRevealWordRects[256];
|
uniform vec4 bookRevealRegionRects[128];
|
||||||
uniform vec4 bookRevealWordTimings[256];
|
uniform vec4 bookRevealRegionTimings[128];
|
||||||
uniform vec3 bookRevealPaperColor;
|
uniform vec3 bookRevealPaperColor;
|
||||||
uniform sampler2D bookRevealBaseMap;
|
uniform sampler2D bookRevealBaseMap;
|
||||||
uniform float bookRevealUseBaseMap;
|
uniform float bookRevealUseBaseMap;
|
||||||
@@ -803,17 +818,17 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
|
|
||||||
float bookRevealVisibleMask(vec2 uv) {
|
float bookRevealVisibleMask(vec2 uv) {
|
||||||
float hidden = 0.0;
|
float hidden = 0.0;
|
||||||
for (int i = 0; i < 256; i++) {
|
for (int i = 0; i < 128; i++) {
|
||||||
if (i >= bookRevealWordCount) break;
|
float enabled = step(float(i) + 0.5, float(bookRevealRegionCount));
|
||||||
vec4 rect = bookRevealWordRects[i];
|
vec4 rect = bookRevealRegionRects[i];
|
||||||
vec2 local = (uv - rect.xy) / max(rect.zw, vec2(0.0001));
|
vec2 local = (uv - rect.xy) / max(rect.zw, vec2(0.0001));
|
||||||
float inside = step(0.0, local.x) * step(0.0, local.y) * step(local.x, 1.0) * step(local.y, 1.0);
|
float inside = step(0.0, local.x) * step(0.0, local.y) * step(local.x, 1.0) * step(local.y, 1.0);
|
||||||
vec4 timing = bookRevealWordTimings[i];
|
vec4 timing = bookRevealRegionTimings[i];
|
||||||
float progress = clamp((bookRevealElapsedMs - timing.x) / max(1.0, timing.y), 0.0, 1.0);
|
float progress = clamp((bookRevealElapsedMs - timing.x) / max(1.0, timing.y), 0.0, 1.0);
|
||||||
float scan = clamp(local.x * 0.88 + (1.0 - local.y) * 0.12, 0.0, 1.0);
|
float scan = clamp(local.x * 0.96 + (1.0 - local.y) * 0.04, 0.0, 1.0);
|
||||||
float feather = max(0.0001, bookRevealSoftness);
|
float feather = max(0.0001, bookRevealSoftness);
|
||||||
float visible = smoothstep(scan - feather, scan + feather, progress);
|
float visible = smoothstep(scan - feather, scan + feather, progress);
|
||||||
hidden = max(hidden, inside * (1.0 - visible));
|
hidden = max(hidden, enabled * inside * (1.0 - visible));
|
||||||
}
|
}
|
||||||
return hidden;
|
return hidden;
|
||||||
}` : ''}
|
}` : ''}
|
||||||
@@ -2075,6 +2090,21 @@ function preloadPageTexture(side, sourceCanvas, revealDetail = {}) {
|
|||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flushPendingRevealStarts() {
|
||||||
|
if (activeFlips.length > 0 || pendingRevealStartBlockIds.size === 0) return;
|
||||||
|
const blockIds = Array.from(pendingRevealStartBlockIds);
|
||||||
|
pendingRevealStartBlockIds.clear();
|
||||||
|
blockIds.forEach(blockId => startPageRevealForBlock(blockId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPageFlipActiveFlag() {
|
||||||
|
if (activeFlips.length > 0) {
|
||||||
|
document.documentElement.dataset.webglPageFlipActive = 'true';
|
||||||
|
} else {
|
||||||
|
delete document.documentElement.dataset.webglPageFlipActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function recordPageCacheProblem(detail = {}) {
|
function recordPageCacheProblem(detail = {}) {
|
||||||
const entry = {
|
const entry = {
|
||||||
...detail,
|
...detail,
|
||||||
@@ -2195,10 +2225,15 @@ async function preloadCachedPageTexture(pageIndex) {
|
|||||||
|
|
||||||
async function prewarmSpreadTextures(spreadIndex) {
|
async function prewarmSpreadTextures(spreadIndex) {
|
||||||
const indices = spreadPageIndices(spreadIndex);
|
const indices = spreadPageIndices(spreadIndex);
|
||||||
await Promise.all([
|
const [left, right] = await Promise.all([
|
||||||
preloadCachedPageTexture(indices.left),
|
preloadCachedPageTexture(indices.left),
|
||||||
preloadCachedPageTexture(indices.right)
|
preloadCachedPageTexture(indices.right)
|
||||||
]);
|
]);
|
||||||
|
return {
|
||||||
|
spreadIndex: Math.max(0, Math.round(Number(spreadIndex || 0))),
|
||||||
|
left,
|
||||||
|
right
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prewarmFlipTextures(direction, targetSpread = null) {
|
async function prewarmFlipTextures(direction, targetSpread = null) {
|
||||||
@@ -2206,10 +2241,14 @@ async function prewarmFlipTextures(direction, targetSpread = null) {
|
|||||||
const nextSpread = Number.isFinite(Number(targetSpread))
|
const nextSpread = Number.isFinite(Number(targetSpread))
|
||||||
? Math.max(0, Math.round(Number(targetSpread)))
|
? Math.max(0, Math.round(Number(targetSpread)))
|
||||||
: Math.max(0, currentSpread + Math.sign(Number(direction || 0)));
|
: Math.max(0, currentSpread + Math.sign(Number(direction || 0)));
|
||||||
await Promise.all([
|
const [current, next] = await Promise.all([
|
||||||
prewarmSpreadTextures(currentSpread),
|
prewarmSpreadTextures(currentSpread),
|
||||||
prewarmSpreadTextures(nextSpread)
|
prewarmSpreadTextures(nextSpread)
|
||||||
]);
|
]);
|
||||||
|
return {
|
||||||
|
current,
|
||||||
|
next
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function takePreparedPageTexture(side, revealDetail = {}) {
|
function takePreparedPageTexture(side, revealDetail = {}) {
|
||||||
@@ -2258,7 +2297,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
|
|
||||||
markPageTextureTiming('revealUpload:start', {
|
markPageTextureTiming('revealUpload:start', {
|
||||||
side,
|
side,
|
||||||
wordCount: Array.isArray(revealDetail.wordRects) ? revealDetail.wordRects.length : 0,
|
regionCount: Array.isArray(revealDetail.lineRects) ? revealDetail.lineRects.length : 0,
|
||||||
usedPreparedTexture: Boolean(prepared),
|
usedPreparedTexture: Boolean(prepared),
|
||||||
usedPreparedBaseTexture: Boolean(prepared?.baseTexture)
|
usedPreparedBaseTexture: Boolean(prepared?.baseTexture)
|
||||||
});
|
});
|
||||||
@@ -2292,7 +2331,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
document.documentElement.dataset.webglRevealDebug = JSON.stringify({
|
document.documentElement.dataset.webglRevealDebug = JSON.stringify({
|
||||||
side,
|
side,
|
||||||
blockIds: pageRevealState[side].blockIds,
|
blockIds: pageRevealState[side].blockIds,
|
||||||
wordCount: Array.isArray(revealDetail.wordRects) ? revealDetail.wordRects.length : 0,
|
regionCount: Array.isArray(revealDetail.lineRects) ? revealDetail.lineRects.length : 0,
|
||||||
shaderReady: Boolean(shader?.uniforms),
|
shaderReady: Boolean(shader?.uniforms),
|
||||||
started: pageRevealState[side].startedAt != null
|
started: pageRevealState[side].startedAt != null
|
||||||
});
|
});
|
||||||
@@ -2303,7 +2342,7 @@ function applyPendingPageReveal(side, shader = getPageRevealShader(side)) {
|
|||||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||||
const revealDetail = material?.userData?.pendingPageReveal;
|
const revealDetail = material?.userData?.pendingPageReveal;
|
||||||
if (!revealDetail || !shader?.uniforms) return false;
|
if (!revealDetail || !shader?.uniforms) return false;
|
||||||
applyPageRevealWords(shader, revealDetail.wordRects || []);
|
applyPageRevealRegions(shader, revealDetail.lineRects || []);
|
||||||
shader.uniforms.bookRevealActive.value = 1;
|
shader.uniforms.bookRevealActive.value = 1;
|
||||||
shader.uniforms.bookRevealElapsedMs.value = 0;
|
shader.uniforms.bookRevealElapsedMs.value = 0;
|
||||||
const baseTexture = pageRevealState[side]?.baseTexture;
|
const baseTexture = pageRevealState[side]?.baseTexture;
|
||||||
@@ -2312,7 +2351,7 @@ function applyPendingPageReveal(side, shader = getPageRevealShader(side)) {
|
|||||||
document.documentElement.dataset.webglRevealDebug = JSON.stringify({
|
document.documentElement.dataset.webglRevealDebug = JSON.stringify({
|
||||||
side,
|
side,
|
||||||
blockIds: pageRevealState[side]?.blockIds || revealDetail.blockIds || [],
|
blockIds: pageRevealState[side]?.blockIds || revealDetail.blockIds || [],
|
||||||
wordCount: Array.isArray(revealDetail.wordRects) ? revealDetail.wordRects.length : 0,
|
regionCount: Array.isArray(revealDetail.lineRects) ? revealDetail.lineRects.length : 0,
|
||||||
shaderReady: true,
|
shaderReady: true,
|
||||||
started: pageRevealState[side]?.startedAt != null
|
started: pageRevealState[side]?.startedAt != null
|
||||||
});
|
});
|
||||||
@@ -2320,20 +2359,19 @@ function applyPendingPageReveal(side, shader = getPageRevealShader(side)) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPageRevealWords(shader, words = []) {
|
function applyPageRevealRegions(shader, regions = []) {
|
||||||
const rectUniforms = shader.uniforms.bookRevealWordRects.value;
|
const rectUniforms = shader.uniforms.bookRevealRegionRects.value;
|
||||||
const timingUniforms = shader.uniforms.bookRevealWordTimings.value;
|
const timingUniforms = shader.uniforms.bookRevealRegionTimings.value;
|
||||||
const source = Array.isArray(words) ? words.slice(0, maxRevealWords) : [];
|
const source = Array.isArray(regions) ? regions : [];
|
||||||
shader.uniforms.bookRevealWordCount.value = source.length;
|
if (source.length > maxRevealRegions) {
|
||||||
source.forEach((word, index) => {
|
throw new Error(`WebGL reveal region count ${source.length} exceeds architectural maximum ${maxRevealRegions}`);
|
||||||
const rect = word.rect || {};
|
}
|
||||||
const timing = word.timing || {};
|
shader.uniforms.bookRevealRegionCount.value = source.length;
|
||||||
const nextTiming = source[index + 1]?.timing || {};
|
source.forEach((region, index) => {
|
||||||
|
const rect = region.rect || {};
|
||||||
|
const timing = region.timing || {};
|
||||||
const delay = Math.max(0, Number(timing.delay || 0));
|
const delay = Math.max(0, Number(timing.delay || 0));
|
||||||
const nextDelay = Number(nextTiming.delay);
|
const duration = Math.max(1, Number(timing.duration || 1));
|
||||||
const allottedDuration = Number.isFinite(nextDelay) && nextDelay > delay
|
|
||||||
? nextDelay - delay
|
|
||||||
: Number(timing.duration || 1);
|
|
||||||
const x = THREE.MathUtils.clamp(Number(rect.x || 0), 0, 1);
|
const x = THREE.MathUtils.clamp(Number(rect.x || 0), 0, 1);
|
||||||
const y = THREE.MathUtils.clamp(Number(rect.y || 0), 0, 1);
|
const y = THREE.MathUtils.clamp(Number(rect.y || 0), 0, 1);
|
||||||
const width = THREE.MathUtils.clamp(Number(rect.width || 0), 0, 1);
|
const width = THREE.MathUtils.clamp(Number(rect.width || 0), 0, 1);
|
||||||
@@ -2346,12 +2384,12 @@ function applyPageRevealWords(shader, words = []) {
|
|||||||
);
|
);
|
||||||
timingUniforms[index].set(
|
timingUniforms[index].set(
|
||||||
delay,
|
delay,
|
||||||
Math.max(1, allottedDuration),
|
duration,
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
for (let index = source.length; index < maxRevealWords; index += 1) {
|
for (let index = source.length; index < maxRevealRegions; index += 1) {
|
||||||
rectUniforms[index].set(0, 0, 0, 0);
|
rectUniforms[index].set(0, 0, 0, 0);
|
||||||
timingUniforms[index].set(0, 1, 0, 0);
|
timingUniforms[index].set(0, 1, 0, 0);
|
||||||
}
|
}
|
||||||
@@ -2370,7 +2408,7 @@ function getRevealDebugState() {
|
|||||||
active: Number(uniforms.bookRevealActive?.value || 0),
|
active: Number(uniforms.bookRevealActive?.value || 0),
|
||||||
elapsedMs: Number(uniforms.bookRevealElapsedMs?.value || 0),
|
elapsedMs: Number(uniforms.bookRevealElapsedMs?.value || 0),
|
||||||
visualElapsedMs: Number(pageRevealState[side]?.visualElapsedMs || 0),
|
visualElapsedMs: Number(pageRevealState[side]?.visualElapsedMs || 0),
|
||||||
wordCount: Number(uniforms.bookRevealWordCount?.value || 0),
|
regionCount: Number(uniforms.bookRevealRegionCount?.value || 0),
|
||||||
usesBaseTexture: Number(uniforms.bookRevealUseBaseMap?.value || 0),
|
usesBaseTexture: Number(uniforms.bookRevealUseBaseMap?.value || 0),
|
||||||
fastForwarding: pageRevealState[side]?.fastForwarding === true,
|
fastForwarding: pageRevealState[side]?.fastForwarding === true,
|
||||||
started: pageRevealState[side]?.startedAt != null,
|
started: pageRevealState[side]?.startedAt != null,
|
||||||
@@ -2403,7 +2441,7 @@ function clearPageReveal(side, reason = 'clear') {
|
|||||||
if (shader?.uniforms?.bookRevealActive) {
|
if (shader?.uniforms?.bookRevealActive) {
|
||||||
shader.uniforms.bookRevealActive.value = 0;
|
shader.uniforms.bookRevealActive.value = 0;
|
||||||
shader.uniforms.bookRevealElapsedMs.value = completedRevealElapsedMs;
|
shader.uniforms.bookRevealElapsedMs.value = completedRevealElapsedMs;
|
||||||
shader.uniforms.bookRevealWordCount.value = 0;
|
shader.uniforms.bookRevealRegionCount.value = 0;
|
||||||
if (shader.uniforms.bookRevealUseBaseMap) shader.uniforms.bookRevealUseBaseMap.value = 0;
|
if (shader.uniforms.bookRevealUseBaseMap) shader.uniforms.bookRevealUseBaseMap.value = 0;
|
||||||
}
|
}
|
||||||
previousState?.baseTexture?.dispose?.();
|
previousState?.baseTexture?.dispose?.();
|
||||||
@@ -2411,6 +2449,15 @@ function clearPageReveal(side, reason = 'clear') {
|
|||||||
|
|
||||||
function startPageRevealForBlock(blockId) {
|
function startPageRevealForBlock(blockId) {
|
||||||
const id = String(blockId ?? '');
|
const id = String(blockId ?? '');
|
||||||
|
if (!id) return;
|
||||||
|
if (activeFlips.length > 0) {
|
||||||
|
pendingRevealStartBlockIds.add(id);
|
||||||
|
markPageTextureTiming('revealStart:deferred-for-flip', {
|
||||||
|
blockId: id,
|
||||||
|
activeFlips: activeFlips.length
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
['left', 'right'].forEach((side) => {
|
['left', 'right'].forEach((side) => {
|
||||||
const state = pageRevealState[side];
|
const state = pageRevealState[side];
|
||||||
if (!state || state.startedAt != null) return;
|
if (!state || state.startedAt != null) return;
|
||||||
@@ -2436,6 +2483,7 @@ function fastForwardPageReveals(blockIds = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updatePageRevealAnimations(now) {
|
function updatePageRevealAnimations(now) {
|
||||||
|
if (activeFlips.length > 0) return;
|
||||||
['left', 'right'].forEach((side) => {
|
['left', 'right'].forEach((side) => {
|
||||||
const state = pageRevealState[side];
|
const state = pageRevealState[side];
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
@@ -2598,10 +2646,11 @@ async function startPageFlip(direction, options = {}) {
|
|||||||
if (activeFlips.length || !currentProceduralBookModel) return false;
|
if (activeFlips.length || !currentProceduralBookModel) return false;
|
||||||
if (!options.force && !canPageFlip(direction)) return false;
|
if (!options.force && !canPageFlip(direction)) return false;
|
||||||
const targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
const targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
||||||
await prewarmFlipTextures(direction, targetSpread);
|
const prewarm = await prewarmFlipTextures(direction, targetSpread);
|
||||||
return startPageFlipPrepared(direction, {
|
return startPageFlipPrepared(direction, {
|
||||||
...options,
|
...options,
|
||||||
targetSpread
|
targetSpread,
|
||||||
|
prewarm
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2610,12 +2659,15 @@ function startPageFlipPrepared(direction, options = {}) {
|
|||||||
if (!options.force && !canPageFlip(direction)) return false;
|
if (!options.force && !canPageFlip(direction)) return false;
|
||||||
const flip = createPageFlip(direction, performance.now(), normalFlipDuration);
|
const flip = createPageFlip(direction, performance.now(), normalFlipDuration);
|
||||||
if (!flip) return false;
|
if (!flip) return false;
|
||||||
|
flip.targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
||||||
|
if (!prepareStaticPageForFlip(flip, options.prewarm || null)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
pendingRightPageFlip = false;
|
pendingRightPageFlip = false;
|
||||||
pendingRightPageFlipAutoplay = false;
|
pendingRightPageFlipAutoplay = false;
|
||||||
delete document.documentElement.dataset.webglPendingPageFlip;
|
delete document.documentElement.dataset.webglPendingPageFlip;
|
||||||
flip.targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
|
||||||
prepareStaticPageForFlip(flip);
|
|
||||||
activeFlips.push(flip);
|
activeFlips.push(flip);
|
||||||
|
setPageFlipActiveFlag();
|
||||||
syncBookControls();
|
syncBookControls();
|
||||||
updateActiveFlips(flip.startTime);
|
updateActiveFlips(flip.startTime);
|
||||||
return true;
|
return true;
|
||||||
@@ -2624,10 +2676,11 @@ function startPageFlipPrepared(direction, options = {}) {
|
|||||||
async function startFastPageFlip(direction, options = {}) {
|
async function startFastPageFlip(direction, options = {}) {
|
||||||
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
||||||
const targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
const targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
||||||
await prewarmFlipTextures(direction, targetSpread);
|
const prewarm = await prewarmFlipTextures(direction, targetSpread);
|
||||||
return startFastPageFlipPrepared(direction, {
|
return startFastPageFlipPrepared(direction, {
|
||||||
...options,
|
...options,
|
||||||
targetSpread
|
targetSpread,
|
||||||
|
prewarm
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2635,7 +2688,7 @@ function startFastPageFlipPrepared(direction, options = {}) {
|
|||||||
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
||||||
const firstFlip = createPageFlip(direction, performance.now(), fastFlipDuration);
|
const firstFlip = createPageFlip(direction, performance.now(), fastFlipDuration);
|
||||||
if (!firstFlip) return false;
|
if (!firstFlip) return false;
|
||||||
prepareStaticPageForFlip(firstFlip);
|
if (!prepareStaticPageForFlip(firstFlip, options.prewarm || null)) return false;
|
||||||
const startTime = firstFlip.startTime;
|
const startTime = firstFlip.startTime;
|
||||||
const interval = fastFlipDuration / fastFlipOverlap;
|
const interval = fastFlipDuration / fastFlipOverlap;
|
||||||
const skippedSpreads = Math.max(2, Number(options.skippedSpreads || fastFlipCount));
|
const skippedSpreads = Math.max(2, Number(options.skippedSpreads || fastFlipCount));
|
||||||
@@ -2651,6 +2704,7 @@ function startFastPageFlipPrepared(direction, options = {}) {
|
|||||||
countAsPending: false
|
countAsPending: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setPageFlipActiveFlag();
|
||||||
syncBookControls();
|
syncBookControls();
|
||||||
updateActiveFlips(startTime);
|
updateActiveFlips(startTime);
|
||||||
return true;
|
return true;
|
||||||
@@ -2676,8 +2730,8 @@ function createPageFlip(direction, startTime, duration) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareStaticPageForFlip(flip) {
|
function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||||
if (!flip) return;
|
if (!flip) return false;
|
||||||
const sourceMaterial = flip.sourcePageSide === 'left' ? materials.leftPage : materials.rightPage;
|
const sourceMaterial = flip.sourcePageSide === 'left' ? materials.leftPage : materials.rightPage;
|
||||||
const sourceTexture = sourceMaterial?.map || (flip.sourcePageSide === 'left' ? leftTexture : rightTexture);
|
const sourceTexture = sourceMaterial?.map || (flip.sourcePageSide === 'left' ? leftTexture : rightTexture);
|
||||||
const targetSpread = Number.isFinite(Number(flip.targetSpread))
|
const targetSpread = Number.isFinite(Number(flip.targetSpread))
|
||||||
@@ -2685,7 +2739,20 @@ function prepareStaticPageForFlip(flip) {
|
|||||||
: Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + Math.sign(Number(flip.direction || 0)));
|
: Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + Math.sign(Number(flip.direction || 0)));
|
||||||
const targetPages = spreadPageIndices(targetSpread);
|
const targetPages = spreadPageIndices(targetSpread);
|
||||||
const targetBackPageIndex = flip.direction > 0 ? targetPages.left : targetPages.right;
|
const targetBackPageIndex = flip.direction > 0 ? targetPages.left : targetPages.right;
|
||||||
const backTexture = getResidentPageTexture(targetBackPageIndex) || getBlankPageTexture();
|
const residentBackTexture = getResidentPageTexture(targetBackPageIndex);
|
||||||
|
const requiresWrittenTexture = targetBackPageIndex <= Math.max(2, Number(bookPaginationState.writtenPageLimit || 0));
|
||||||
|
if (!residentBackTexture && requiresWrittenTexture) {
|
||||||
|
recordPageCacheProblem({
|
||||||
|
type: 'flip-back-texture-missing',
|
||||||
|
targetBackPageIndex,
|
||||||
|
targetSpread,
|
||||||
|
direction: flip.direction,
|
||||||
|
prewarmedCurrent: Boolean(prewarm?.current),
|
||||||
|
prewarmedNext: Boolean(prewarm?.next)
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const backTexture = residentBackTexture || getBlankPageTexture();
|
||||||
materials.flipPageSurface.map = sourceTexture;
|
materials.flipPageSurface.map = sourceTexture;
|
||||||
materials.flipPageBackSurface.map = backTexture;
|
materials.flipPageBackSurface.map = backTexture;
|
||||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||||
@@ -2712,6 +2779,14 @@ function prepareStaticPageForFlip(flip) {
|
|||||||
materials.leftPage.needsUpdate = true;
|
materials.leftPage.needsUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
markPageTextureTiming('flipTexturePreflight:ready', {
|
||||||
|
direction: flip.direction,
|
||||||
|
sourceSide: flip.sourcePageSide,
|
||||||
|
targetSpread,
|
||||||
|
targetBackPageIndex,
|
||||||
|
usedResidentBackTexture: Boolean(residentBackTexture)
|
||||||
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function canPageFlip(direction) {
|
function canPageFlip(direction) {
|
||||||
@@ -3013,6 +3088,7 @@ function createFlippingPageGeometry(surface) {
|
|||||||
function finishActiveFlip(flip) {
|
function finishActiveFlip(flip) {
|
||||||
removeFlipMesh(flip);
|
removeFlipMesh(flip);
|
||||||
activeFlips = activeFlips.filter((active) => active !== flip);
|
activeFlips = activeFlips.filter((active) => active !== flip);
|
||||||
|
setPageFlipActiveFlag();
|
||||||
if (activeFlips.length === 0 && Number.isFinite(Number(flip.targetSpread))) {
|
if (activeFlips.length === 0 && Number.isFinite(Number(flip.targetSpread))) {
|
||||||
bookPaginationState = {
|
bookPaginationState = {
|
||||||
...bookPaginationState,
|
...bookPaginationState,
|
||||||
@@ -3027,6 +3103,7 @@ function finishActiveFlip(flip) {
|
|||||||
targetSpread: Number.isFinite(Number(flip.targetSpread)) ? Math.max(0, Math.round(Number(flip.targetSpread))) : null
|
targetSpread: Number.isFinite(Number(flip.targetSpread)) ? Math.max(0, Math.round(Number(flip.targetSpread))) : null
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
flushPendingRevealStarts();
|
||||||
if (flip.commitBundleOnFinish) {
|
if (flip.commitBundleOnFinish) {
|
||||||
if (Number.isFinite(Number(flip.targetSpread))) {
|
if (Number.isFinite(Number(flip.targetSpread))) {
|
||||||
syncBookControls();
|
syncBookControls();
|
||||||
@@ -4119,10 +4196,8 @@ function animate(now = performance.now()) {
|
|||||||
updateBookShadowMaps();
|
updateBookShadowMaps();
|
||||||
lastFrameTiming.shadows = performance.now() - shadowStartedAt;
|
lastFrameTiming.shadows = performance.now() - shadowStartedAt;
|
||||||
const reflectionStartedAt = performance.now();
|
const reflectionStartedAt = performance.now();
|
||||||
const refreshStaticSceneBuffers = true;
|
const refreshStaticSceneBuffers = staticSceneBuffersDirty || activeFlips.length > 0;
|
||||||
if (refreshStaticSceneBuffers) {
|
updateTableReflection();
|
||||||
updateTableReflection();
|
|
||||||
}
|
|
||||||
lastFrameTiming.reflection = performance.now() - reflectionStartedAt;
|
lastFrameTiming.reflection = performance.now() - reflectionStartedAt;
|
||||||
const renderStartedAt = performance.now();
|
const renderStartedAt = performance.now();
|
||||||
if (tableDebugMode === tableDebugModes.mirror) {
|
if (tableDebugMode === tableDebugModes.mirror) {
|
||||||
|
|||||||
@@ -108,8 +108,11 @@ const checks = [
|
|||||||
['3D UI defers rendered history mark until playback completes', /deferRenderedMark/.test(uiDisplayHandlerSource) && /prepareWebGLBookReveal/.test(uiDisplayHandlerSource) && /markBlockRendered\(sentence\.blockId/.test(uiDisplayHandlerSource)],
|
['3D UI defers rendered history mark until playback completes', /deferRenderedMark/.test(uiDisplayHandlerSource) && /prepareWebGLBookReveal/.test(uiDisplayHandlerSource) && /markBlockRendered\(sentence\.blockId/.test(uiDisplayHandlerSource)],
|
||||||
['pagination can build a pending unrendered 3D block', /preparePendingBlock/.test(bookPaginationSource) && /book-pagination:prepare-block/.test(bookPaginationSource)],
|
['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 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)],
|
['texture renderer publishes line reveal coordinates from final page layout', /buildRevealRegions/.test(textureRendererSource) && /lineRects/.test(textureRendererSource) && /getLineInkRect/.test(textureRendererSource) && /fixedDurationMs/.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)],
|
['texture renderer carries page side through reveal region normalization', /normalizeRevealRegion\(side, blockId, lineRecord/.test(textureRendererSource) && /normalizeRevealRegion\(side, blockId, lineRecord, x, y, width, height/.test(textureRendererSource) && /normalizeRevealRegion\(side, blockId, lineRecord, rect\.x, rect\.y, rect\.width, rect\.height/.test(textureRendererSource)],
|
||||||
|
['texture renderer does not call removed word reveal recorder', !/recordRevealRect/.test(textureRendererSource)],
|
||||||
|
['page reveal shader uses line coordinate mask instead of comparing page textures', /bookRevealRegionRects/.test(source) && /bookRevealRegionTimings/.test(source) && /bookRevealElapsedMs/.test(source) && !/texture2D\(bookRevealMap/.test(source)],
|
||||||
|
['page reveal shader keeps a fixed loop without dynamic break', /float enabled = step\(float\(i\) \+ 0\.5, float\(bookRevealRegionCount\)\)/.test(source) && !/if \(i >= bookRevealRegionCount\) break/.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)],
|
||||||
@@ -119,7 +122,7 @@ const checks = [
|
|||||||
['webgl lab exposes loader timing diagnostics', /loaderTimings/.test(source) && /markLoaderTiming/.test(source) && /primeSceneForLoader/.test(source)],
|
['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 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 sizes render targets before static loader prime', /await reportLabStep\(86, 'Preparing static shadow and mirror maps'\);\s*resize\(\);\s*primeSceneForLoader\(\);/.test(source) && /lastResizeWidth/.test(source) && /lastResizeHeight/.test(source)],
|
['webgl lab sizes render targets before static loader prime', /await reportLabStep\(86, 'Preparing static shadow and mirror maps'\);\s*resize\(\);\s*primeSceneForLoader\(\);/.test(source) && /lastResizeWidth/.test(source) && /lastResizeHeight/.test(source)],
|
||||||
['webgl lab exposes reveal uniform diagnostics', /getRevealDebugState/.test(source) && /bookRevealActive/.test(source) && /bookRevealElapsedMs/.test(source) && /bookRevealWordCount/.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 caps missed-frame deltas', /visualElapsedMs/.test(source) && /revealFrameDeltaMs/.test(source) && /Math\.min\(revealFrameDeltaMs/.test(source)],
|
||||||
@@ -129,7 +132,7 @@ const checks = [
|
|||||||
['page texture dark-pixel sampling only runs in table debug mode', /function shouldSamplePageTextureDebug\(\)/.test(source) && /tableDebugMode !== tableDebugModes\.none/.test(source) && /shouldSamplePageTextureDebug\(\) \? countPageTextureDarkPixels\(canvas\) : null/.test(source)],
|
['page texture dark-pixel sampling only runs in table debug mode', /function shouldSamplePageTextureDebug\(\)/.test(source) && /tableDebugMode !== tableDebugModes\.none/.test(source) && /shouldSamplePageTextureDebug\(\) \? countPageTextureDarkPixels\(canvas\) : null/.test(source)],
|
||||||
['texture renderer exposes reveal pipeline diagnostics', /pipelineTimings/.test(textureRendererSource) && /markPipelineTiming/.test(textureRendererSource) && /webglTexturePipeline/.test(textureRendererSource)],
|
['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 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)],
|
['texture renderer diagnostics include reveal region counts', /regionCounts/.test(textureRendererSource) && /lineRects/.test(textureRendererSource) && /durationMs/.test(textureRendererSource)],
|
||||||
['texture renderer does not draw immediate non-pending sides during pending reveal preparation', !/immediateSides/.test(textureRendererSource) && !/this\.drawSpread\(this\.currentSpread, immediateSides\)/.test(textureRendererSource)],
|
['texture renderer does not draw immediate non-pending sides during pending reveal preparation', !/immediateSides/.test(textureRendererSource) && !/this\.drawSpread\(this\.currentSpread, immediateSides\)/.test(textureRendererSource)],
|
||||||
['sentence queue consumes completed prepared lookahead items', /preparedSentenceCache/.test(sentenceQueueSource) && /this\.preparedSentenceCache\.get\(cacheKey\)/.test(sentenceQueueSource) && /return prefetched/.test(sentenceQueueSource)],
|
['sentence queue consumes completed prepared lookahead items', /preparedSentenceCache/.test(sentenceQueueSource) && /this\.preparedSentenceCache\.get\(cacheKey\)/.test(sentenceQueueSource) && /return prefetched/.test(sentenceQueueSource)],
|
||||||
['sentence queue front-loads 3D book presentation before playback callback', /const sentence = await this\.getPreparedSentence\(item\);[\s\S]*await this\.prefetchWebGLBookPresentation\(sentence[\s\S]*immediate: true[\s\S]*this\.prefetchAhead\(6, queueGeneration\);[\s\S]*this\.onSentenceReadyCallback/.test(sentenceQueueSource)],
|
['sentence queue front-loads 3D book presentation before playback callback', /const sentence = await this\.getPreparedSentence\(item\);[\s\S]*await this\.prefetchWebGLBookPresentation\(sentence[\s\S]*immediate: true[\s\S]*this\.prefetchAhead\(6, queueGeneration\);[\s\S]*this\.onSentenceReadyCallback/.test(sentenceQueueSource)],
|
||||||
@@ -159,11 +162,12 @@ const checks = [
|
|||||||
['webgl lab can preload page textures without swapping visible page material', /preparedPageTextures/.test(source) && /preloadPageTexture/.test(source) && /renderer\.initTexture\(texture\)/.test(source) && /takePreparedPageTexture/.test(source)],
|
['webgl lab can preload page textures without swapping visible page material', /preparedPageTextures/.test(source) && /preloadPageTexture/.test(source) && /renderer\.initTexture\(texture\)/.test(source) && /takePreparedPageTexture/.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 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 x-dominant scan', /smoothstep\(0\.52, 0\.9, luminance\)/.test(source) && /local\.x \* 0\.88/.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 words consume the allotted time until the next word', /nextTiming/.test(source) && /allottedDuration/.test(source) && /nextDelay - delay/.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 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)],
|
||||||
|
['3D live text bypasses #page_right DOM rendering and uses book texture reveal directly', /const useWebGLBookReveal = this\.isWebGLMode\(\) && \(sentence\.kind === 'paragraph' \|\| sentence\.kind === 'heading'\)/.test(uiDisplayHandlerSource) && /if \(useWebGLBookReveal\) \{[\s\S]*await this\.prepareWebGLBookReveal\(sentence\);[\s\S]*await this\.playbackCoordinator\.play\(sentence\);[\s\S]*this\.markBlockRendered\(sentence\.blockId\);[\s\S]*return null;/.test(uiDisplayHandlerSource)],
|
||||||
['drop-cap remaining text does not reinsert discretionary hyphen markers', /extractRemainingLayoutText/.test(bookPaginationSource) && !bookPaginationSource.includes("fragments.push('|')")],
|
['drop-cap remaining text does not reinsert discretionary hyphen markers', /extractRemainingLayoutText/.test(bookPaginationSource) && !bookPaginationSource.includes("fragments.push('|')")],
|
||||||
['drop-cap reservation keeps a normal text gap beside the initial', /measureDropCapReservation/.test(bookPaginationSource) && /measureNormalTextGap\(fontPx\)/.test(bookPaginationSource)],
|
['drop-cap reservation keeps a normal text gap beside the initial', /measureDropCapReservation/.test(bookPaginationSource) && /measureNormalTextGap\(fontPx\)/.test(bookPaginationSource)],
|
||||||
['drop-cap reservation uses both ink bounds and font advance width', /const advanceWidth = metrics\.width \|\| 0/.test(bookPaginationSource) && /Math\.max\(inkRight, advanceWidth, lineHeightPx \* 1\.08\)/.test(bookPaginationSource)],
|
['drop-cap reservation uses both ink bounds and font advance width', /const advanceWidth = metrics\.width \|\| 0/.test(bookPaginationSource) && /Math\.max\(inkRight, advanceWidth, lineHeightPx \* 1\.08\)/.test(bookPaginationSource)],
|
||||||
@@ -176,7 +180,7 @@ const checks = [
|
|||||||
['texture renderer draws title page and page numbers from versioned page metadata', /drawTitlePage/.test(textureRendererSource) && /game_title/.test(textureRendererSource) && /drawPageNumber/.test(textureRendererSource) && /pageMeta: this\.buildPublishPageMeta\(sidesToPublish\)/.test(textureRendererSource)],
|
['texture renderer draws title page and page numbers from versioned page metadata', /drawTitlePage/.test(textureRendererSource) && /game_title/.test(textureRendererSource) && /drawPageNumber/.test(textureRendererSource) && /pageMeta: this\.buildPublishPageMeta\(sidesToPublish\)/.test(textureRendererSource)],
|
||||||
['texture renderer uses plural page margin metrics for page numbers', /this\.metrics\.margins\.bottom/.test(textureRendererSource) && !/this\.metrics\.margin\.bottom/.test(textureRendererSource)],
|
['texture renderer uses plural page margin metrics for page numbers', /this\.metrics\.margins\.bottom/.test(textureRendererSource) && !/this\.metrics\.margin\.bottom/.test(textureRendererSource)],
|
||||||
['webgl flip borrows resident page texture and blanks right stack before forward animation', /prepareStaticPageForFlip/.test(source) && /materials\.flipPageSurface\.map = sourceTexture/.test(source) && /materials\.rightPage\.map = blankTexture/.test(source) && /webgl-book:page-flip-near-end/.test(source)],
|
['webgl flip borrows resident page texture and blanks right stack before forward animation', /prepareStaticPageForFlip/.test(source) && /materials\.flipPageSurface\.map = sourceTexture/.test(source) && /materials\.rightPage\.map = blankTexture/.test(source) && /webgl-book:page-flip-near-end/.test(source)],
|
||||||
['webgl flip never falls back to the opposite visible stack for target back texture', /const backTexture = getResidentPageTexture\(targetBackPageIndex\) \|\| getBlankPageTexture\(\)/.test(source) && !/oppositeMaterial\?\.map/.test(methodBody(source, 'prepareStaticPageForFlip'))],
|
['webgl flip never falls back to the opposite visible stack for target back texture', /const residentBackTexture = getResidentPageTexture\(targetBackPageIndex\)/.test(source) && /const backTexture = residentBackTexture \|\| getBlankPageTexture\(\)/.test(source) && !/oppositeMaterial\?\.map/.test(methodBody(source, 'prepareStaticPageForFlip'))],
|
||||||
['webgl page canvas metadata accepts explicit blank sides instead of retaining stale pages', /hasLeftMeta/.test(source) && /hasRightMeta/.test(source) && /Object\.prototype\.hasOwnProperty\.call\(detail\.pageMeta, 'right'\)/.test(source)],
|
['webgl page canvas metadata accepts explicit blank sides instead of retaining stale pages', /hasLeftMeta/.test(source) && /hasRightMeta/.test(source) && /Object\.prototype\.hasOwnProperty\.call\(detail\.pageMeta, 'right'\)/.test(source)],
|
||||||
['texture renderer publishes both spread sides for reveal preparation and fallback start', /this\.drawSpread\(this\.currentSpread \|\| this\.pagination\?\.getCurrentSpread\?\.\(\), \['left', 'right'\]\)/.test(textureRendererSource) && /const sides = \['left', 'right'\]/.test(textureRendererSource) && /pageMeta: prepared\.pageMeta \|\| \{\}/.test(textureRendererSource)],
|
['texture renderer publishes both spread sides for reveal preparation and fallback start', /this\.drawSpread\(this\.currentSpread \|\| this\.pagination\?\.getCurrentSpread\?\.\(\), \['left', 'right'\]\)/.test(textureRendererSource) && /const sides = \['left', 'right'\]/.test(textureRendererSource) && /pageMeta: prepared\.pageMeta \|\| \{\}/.test(textureRendererSource)],
|
||||||
['texture renderer marks page canvases with content versions before cache writes', /pageContentVersions/.test(textureRendererSource) && /buildPublishPageMeta/.test(textureRendererSource) && /completenessScore: \(maxBlockId \* 1000\) \+ lineCount/.test(textureRendererSource)],
|
['texture renderer marks page canvases with content versions before cache writes', /pageContentVersions/.test(textureRendererSource) && /buildPublishPageMeta/.test(textureRendererSource) && /completenessScore: \(maxBlockId \* 1000\) \+ lineCount/.test(textureRendererSource)],
|
||||||
@@ -185,8 +189,9 @@ 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 scene targets 60fps with browser-frame scheduling and live mirror refresh', /const targetFrameDurationMs = 1000 \/ 60/.test(source) && /this\.targetFrameDurationMs = 1000 \/ 60/.test(textureRendererSource) && /requestAnimationFrame\(animate\)/.test(source) && /const refreshStaticSceneBuffers = 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 debug exposes runtime invariants for visual regression tests', /getRuntimeInvariants\(\)/.test(source) && /residentPageTextureCount/.test(source) && /flipFrontBackShareMaterial/.test(source) && /mirrorRefreshesEveryFrame: true/.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)],
|
||||||
['book pagination reloads to the continuation block spread when unrendered history exists', /getContinuationBlockId/.test(bookPaginationSource) && /const continuationBlockId = this\.getContinuationBlockId\(latestBlockId, latestRenderedBlockId\)/.test(bookPaginationSource) && /const continuationSpreadIndex = this\.findSpreadIndexForBlock\(continuationBlockId\)/.test(bookPaginationSource) && /rendered < latest \? rendered \+ 1 : latest/.test(bookPaginationSource)],
|
['book pagination reloads to the continuation block spread when unrendered history exists', /getContinuationBlockId/.test(bookPaginationSource) && /const continuationBlockId = this\.getContinuationBlockId\(latestBlockId, latestRenderedBlockId\)/.test(bookPaginationSource) && /const continuationSpreadIndex = this\.findSpreadIndexForBlock\(continuationBlockId\)/.test(bookPaginationSource) && /rendered < latest \? rendered \+ 1 : latest/.test(bookPaginationSource)],
|
||||||
['webgl page navigation is page-count based with explicit spread mapping', /function pageToSpreadIndex/.test(source) && /Math\.floor\(page \/ 2\) \+ 1/.test(source) && /function spreadIndexToPagePosition/.test(source) && /\(spread - 1\) \* 2/.test(source)],
|
['webgl page navigation is page-count based with explicit spread mapping', /function pageToSpreadIndex/.test(source) && /Math\.floor\(page \/ 2\) \+ 1/.test(source) && /function spreadIndexToPagePosition/.test(source) && /\(spread - 1\) \* 2/.test(source)],
|
||||||
['webgl reading progress sync does not rebuild pagination as a page-count change', /function syncReadingProgressToCurrentPage/.test(source) && !/notifyBookPageCountChanged/.test(methodBody(source, 'syncReadingProgressToCurrentPage'))],
|
['webgl reading progress sync does not rebuild pagination as a page-count change', /function syncReadingProgressToCurrentPage/.test(source) && !/notifyBookPageCountChanged/.test(methodBody(source, 'syncReadingProgressToCurrentPage'))],
|
||||||
@@ -197,6 +202,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 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