Optimize WebGL book texture reveal
This commit is contained in:
@@ -26,6 +26,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
};
|
||||
this.currentSpread = null;
|
||||
this.activeAnimations = new Map();
|
||||
this.revealedBlockIds = new Set();
|
||||
this.animationFrameId = null;
|
||||
this.lastAnimationFrameAt = 0;
|
||||
this.targetFrameDurationMs = 1000 / 30;
|
||||
@@ -39,9 +40,13 @@ class BookTextureRendererModule extends BaseModule {
|
||||
'drawPageLines',
|
||||
'drawLine',
|
||||
'drawWord',
|
||||
'buildLineSegments',
|
||||
'startRevealAnimation',
|
||||
'fastForwardAnimations',
|
||||
'stopAnimations',
|
||||
'getBlockSides',
|
||||
'getAnimatedSides',
|
||||
'markPendingReveal',
|
||||
'requestAnimationFrame',
|
||||
'tickAnimations',
|
||||
'publishSpread',
|
||||
@@ -60,6 +65,9 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.drawEmptySpread();
|
||||
this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady);
|
||||
this.addEventListener(document, 'book-pagination:spread-updated', (event) => {
|
||||
const latestBlockId = event.detail?.latestBlockId;
|
||||
const latestRenderedBlockId = Math.max(0, Number(event.detail?.latestRenderedBlockId || 0));
|
||||
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) this.markPendingReveal(latestBlockId);
|
||||
this.drawSpread(event.detail?.spread || this.pagination?.getCurrentSpread?.());
|
||||
});
|
||||
this.addEventListener(document, 'book-texture:reveal-block', (event) => {
|
||||
@@ -92,13 +100,15 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.publishSpread();
|
||||
}
|
||||
|
||||
drawSpread(spread = null) {
|
||||
drawSpread(spread = null, sides = null) {
|
||||
this.currentSpread = spread || { left: [], right: [] };
|
||||
this.drawPageBase('left');
|
||||
this.drawPageBase('right');
|
||||
this.drawPageLines('left', this.currentSpread?.left || []);
|
||||
this.drawPageLines('right', this.currentSpread?.right || []);
|
||||
this.publishSpread();
|
||||
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||
sidesToDraw.forEach((side) => {
|
||||
if (!this.canvases[side]) return;
|
||||
this.drawPageBase(side);
|
||||
this.drawPageLines(side, this.currentSpread?.[side] || []);
|
||||
});
|
||||
this.publishSpread(sidesToDraw);
|
||||
}
|
||||
|
||||
drawPageBase(side) {
|
||||
@@ -158,51 +168,96 @@ class BookTextureRendererModule extends BaseModule {
|
||||
let x = metrics.content.x + centerOffset;
|
||||
let wordIndex = 0;
|
||||
|
||||
ctx.font = `${fontStyle}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||
ctx.font = `${fontStyle}${lineRecord.smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||
if (lineRecord.dropCapText) {
|
||||
ctx.save();
|
||||
ctx.font = `${Math.round(lineHeightPx * 2.08)}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(
|
||||
String(lineRecord.dropCapText),
|
||||
metrics.content.x,
|
||||
metrics.content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) - (lineHeightPx * 0.08)
|
||||
);
|
||||
ctx.restore();
|
||||
ctx.font = `${fontStyle}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||
const alpha = this.getWordAlpha(lineRecord, 0);
|
||||
if (alpha <= 0) {
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.globalAlpha *= alpha;
|
||||
ctx.font = `${Math.round(lineHeightPx * 2.14)}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(
|
||||
String(lineRecord.dropCapText),
|
||||
metrics.content.x,
|
||||
metrics.content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) - (lineHeightPx * 0.05)
|
||||
);
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.font = `${fontStyle}${lineRecord.smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||
}
|
||||
this.buildLineSegments(ctx, nodes, line, ratio).forEach((segment) => {
|
||||
this.drawWord(ctx, segment.value, x + segment.x, baseY, lineRecord, segment.wordIndex);
|
||||
});
|
||||
}
|
||||
|
||||
buildLineSegments(ctx, nodes = [], line = {}, ratio = 0) {
|
||||
const segments = [];
|
||||
let x = 0;
|
||||
let currentSegment = null;
|
||||
let previousWasGlue = true;
|
||||
|
||||
nodes.forEach((node, index) => {
|
||||
if (!node) return;
|
||||
if (node.type === 'box' && node.value) {
|
||||
const nextNode = nodes[index + 1];
|
||||
const value = `${node.value}${nextNode?.type === 'penalty' && nextNode.penalty === 100 ? '-' : ''}`;
|
||||
this.drawWord(ctx, value, x, baseY, lineRecord, wordIndex);
|
||||
x += Number(node.width || ctx.measureText(value).width || 0);
|
||||
wordIndex += 1;
|
||||
const value = String(node.value);
|
||||
const width = Number(node.width || ctx.measureText(value).width || 0);
|
||||
if (currentSegment && !previousWasGlue) {
|
||||
currentSegment.value += value;
|
||||
currentSegment.width += width;
|
||||
} else {
|
||||
currentSegment = {
|
||||
value,
|
||||
x,
|
||||
width,
|
||||
wordIndex: segments.length
|
||||
};
|
||||
segments.push(currentSegment);
|
||||
}
|
||||
x += width;
|
||||
previousWasGlue = false;
|
||||
} else if (node.type === 'glue' && node.width !== 0) {
|
||||
let width = Number(node.width || 0);
|
||||
if (ratio > 0) width += Number(node.stretch || 0) * ratio;
|
||||
if (ratio < 0) width += Number(node.shrink || 0) * ratio;
|
||||
x += width;
|
||||
previousWasGlue = true;
|
||||
currentSegment = null;
|
||||
} else if (node.type === 'penalty' && node.penalty === 100) {
|
||||
const isLineEndHyphen = Boolean(line.hyphenated && index === nodes.length - 1 && currentSegment);
|
||||
if (isLineEndHyphen) {
|
||||
const hyphenWidth = Number(node.width || ctx.measureText('-').width || 0);
|
||||
currentSegment.value += '-';
|
||||
currentSegment.width += hyphenWidth;
|
||||
x += hyphenWidth;
|
||||
}
|
||||
previousWasGlue = false;
|
||||
}
|
||||
});
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
drawWord(ctx, value, x, baseY, lineRecord, localWordIndex) {
|
||||
const alpha = this.getWordAlpha(lineRecord, localWordIndex);
|
||||
if (alpha <= 0) return;
|
||||
const previousAlpha = ctx.globalAlpha;
|
||||
ctx.globalAlpha = previousAlpha * alpha;
|
||||
ctx.fillText(value, x, baseY);
|
||||
ctx.globalAlpha = previousAlpha;
|
||||
}
|
||||
|
||||
getWordAlpha(lineRecord, localWordIndex) {
|
||||
const animation = this.activeAnimations.get(String(lineRecord.blockId ?? ''));
|
||||
if (!animation) {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillText(value, x, baseY);
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const globalWordIndex = Number(lineRecord.blockWordStart || 0) + localWordIndex;
|
||||
const timing = animation.wordTimings[globalWordIndex];
|
||||
if (!timing) {
|
||||
ctx.globalAlpha = animation.completed ? 1 : 0;
|
||||
ctx.fillText(value, x, baseY);
|
||||
ctx.globalAlpha = 1;
|
||||
return;
|
||||
return animation.completed ? 1 : 0;
|
||||
}
|
||||
|
||||
const elapsed = animation.completed
|
||||
@@ -210,12 +265,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
: performance.now() - animation.startedAt;
|
||||
const duration = Math.max(1, Number(timing.duration || 1));
|
||||
const progress = Math.max(0, Math.min(1, (elapsed - Number(timing.delay || 0)) / duration));
|
||||
if (progress <= 0) return;
|
||||
|
||||
const previousAlpha = ctx.globalAlpha;
|
||||
ctx.globalAlpha = previousAlpha * progress;
|
||||
ctx.fillText(value, x, baseY);
|
||||
ctx.globalAlpha = previousAlpha;
|
||||
return progress;
|
||||
}
|
||||
|
||||
startRevealAnimation(detail = {}) {
|
||||
@@ -227,6 +277,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
startedAt: performance.now(),
|
||||
completed: false
|
||||
});
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
|
||||
this.requestAnimationFrame();
|
||||
}
|
||||
|
||||
@@ -235,11 +286,12 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.activeAnimations.forEach((animation) => {
|
||||
if (!animation.completed) {
|
||||
animation.completed = true;
|
||||
this.revealedBlockIds.add(String(animation.blockId ?? ''));
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
if (changed) {
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.());
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getAnimatedSides(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +304,39 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.());
|
||||
}
|
||||
|
||||
getBlockSides(blockId) {
|
||||
const id = String(blockId ?? '');
|
||||
const spread = this.currentSpread || this.pagination?.getCurrentSpread?.() || { left: [], right: [] };
|
||||
return ['left', 'right'].filter((side) => {
|
||||
const lines = Array.isArray(spread?.[side]) ? spread[side] : [];
|
||||
return lines.some(line => String(line?.blockId ?? '') === id);
|
||||
});
|
||||
}
|
||||
|
||||
getAnimatedSides(includeCompleted = false) {
|
||||
const spread = this.currentSpread || this.pagination?.getCurrentSpread?.() || { left: [], right: [] };
|
||||
const activeBlockIds = new Set();
|
||||
this.activeAnimations.forEach((animation, blockId) => {
|
||||
if (includeCompleted || !animation.completed) activeBlockIds.add(String(blockId));
|
||||
});
|
||||
const sides = ['left', 'right'].filter((side) => {
|
||||
const lines = Array.isArray(spread?.[side]) ? spread[side] : [];
|
||||
return lines.some(line => activeBlockIds.has(String(line?.blockId ?? '')));
|
||||
});
|
||||
return sides.length ? sides : ['left', 'right'];
|
||||
}
|
||||
|
||||
markPendingReveal(blockId) {
|
||||
const id = String(blockId ?? '');
|
||||
if (!id || this.activeAnimations.has(id) || this.revealedBlockIds.has(id)) return;
|
||||
this.activeAnimations.set(id, {
|
||||
blockId,
|
||||
wordTimings: [],
|
||||
startedAt: performance.now(),
|
||||
completed: false
|
||||
});
|
||||
}
|
||||
|
||||
requestAnimationFrame() {
|
||||
if (this.animationFrameId) return;
|
||||
this.animationFrameId = window.setTimeout(() => this.tickAnimations(performance.now()), this.targetFrameDurationMs);
|
||||
@@ -269,26 +354,30 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const currentNow = performance.now();
|
||||
this.activeAnimations.forEach((animation) => {
|
||||
if (animation.completed) return;
|
||||
if (!Array.isArray(animation.wordTimings) || animation.wordTimings.length === 0) return;
|
||||
const lastTiming = animation.wordTimings.at(-1);
|
||||
const total = Number(lastTiming?.delay || 0) + Number(lastTiming?.duration || 0);
|
||||
if (currentNow - animation.startedAt >= total + 50) {
|
||||
animation.completed = true;
|
||||
this.revealedBlockIds.add(String(animation.blockId ?? ''));
|
||||
} else {
|
||||
hasActive = true;
|
||||
}
|
||||
});
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.());
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getAnimatedSides(true));
|
||||
if (hasActive) this.requestAnimationFrame();
|
||||
}
|
||||
|
||||
publishSpread() {
|
||||
publishSpread(sides = null) {
|
||||
const sidesToPublish = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||
const detail = {
|
||||
metrics: this.metrics,
|
||||
hitMaps: this.hitMaps
|
||||
};
|
||||
if (sidesToPublish.includes('left')) detail.left = this.canvases.left;
|
||||
if (sidesToPublish.includes('right')) detail.right = this.canvases.right;
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
||||
detail: {
|
||||
left: this.canvases.left,
|
||||
right: this.canvases.right,
|
||||
metrics: this.metrics,
|
||||
hitMaps: this.hitMaps
|
||||
}
|
||||
detail
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user