Fix WebGL reveal timing and flip prewarm
This commit is contained in:
@@ -65,8 +65,10 @@ class BookTextureRendererModule extends BaseModule {
|
||||
'buildRevealRegions',
|
||||
'collectRevealRegionCandidates',
|
||||
'createRevealRegionForLine',
|
||||
'assignRevealTiming',
|
||||
'getLineInkRect',
|
||||
'getLineNaturalWidth',
|
||||
'getLineWordCount',
|
||||
'getImageRevealDurationMs',
|
||||
'getInlineStyleState',
|
||||
'updateInlineStyleState',
|
||||
@@ -76,6 +78,8 @@ class BookTextureRendererModule extends BaseModule {
|
||||
'buildLineSegments',
|
||||
'startRevealAnimation',
|
||||
'prepareRevealBlock',
|
||||
'preloadAdditionalRevealSpreads',
|
||||
'spreadContainsBlock',
|
||||
'hasPreparedRevealBlock',
|
||||
'createAnimationState',
|
||||
'publishPreparedReveal',
|
||||
@@ -118,6 +122,11 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.currentSpread = spread || { left: [], right: [] };
|
||||
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
||||
this.markPendingReveal(latestBlockId);
|
||||
const id = String(latestBlockId);
|
||||
if (this.activeAnimations.has(id)) {
|
||||
this.revealPublishBlockIds = new Set([id]);
|
||||
this.drawSpread(this.currentSpread, ['left', 'right']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.drawSpread(this.currentSpread);
|
||||
@@ -591,34 +600,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
byBlock.forEach((blockRegions, blockId) => {
|
||||
const animation = this.activeAnimations.get(blockId);
|
||||
if (!animation || animation.completed) return;
|
||||
const fixedRegions = blockRegions.filter(region => region.fixedDurationMs > 0);
|
||||
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);
|
||||
});
|
||||
regions.push(...this.assignRevealTiming(blockRegions, animation));
|
||||
});
|
||||
const sideRegions = regions.filter(region => region.side === side);
|
||||
if (!sideRegions.length) return null;
|
||||
@@ -635,7 +617,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
});
|
||||
return {
|
||||
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,
|
||||
lineRects: sideRegions.map(region => ({
|
||||
blockId: region.blockId,
|
||||
@@ -664,6 +646,67 @@ class BookTextureRendererModule extends BaseModule {
|
||||
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 = {}) {
|
||||
const blockId = String(lineRecord?.blockId ?? '');
|
||||
if (!blockId || !this.revealPublishBlockIds.has(blockId)) return null;
|
||||
@@ -680,10 +723,12 @@ class BookTextureRendererModule extends BaseModule {
|
||||
}
|
||||
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);
|
||||
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 left = Math.max(0, x - padding);
|
||||
const top = Math.max(0, y - padding);
|
||||
@@ -696,6 +741,8 @@ class BookTextureRendererModule extends BaseModule {
|
||||
blockId,
|
||||
lineIndex: Number(lineRecord.lineIndex ?? lineRecord.pageLine ?? 0),
|
||||
fixedDurationMs,
|
||||
wordStart,
|
||||
wordEnd,
|
||||
area: rectWidth * rectHeight,
|
||||
pixelRect: { x: left, y: top, right, bottom },
|
||||
rect: {
|
||||
@@ -743,6 +790,25 @@ class BookTextureRendererModule extends BaseModule {
|
||||
}, 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 = {}) {
|
||||
const metadata = lineRecord.metadata || {};
|
||||
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 sides = ['left', 'right'];
|
||||
const published = this.drawSpread(spread, sides, { preloadOnly });
|
||||
if (!preloadOnly) this.preloadAdditionalRevealSpreads(id, spread);
|
||||
if (preloadOnly && published) {
|
||||
this.preparedRevealCache.set(id, {
|
||||
...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) {
|
||||
const id = String(blockId ?? '');
|
||||
return Boolean(id && this.preparedRevealCache.has(id));
|
||||
|
||||
Reference in New Issue
Block a user