Queue WebGL book reveal masks
This commit is contained in:
@@ -29,6 +29,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.revealedBlockIds = new Set();
|
||||
this.pendingRevealBlockIds = new Set();
|
||||
this.revealBounds = null;
|
||||
this.revealWords = null;
|
||||
this.revealPublishBlockIds = null;
|
||||
this.animationFrameId = null;
|
||||
this.lastAnimationFrameAt = 0;
|
||||
@@ -47,6 +48,8 @@ class BookTextureRendererModule extends BaseModule {
|
||||
'getPageContent',
|
||||
'buildLineSegments',
|
||||
'startRevealAnimation',
|
||||
'prepareRevealBlock',
|
||||
'startPreparedRevealAnimation',
|
||||
'fastForwardAnimations',
|
||||
'stopAnimations',
|
||||
'getBlockSides',
|
||||
@@ -90,6 +93,9 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.addEventListener(document, 'book-texture:reveal-block', (event) => {
|
||||
this.startRevealAnimation(event.detail || {});
|
||||
});
|
||||
this.addEventListener(document, 'book-texture:prepare-reveal-block', (event) => {
|
||||
this.prepareRevealBlock(event.detail || {});
|
||||
});
|
||||
this.addEventListener(document, 'book-texture:fast-forward', this.fastForwardAnimations);
|
||||
this.addEventListener(document, 'ui:command', (event) => {
|
||||
if (event.detail?.type === 'continue') this.fastForwardAnimations();
|
||||
@@ -121,6 +127,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.currentSpread = spread || { left: [], right: [] };
|
||||
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||
this.revealBounds = { left: null, right: null };
|
||||
this.revealWords = { left: [], right: [] };
|
||||
sidesToDraw.forEach((side) => {
|
||||
if (!this.canvases[side]) return;
|
||||
this.drawPageBase(side);
|
||||
@@ -128,6 +135,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
});
|
||||
this.publishSpread(sidesToDraw);
|
||||
this.revealBounds = null;
|
||||
this.revealWords = null;
|
||||
this.revealPublishBlockIds = null;
|
||||
}
|
||||
|
||||
@@ -202,7 +210,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
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);
|
||||
this.recordRevealRect(side, lineRecord, dropCapX, dropCapY, fontPx * 2.9, dropCapFontPx * 0.9, 0);
|
||||
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';
|
||||
@@ -301,11 +309,33 @@ class BookTextureRendererModule extends BaseModule {
|
||||
...nextRect,
|
||||
blockIds: new Set([blockId])
|
||||
};
|
||||
const globalWordIndex = Math.max(0, Number(lineRecord.blockWordStart || 0) + Number(localWordIndex || 0));
|
||||
const timing = Array.isArray(animation.wordTimings) ? animation.wordTimings[globalWordIndex] : null;
|
||||
if (!timing || !this.revealWords?.[side]) return;
|
||||
this.revealWords[side].push({
|
||||
blockId,
|
||||
wordIndex: globalWordIndex,
|
||||
rect: {
|
||||
x: nextRect.x / this.metrics.width,
|
||||
y: nextRect.y / this.metrics.height,
|
||||
width: Math.max(0.001, (nextRect.right - nextRect.x) / this.metrics.width),
|
||||
height: Math.max(0.001, (nextRect.bottom - nextRect.y) / this.metrics.height)
|
||||
},
|
||||
timing: {
|
||||
delay: Math.max(0, Number(timing.delay || 0)),
|
||||
duration: Math.max(1, Number(timing.duration || 1))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startRevealAnimation(detail = {}) {
|
||||
const blockId = detail.blockId ?? detail.id ?? null;
|
||||
if (blockId == null || !Array.isArray(detail.wordTimings)) return;
|
||||
const existing = this.activeAnimations.get(String(blockId));
|
||||
if (existing && existing.prepared) {
|
||||
this.startPreparedRevealAnimation(blockId);
|
||||
return;
|
||||
}
|
||||
this.activeAnimations.set(String(blockId), {
|
||||
blockId,
|
||||
wordTimings: detail.wordTimings,
|
||||
@@ -319,21 +349,69 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.pendingRevealBlockIds.delete(String(blockId));
|
||||
this.revealPublishBlockIds = new Set([String(blockId)]);
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-reveal-start', {
|
||||
detail: {
|
||||
blockId
|
||||
}
|
||||
}));
|
||||
this.requestAnimationFrame();
|
||||
}
|
||||
|
||||
prepareRevealBlock(detail = {}) {
|
||||
const blockId = detail.blockId ?? detail.id ?? null;
|
||||
if (blockId == null || !Array.isArray(detail.wordTimings)) return;
|
||||
const id = String(blockId);
|
||||
const wordTimings = detail.wordTimings;
|
||||
this.activeAnimations.set(id, {
|
||||
blockId,
|
||||
wordTimings,
|
||||
startedAt: null,
|
||||
totalDuration: Math.max(
|
||||
Number(detail.totalDuration || 0),
|
||||
...wordTimings.map(timing => Number(timing.delay || 0) + Number(timing.duration || 0))
|
||||
),
|
||||
completed: false,
|
||||
prepared: true
|
||||
});
|
||||
this.pendingRevealBlockIds.delete(id);
|
||||
this.revealPublishBlockIds = new Set([id]);
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
|
||||
}
|
||||
|
||||
startPreparedRevealAnimation(blockId) {
|
||||
const id = String(blockId ?? '');
|
||||
const animation = this.activeAnimations.get(id);
|
||||
if (!animation) return false;
|
||||
animation.startedAt = performance.now();
|
||||
animation.prepared = false;
|
||||
animation.completed = false;
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-reveal-start', {
|
||||
detail: {
|
||||
blockId: animation.blockId
|
||||
}
|
||||
}));
|
||||
this.requestAnimationFrame();
|
||||
return true;
|
||||
}
|
||||
|
||||
fastForwardAnimations() {
|
||||
let changed = false;
|
||||
const blockIds = [];
|
||||
this.activeAnimations.forEach((animation) => {
|
||||
if (!animation.completed) {
|
||||
animation.completed = true;
|
||||
this.revealedBlockIds.add(String(animation.blockId ?? ''));
|
||||
blockIds.push(animation.blockId);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
if (changed) {
|
||||
this.pendingRevealBlockIds.clear();
|
||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getAnimatedSides(true));
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-reveal-fast-forward', {
|
||||
detail: {
|
||||
blockIds
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +471,10 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.activeAnimations.forEach((animation) => {
|
||||
if (animation.completed) return;
|
||||
if (!Array.isArray(animation.wordTimings) || animation.wordTimings.length === 0) return;
|
||||
if (animation.startedAt == null) {
|
||||
hasActive = true;
|
||||
return;
|
||||
}
|
||||
const lastTiming = animation.wordTimings.at(-1);
|
||||
const total = Number(lastTiming?.delay || 0) + Number(lastTiming?.duration || 0);
|
||||
if (currentNow - animation.startedAt >= total + 50) {
|
||||
@@ -426,6 +508,12 @@ class BookTextureRendererModule extends BaseModule {
|
||||
reveal[side] = {
|
||||
blockIds,
|
||||
durationMs,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user