Add shader page reveal checkpoint
This commit is contained in:
@@ -27,6 +27,9 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.currentSpread = null;
|
||||
this.activeAnimations = new Map();
|
||||
this.revealedBlockIds = new Set();
|
||||
this.pendingRevealBlockIds = new Set();
|
||||
this.revealBounds = null;
|
||||
this.revealPublishBlockIds = null;
|
||||
this.animationFrameId = null;
|
||||
this.lastAnimationFrameAt = 0;
|
||||
this.targetFrameDurationMs = 1000 / 30;
|
||||
@@ -40,6 +43,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
'drawPageLines',
|
||||
'drawLine',
|
||||
'drawWord',
|
||||
'recordRevealRect',
|
||||
'getPageContent',
|
||||
'buildLineSegments',
|
||||
'startRevealAnimation',
|
||||
@@ -70,10 +74,18 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.addEventListener(document, 'webgl-book:page-count-changed', this.handlePageCountChanged);
|
||||
this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady);
|
||||
this.addEventListener(document, 'book-pagination:spread-updated', (event) => {
|
||||
const spread = event.detail?.spread || this.pagination?.getCurrentSpread?.();
|
||||
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.currentSpread = spread || { left: [], right: [] };
|
||||
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
||||
this.markPendingReveal(latestBlockId);
|
||||
const pendingSides = this.getBlockSides(latestBlockId);
|
||||
const immediateSides = ['left', 'right'].filter(side => !pendingSides.includes(side));
|
||||
if (immediateSides.length) this.drawSpread(this.currentSpread, immediateSides);
|
||||
return;
|
||||
}
|
||||
this.drawSpread(this.currentSpread);
|
||||
});
|
||||
this.addEventListener(document, 'book-texture:reveal-block', (event) => {
|
||||
this.startRevealAnimation(event.detail || {});
|
||||
@@ -108,12 +120,15 @@ class BookTextureRendererModule extends BaseModule {
|
||||
drawSpread(spread = null, sides = null) {
|
||||
this.currentSpread = spread || { left: [], right: [] };
|
||||
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||
this.revealBounds = { left: null, right: null };
|
||||
sidesToDraw.forEach((side) => {
|
||||
if (!this.canvases[side]) return;
|
||||
this.drawPageBase(side);
|
||||
this.drawPageLines(side, this.currentSpread?.[side] || []);
|
||||
});
|
||||
this.publishSpread(sidesToDraw);
|
||||
this.revealBounds = null;
|
||||
this.revealPublishBlockIds = null;
|
||||
}
|
||||
|
||||
drawPageBase(side) {
|
||||
@@ -181,26 +196,20 @@ class BookTextureRendererModule extends BaseModule {
|
||||
ctx.font = `${fontStyle}${smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||
if (lineRecord.dropCapText) {
|
||||
ctx.save();
|
||||
const alpha = this.getWordAlpha(lineRecord, 0);
|
||||
if (alpha <= 0) {
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.globalAlpha *= alpha;
|
||||
ctx.font = `${Math.round(fontPx * 2.68)}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(
|
||||
String(lineRecord.dropCapText),
|
||||
content.x,
|
||||
content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) + (fontPx * 0.25)
|
||||
);
|
||||
ctx.restore();
|
||||
}
|
||||
const dropCapFontPx = Math.round(fontPx * 2.68);
|
||||
const dropCapX = content.x;
|
||||
const dropCapY = content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) + (fontPx * 0.25);
|
||||
ctx.font = `${dropCapFontPx}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(String(lineRecord.dropCapText), dropCapX, dropCapY);
|
||||
this.recordRevealRect(side, lineRecord, dropCapX, dropCapY, fontPx * 2.9, dropCapFontPx * 0.9);
|
||||
ctx.restore();
|
||||
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = smallCaps ? 'all-small-caps' : 'normal';
|
||||
if ('letterSpacing' in ctx) ctx.letterSpacing = smallCaps ? `${fontPx * 0.012}px` : '0px';
|
||||
ctx.font = `${fontStyle}${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);
|
||||
this.drawWord(ctx, segment.value, x + segment.x, baseY, lineRecord, segment.wordIndex, side, fontPx, lineHeightPx);
|
||||
});
|
||||
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = previousVariantCaps || 'normal';
|
||||
if ('letterSpacing' in ctx) ctx.letterSpacing = previousLetterSpacing || '0px';
|
||||
@@ -262,33 +271,36 @@ class BookTextureRendererModule extends BaseModule {
|
||||
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;
|
||||
drawWord(ctx, value, x, baseY, lineRecord, localWordIndex, side, fontPx, lineHeightPx) {
|
||||
ctx.fillText(value, x, baseY);
|
||||
ctx.globalAlpha = previousAlpha;
|
||||
const width = ctx.measureText(value).width || fontPx;
|
||||
this.recordRevealRect(side, lineRecord, x, baseY - fontPx, width, lineHeightPx, localWordIndex);
|
||||
}
|
||||
|
||||
getWordAlpha(lineRecord, localWordIndex) {
|
||||
const animation = this.activeAnimations.get(String(lineRecord.blockId ?? ''));
|
||||
if (!animation) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const globalWordIndex = Number(lineRecord.blockWordStart || 0) + localWordIndex;
|
||||
const timing = animation.wordTimings[globalWordIndex];
|
||||
if (!timing) {
|
||||
return animation.completed ? 1 : 0;
|
||||
}
|
||||
|
||||
const elapsed = animation.completed
|
||||
? Number.POSITIVE_INFINITY
|
||||
: 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));
|
||||
return progress;
|
||||
recordRevealRect(side, lineRecord, x, y, width, height, localWordIndex = 0) {
|
||||
if (!this.revealBounds || !this.revealPublishBlockIds) return;
|
||||
const blockId = String(lineRecord?.blockId ?? '');
|
||||
if (!blockId || !this.revealPublishBlockIds.has(blockId)) return;
|
||||
const animation = this.activeAnimations.get(blockId);
|
||||
if (!animation || animation.completed) return;
|
||||
const padding = Math.max(2, Number(lineRecord.fontPx || 18) * 0.12);
|
||||
const nextRect = {
|
||||
x: Math.max(0, x - padding),
|
||||
y: Math.max(0, y - padding),
|
||||
right: Math.min(this.metrics.width, x + width + padding),
|
||||
bottom: Math.min(this.metrics.height, y + height + padding)
|
||||
};
|
||||
const current = this.revealBounds[side];
|
||||
this.revealBounds[side] = current ? {
|
||||
x: Math.min(current.x, nextRect.x),
|
||||
y: Math.min(current.y, nextRect.y),
|
||||
right: Math.max(current.right, nextRect.right),
|
||||
bottom: Math.max(current.bottom, nextRect.bottom),
|
||||
blockIds: current.blockIds.add(blockId)
|
||||
} : {
|
||||
...nextRect,
|
||||
blockIds: new Set([blockId])
|
||||
};
|
||||
}
|
||||
|
||||
startRevealAnimation(detail = {}) {
|
||||
@@ -298,8 +310,14 @@ class BookTextureRendererModule extends BaseModule {
|
||||
blockId,
|
||||
wordTimings: detail.wordTimings,
|
||||
startedAt: performance.now(),
|
||||
totalDuration: Math.max(
|
||||
Number(detail.totalDuration || 0),
|
||||
...detail.wordTimings.map(timing => Number(timing.delay || 0) + Number(timing.duration || 0))
|
||||
),
|
||||
completed: false
|
||||
});
|
||||
this.pendingRevealBlockIds.delete(String(blockId));
|
||||
this.revealPublishBlockIds = new Set([String(blockId)]);
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
|
||||
this.requestAnimationFrame();
|
||||
}
|
||||
@@ -314,12 +332,14 @@ class BookTextureRendererModule extends BaseModule {
|
||||
}
|
||||
});
|
||||
if (changed) {
|
||||
this.pendingRevealBlockIds.clear();
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getAnimatedSides(true));
|
||||
}
|
||||
}
|
||||
|
||||
stopAnimations() {
|
||||
this.activeAnimations.clear();
|
||||
this.pendingRevealBlockIds.clear();
|
||||
if (this.animationFrameId) {
|
||||
clearTimeout(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
@@ -352,12 +372,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
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
|
||||
});
|
||||
this.pendingRevealBlockIds.add(id);
|
||||
}
|
||||
|
||||
requestAnimationFrame() {
|
||||
@@ -387,7 +402,6 @@ class BookTextureRendererModule extends BaseModule {
|
||||
hasActive = true;
|
||||
}
|
||||
});
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getAnimatedSides(true));
|
||||
if (hasActive) this.requestAnimationFrame();
|
||||
}
|
||||
|
||||
@@ -399,6 +413,28 @@ class BookTextureRendererModule extends BaseModule {
|
||||
};
|
||||
if (sidesToPublish.includes('left')) detail.left = this.canvases.left;
|
||||
if (sidesToPublish.includes('right')) detail.right = this.canvases.right;
|
||||
const reveal = {};
|
||||
sidesToPublish.forEach((side) => {
|
||||
const bounds = this.revealBounds?.[side];
|
||||
if (!bounds) return;
|
||||
const blockIds = Array.from(bounds.blockIds || []);
|
||||
const durationMs = blockIds.reduce((maxDuration, blockId) => {
|
||||
const animation = this.activeAnimations.get(String(blockId));
|
||||
return Math.max(maxDuration, Number(animation?.totalDuration || 0));
|
||||
}, 0);
|
||||
if (durationMs <= 0) return;
|
||||
reveal[side] = {
|
||||
blockIds,
|
||||
durationMs,
|
||||
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;
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
||||
detail
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user