Optimize WebGL book texture reveal
This commit is contained in:
@@ -18,7 +18,7 @@ class BookPageFormatModule extends BaseModule {
|
|||||||
topIn: 0.46,
|
topIn: 0.46,
|
||||||
bottomIn: 0.58,
|
bottomIn: 0.58,
|
||||||
innerIn: 0.62,
|
innerIn: 0.62,
|
||||||
outerIn: 0.72
|
outerIn: 0.86
|
||||||
}),
|
}),
|
||||||
typography: Object.freeze({
|
typography: Object.freeze({
|
||||||
fontFamily: '"EB Garamond", "EB Garamond 12", serif',
|
fontFamily: '"EB Garamond", "EB Garamond 12", serif',
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class BookPaginationModule extends BaseModule {
|
|||||||
this.spreads = [];
|
this.spreads = [];
|
||||||
this.currentSpreadIndex = 0;
|
this.currentSpreadIndex = 0;
|
||||||
this.refreshToken = 0;
|
this.refreshToken = 0;
|
||||||
|
this.latestBlockId = 0;
|
||||||
|
this.latestRenderedBlockId = 0;
|
||||||
|
|
||||||
this.bindMethods([
|
this.bindMethods([
|
||||||
'initialize',
|
'initialize',
|
||||||
@@ -24,6 +26,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
'getDropCapText',
|
'getDropCapText',
|
||||||
'extractDropCapText',
|
'extractDropCapText',
|
||||||
'extractLines',
|
'extractLines',
|
||||||
|
'countLineWords',
|
||||||
'getLineGeometry',
|
'getLineGeometry',
|
||||||
'getSpread',
|
'getSpread',
|
||||||
'getCurrentSpread',
|
'getCurrentSpread',
|
||||||
@@ -57,12 +60,19 @@ class BookPaginationModule extends BaseModule {
|
|||||||
);
|
);
|
||||||
if (!gameId || latestBlockId <= 0 || typeof this.storyHistory?.getBlocksRange !== 'function') {
|
if (!gameId || latestBlockId <= 0 || typeof this.storyHistory?.getBlocksRange !== 'function') {
|
||||||
this.spreads = [];
|
this.spreads = [];
|
||||||
|
this.latestBlockId = 0;
|
||||||
|
this.latestRenderedBlockId = 0;
|
||||||
this.publish();
|
this.publish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocks = await this.storyHistory.getBlocksRange(gameId, 1, latestBlockId);
|
const blocks = await this.storyHistory.getBlocksRange(gameId, 1, latestBlockId);
|
||||||
if (token !== this.refreshToken) return;
|
if (token !== this.refreshToken) return;
|
||||||
|
this.latestBlockId = latestBlockId;
|
||||||
|
this.latestRenderedBlockId = Math.max(
|
||||||
|
0,
|
||||||
|
Number(detail.latestRenderedBlockId || this.storyHistory?.latestRenderedBlockId || 0)
|
||||||
|
);
|
||||||
this.spreads = this.buildSpreads(blocks);
|
this.spreads = this.buildSpreads(blocks);
|
||||||
this.currentSpreadIndex = Math.max(0, Math.min(this.currentSpreadIndex, Math.max(0, this.spreads.length - 1)));
|
this.currentSpreadIndex = Math.max(0, Math.min(this.currentSpreadIndex, Math.max(0, this.spreads.length - 1)));
|
||||||
this.publish();
|
this.publish();
|
||||||
@@ -84,7 +94,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
|
|
||||||
layout.lines.forEach((line, layoutLineIndex) => {
|
layout.lines.forEach((line, layoutLineIndex) => {
|
||||||
const geometry = this.getLineGeometry(cursorLine);
|
const geometry = this.getLineGeometry(cursorLine);
|
||||||
const lineWordCount = line.nodes.filter(node => node?.type === 'box' && node.value).length;
|
const lineWordCount = this.countLineWords(line);
|
||||||
if (!spreads[geometry.spreadIndex]) {
|
if (!spreads[geometry.spreadIndex]) {
|
||||||
spreads[geometry.spreadIndex] = { index: geometry.spreadIndex, left: [], right: [] };
|
spreads[geometry.spreadIndex] = { index: geometry.spreadIndex, left: [], right: [] };
|
||||||
}
|
}
|
||||||
@@ -100,7 +110,8 @@ class BookPaginationModule extends BaseModule {
|
|||||||
lineHeightPx: layout.lineHeightPx,
|
lineHeightPx: layout.lineHeightPx,
|
||||||
fontStyle: layout.fontStyle,
|
fontStyle: layout.fontStyle,
|
||||||
blockWordStart: blockWordCursor,
|
blockWordStart: blockWordCursor,
|
||||||
dropCapText: layoutLineIndex === 0 ? layout.dropCapText : ''
|
dropCapText: layoutLineIndex === 0 ? layout.dropCapText : '',
|
||||||
|
smallCaps: Boolean(layout.dropCap && layoutLineIndex === 0)
|
||||||
});
|
});
|
||||||
blockWordCursor += lineWordCount;
|
blockWordCursor += lineWordCount;
|
||||||
cursorLine += 1;
|
cursorLine += 1;
|
||||||
@@ -125,7 +136,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
const bottomSpaceLines = role === 'chapter-heading' || role === 'section-heading' ? 1 : 0;
|
const bottomSpaceLines = role === 'chapter-heading' || role === 'section-heading' ? 1 : 0;
|
||||||
const lineHeightPx = Math.max(1, Number(this.metrics.typographyLineHeightPx || 1));
|
const lineHeightPx = Math.max(1, Number(this.metrics.typographyLineHeightPx || 1));
|
||||||
const fontPx = Math.max(1, Number(this.metrics.bodyFontSizePx || lineHeightPx / 1.5));
|
const fontPx = Math.max(1, Number(this.metrics.bodyFontSizePx || lineHeightPx / 1.5));
|
||||||
const dropCapWidth = dropCap ? lineHeightPx * 1.58 : 0;
|
const dropCapWidth = dropCap ? lineHeightPx * 1.72 : 0;
|
||||||
const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace)
|
const indent = (isHeading || block.isFirstParagraphInChapter || block.metadata?.isFirstParagraphInChapter || block.addTopSpace)
|
||||||
? 0
|
? 0
|
||||||
: lineHeightPx * 1.5;
|
: lineHeightPx * 1.5;
|
||||||
@@ -195,12 +206,32 @@ class BookPaginationModule extends BaseModule {
|
|||||||
offset,
|
offset,
|
||||||
ratio: breaks[index].ratio || 0,
|
ratio: breaks[index].ratio || 0,
|
||||||
isFinal: index === breaks.length - 1,
|
isFinal: index === breaks.length - 1,
|
||||||
|
hyphenated: Boolean(lineNodes.at(-1)?.type === 'penalty' && lineNodes.at(-1)?.penalty === 100),
|
||||||
align: options.align || 'justify'
|
align: options.align || 'justify'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countLineWords(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;
|
||||||
|
}
|
||||||
|
|
||||||
getLineGeometry(globalLine) {
|
getLineGeometry(globalLine) {
|
||||||
const linesPerPage = Math.max(1, Math.floor(this.metrics.content.height / this.metrics.typographyLineHeightPx || 1));
|
const linesPerPage = Math.max(1, Math.floor(this.metrics.content.height / this.metrics.typographyLineHeightPx || 1));
|
||||||
const spreadLineCount = linesPerPage * 2;
|
const spreadLineCount = linesPerPage * 2;
|
||||||
@@ -233,7 +264,9 @@ class BookPaginationModule extends BaseModule {
|
|||||||
detail: {
|
detail: {
|
||||||
spread: this.getCurrentSpread(),
|
spread: this.getCurrentSpread(),
|
||||||
spreadIndex: this.currentSpreadIndex,
|
spreadIndex: this.currentSpreadIndex,
|
||||||
spreadCount: this.spreads.length
|
spreadCount: this.spreads.length,
|
||||||
|
latestBlockId: this.latestBlockId,
|
||||||
|
latestRenderedBlockId: this.latestRenderedBlockId
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
};
|
};
|
||||||
this.currentSpread = null;
|
this.currentSpread = null;
|
||||||
this.activeAnimations = new Map();
|
this.activeAnimations = new Map();
|
||||||
|
this.revealedBlockIds = new Set();
|
||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
this.lastAnimationFrameAt = 0;
|
this.lastAnimationFrameAt = 0;
|
||||||
this.targetFrameDurationMs = 1000 / 30;
|
this.targetFrameDurationMs = 1000 / 30;
|
||||||
@@ -39,9 +40,13 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'drawPageLines',
|
'drawPageLines',
|
||||||
'drawLine',
|
'drawLine',
|
||||||
'drawWord',
|
'drawWord',
|
||||||
|
'buildLineSegments',
|
||||||
'startRevealAnimation',
|
'startRevealAnimation',
|
||||||
'fastForwardAnimations',
|
'fastForwardAnimations',
|
||||||
'stopAnimations',
|
'stopAnimations',
|
||||||
|
'getBlockSides',
|
||||||
|
'getAnimatedSides',
|
||||||
|
'markPendingReveal',
|
||||||
'requestAnimationFrame',
|
'requestAnimationFrame',
|
||||||
'tickAnimations',
|
'tickAnimations',
|
||||||
'publishSpread',
|
'publishSpread',
|
||||||
@@ -60,6 +65,9 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.drawEmptySpread();
|
this.drawEmptySpread();
|
||||||
this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady);
|
this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady);
|
||||||
this.addEventListener(document, 'book-pagination:spread-updated', (event) => {
|
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.drawSpread(event.detail?.spread || this.pagination?.getCurrentSpread?.());
|
||||||
});
|
});
|
||||||
this.addEventListener(document, 'book-texture:reveal-block', (event) => {
|
this.addEventListener(document, 'book-texture:reveal-block', (event) => {
|
||||||
@@ -92,13 +100,15 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.publishSpread();
|
this.publishSpread();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawSpread(spread = null) {
|
drawSpread(spread = null, sides = null) {
|
||||||
this.currentSpread = spread || { left: [], right: [] };
|
this.currentSpread = spread || { left: [], right: [] };
|
||||||
this.drawPageBase('left');
|
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||||
this.drawPageBase('right');
|
sidesToDraw.forEach((side) => {
|
||||||
this.drawPageLines('left', this.currentSpread?.left || []);
|
if (!this.canvases[side]) return;
|
||||||
this.drawPageLines('right', this.currentSpread?.right || []);
|
this.drawPageBase(side);
|
||||||
this.publishSpread();
|
this.drawPageLines(side, this.currentSpread?.[side] || []);
|
||||||
|
});
|
||||||
|
this.publishSpread(sidesToDraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPageBase(side) {
|
drawPageBase(side) {
|
||||||
@@ -158,51 +168,96 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
let x = metrics.content.x + centerOffset;
|
let x = metrics.content.x + centerOffset;
|
||||||
let wordIndex = 0;
|
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) {
|
if (lineRecord.dropCapText) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = `${Math.round(lineHeightPx * 2.08)}px "EB Garamond Initials", ${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.textBaseline = 'top';
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
String(lineRecord.dropCapText),
|
String(lineRecord.dropCapText),
|
||||||
metrics.content.x,
|
metrics.content.x,
|
||||||
metrics.content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) - (lineHeightPx * 0.08)
|
metrics.content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) - (lineHeightPx * 0.05)
|
||||||
);
|
);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
ctx.font = `${fontStyle}${fontPx}px ${metrics.typography.fontFamily}`;
|
|
||||||
}
|
}
|
||||||
|
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) => {
|
nodes.forEach((node, index) => {
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
if (node.type === 'box' && node.value) {
|
if (node.type === 'box' && node.value) {
|
||||||
const nextNode = nodes[index + 1];
|
const value = String(node.value);
|
||||||
const value = `${node.value}${nextNode?.type === 'penalty' && nextNode.penalty === 100 ? '-' : ''}`;
|
const width = Number(node.width || ctx.measureText(value).width || 0);
|
||||||
this.drawWord(ctx, value, x, baseY, lineRecord, wordIndex);
|
if (currentSegment && !previousWasGlue) {
|
||||||
x += Number(node.width || ctx.measureText(value).width || 0);
|
currentSegment.value += value;
|
||||||
wordIndex += 1;
|
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) {
|
} else if (node.type === 'glue' && node.width !== 0) {
|
||||||
let width = Number(node.width || 0);
|
let width = Number(node.width || 0);
|
||||||
if (ratio > 0) width += Number(node.stretch || 0) * ratio;
|
if (ratio > 0) width += Number(node.stretch || 0) * ratio;
|
||||||
if (ratio < 0) width += Number(node.shrink || 0) * ratio;
|
if (ratio < 0) width += Number(node.shrink || 0) * ratio;
|
||||||
x += width;
|
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) {
|
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 ?? ''));
|
const animation = this.activeAnimations.get(String(lineRecord.blockId ?? ''));
|
||||||
if (!animation) {
|
if (!animation) {
|
||||||
ctx.globalAlpha = 1;
|
return 1;
|
||||||
ctx.fillText(value, x, baseY);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalWordIndex = Number(lineRecord.blockWordStart || 0) + localWordIndex;
|
const globalWordIndex = Number(lineRecord.blockWordStart || 0) + localWordIndex;
|
||||||
const timing = animation.wordTimings[globalWordIndex];
|
const timing = animation.wordTimings[globalWordIndex];
|
||||||
if (!timing) {
|
if (!timing) {
|
||||||
ctx.globalAlpha = animation.completed ? 1 : 0;
|
return animation.completed ? 1 : 0;
|
||||||
ctx.fillText(value, x, baseY);
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsed = animation.completed
|
const elapsed = animation.completed
|
||||||
@@ -210,12 +265,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
: performance.now() - animation.startedAt;
|
: performance.now() - animation.startedAt;
|
||||||
const duration = Math.max(1, Number(timing.duration || 1));
|
const duration = Math.max(1, Number(timing.duration || 1));
|
||||||
const progress = Math.max(0, Math.min(1, (elapsed - Number(timing.delay || 0)) / duration));
|
const progress = Math.max(0, Math.min(1, (elapsed - Number(timing.delay || 0)) / duration));
|
||||||
if (progress <= 0) return;
|
return progress;
|
||||||
|
|
||||||
const previousAlpha = ctx.globalAlpha;
|
|
||||||
ctx.globalAlpha = previousAlpha * progress;
|
|
||||||
ctx.fillText(value, x, baseY);
|
|
||||||
ctx.globalAlpha = previousAlpha;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startRevealAnimation(detail = {}) {
|
startRevealAnimation(detail = {}) {
|
||||||
@@ -227,6 +277,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
startedAt: performance.now(),
|
startedAt: performance.now(),
|
||||||
completed: false
|
completed: false
|
||||||
});
|
});
|
||||||
|
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
|
||||||
this.requestAnimationFrame();
|
this.requestAnimationFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,11 +286,12 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.activeAnimations.forEach((animation) => {
|
this.activeAnimations.forEach((animation) => {
|
||||||
if (!animation.completed) {
|
if (!animation.completed) {
|
||||||
animation.completed = true;
|
animation.completed = true;
|
||||||
|
this.revealedBlockIds.add(String(animation.blockId ?? ''));
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (changed) {
|
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?.());
|
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() {
|
requestAnimationFrame() {
|
||||||
if (this.animationFrameId) return;
|
if (this.animationFrameId) return;
|
||||||
this.animationFrameId = window.setTimeout(() => this.tickAnimations(performance.now()), this.targetFrameDurationMs);
|
this.animationFrameId = window.setTimeout(() => this.tickAnimations(performance.now()), this.targetFrameDurationMs);
|
||||||
@@ -269,26 +354,30 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
const currentNow = performance.now();
|
const currentNow = performance.now();
|
||||||
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;
|
||||||
const lastTiming = animation.wordTimings.at(-1);
|
const lastTiming = animation.wordTimings.at(-1);
|
||||||
const total = Number(lastTiming?.delay || 0) + Number(lastTiming?.duration || 0);
|
const total = Number(lastTiming?.delay || 0) + Number(lastTiming?.duration || 0);
|
||||||
if (currentNow - animation.startedAt >= total + 50) {
|
if (currentNow - animation.startedAt >= total + 50) {
|
||||||
animation.completed = true;
|
animation.completed = true;
|
||||||
|
this.revealedBlockIds.add(String(animation.blockId ?? ''));
|
||||||
} else {
|
} else {
|
||||||
hasActive = true;
|
hasActive = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.());
|
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getAnimatedSides(true));
|
||||||
if (hasActive) this.requestAnimationFrame();
|
if (hasActive) this.requestAnimationFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
publishSpread() {
|
publishSpread(sides = null) {
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
const sidesToPublish = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||||
detail: {
|
const detail = {
|
||||||
left: this.canvases.left,
|
|
||||||
right: this.canvases.right,
|
|
||||||
metrics: this.metrics,
|
metrics: this.metrics,
|
||||||
hitMaps: this.hitMaps
|
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
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260606-webgl-texture-dropcap-animation';
|
const MODULE_CACHE_BUSTER = '20260606-webgl-texture-refresh-fix';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const PROCEDURAL_BOOK = {
|
|||||||
PAGE_WIDTH: 2.24 * 2 / 3,
|
PAGE_WIDTH: 2.24 * 2 / 3,
|
||||||
COVER_DEPTH: 2.30,
|
COVER_DEPTH: 2.30,
|
||||||
OPEN_SEAM_GAP: 0.003,
|
OPEN_SEAM_GAP: 0.003,
|
||||||
PAGE_TEXTURE_FORE_EDGE_INSET_RATIO: 0.075,
|
PAGE_TEXTURE_FORE_EDGE_INSET_RATIO: 0.12,
|
||||||
PROFILE: {
|
PROFILE: {
|
||||||
tableY: 0,
|
tableY: 0,
|
||||||
coverThickness: 0.03,
|
coverThickness: 0.03,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
|
|||||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-texture-dropcap-animation';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-texture-refresh-fix';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@@ -25,7 +25,7 @@ const appInitialState = window.WebGLBookInitialState || {};
|
|||||||
const tableDebugName = urlParams.get('tableDebug') || 'none';
|
const tableDebugName = urlParams.get('tableDebug') || 'none';
|
||||||
const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none;
|
const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none;
|
||||||
const isAppIntegrationMode = appInitialState.appMode === true;
|
const isAppIntegrationMode = appInitialState.appMode === true;
|
||||||
const appRenderPixelRatio = isAppIntegrationMode ? 0.5 : Math.min(window.devicePixelRatio || 1, 2);
|
const appRenderPixelRatio = isAppIntegrationMode ? 1 : Math.min(window.devicePixelRatio || 1, 2);
|
||||||
const labStatus = document.getElementById('lab_status');
|
const labStatus = document.getElementById('lab_status');
|
||||||
if (labStatus && tableDebugMode !== tableDebugModes.none) {
|
if (labStatus && tableDebugMode !== tableDebugModes.none) {
|
||||||
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
|
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
|
||||||
@@ -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 = isAppIntegrationMode ? 0.28 : Math.min(window.devicePixelRatio || 1, 2);
|
const reflectionPixelRatio = isAppIntegrationMode ? 0.5 : Math.min(window.devicePixelRatio || 1, 2);
|
||||||
const pageTextureWidth = isAppIntegrationMode ? 1280 : 3200;
|
const pageTextureWidth = isAppIntegrationMode ? 1280 : 3200;
|
||||||
const reflectionTargetSize = new THREE.Vector2();
|
const reflectionTargetSize = new THREE.Vector2();
|
||||||
const pageRaycaster = new THREE.Raycaster();
|
const pageRaycaster = new THREE.Raycaster();
|
||||||
@@ -65,8 +65,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 = isAppIntegrationMode ? 480 : 4096;
|
const tableReflectionBaseWidth = isAppIntegrationMode ? 640 : 4096;
|
||||||
const tableReflectionBaseHeight = isAppIntegrationMode ? 270 : 2304;
|
const tableReflectionBaseHeight = isAppIntegrationMode ? 360 : 2304;
|
||||||
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
|
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
|
||||||
colorSpace: THREE.SRGBColorSpace,
|
colorSpace: THREE.SRGBColorSpace,
|
||||||
depthBuffer: true,
|
depthBuffer: true,
|
||||||
@@ -90,7 +90,7 @@ const reflectionUp = new THREE.Vector3();
|
|||||||
const candleShadowSources = [];
|
const candleShadowSources = [];
|
||||||
const candleWorldPosition = new THREE.Vector3();
|
const candleWorldPosition = new THREE.Vector3();
|
||||||
const flameWorldPosition = new THREE.Vector3();
|
const flameWorldPosition = new THREE.Vector3();
|
||||||
const bookShadowMapSize = isAppIntegrationMode ? 128 : 1536;
|
const bookShadowMapSize = isAppIntegrationMode ? 256 : 1536;
|
||||||
const bookShadowTargets = Array.from({ length: 3 }, () => {
|
const bookShadowTargets = Array.from({ length: 3 }, () => {
|
||||||
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
|
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
|
||||||
colorSpace: THREE.NoColorSpace,
|
colorSpace: THREE.NoColorSpace,
|
||||||
@@ -179,9 +179,9 @@ const rightTexture = new THREE.CanvasTexture(rightCanvas);
|
|||||||
[leftTexture, rightTexture].forEach((texture) => {
|
[leftTexture, rightTexture].forEach((texture) => {
|
||||||
texture.colorSpace = THREE.SRGBColorSpace;
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
texture.anisotropy = maxTextureAnisotropy;
|
texture.anisotropy = maxTextureAnisotropy;
|
||||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
texture.minFilter = THREE.LinearFilter;
|
||||||
texture.magFilter = THREE.LinearFilter;
|
texture.magFilter = THREE.LinearFilter;
|
||||||
texture.generateMipmaps = true;
|
texture.generateMipmaps = false;
|
||||||
});
|
});
|
||||||
const leatherTextures = createLeatherTextures();
|
const leatherTextures = createLeatherTextures();
|
||||||
const spineClothTextures = createSpineClothTextures();
|
const spineClothTextures = createSpineClothTextures();
|
||||||
|
|||||||
@@ -484,28 +484,12 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
|
|
||||||
handleProcessState(event) {
|
handleProcessState(event) {
|
||||||
const state = event.detail?.state || 'ready';
|
const state = event.detail?.state || 'ready';
|
||||||
if (state === 'ready' || state === 'paused' || this.mode !== '3d') {
|
|
||||||
this.stopAnimatedTextureRefresh();
|
this.stopAnimatedTextureRefresh();
|
||||||
this.triggerTextureRefresh();
|
if (state === 'ready' || state === 'paused' || this.mode !== '3d') this.triggerTextureRefresh();
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.startAnimatedTextureRefresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startAnimatedTextureRefresh() {
|
startAnimatedTextureRefresh() {
|
||||||
if (this.textureRefreshAnimationId) return;
|
this.stopAnimatedTextureRefresh();
|
||||||
const tick = (now) => {
|
|
||||||
if (this.mode !== '3d') {
|
|
||||||
this.textureRefreshAnimationId = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (now - this.lastAnimatedTextureRefresh > 100) {
|
|
||||||
this.lastAnimatedTextureRefresh = now;
|
|
||||||
window.BookLabDebug?.redrawPageTextures?.();
|
|
||||||
}
|
|
||||||
this.textureRefreshAnimationId = window.requestAnimationFrame(tick);
|
|
||||||
};
|
|
||||||
this.textureRefreshAnimationId = window.requestAnimationFrame(tick);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stopAnimatedTextureRefresh() {
|
stopAnimatedTextureRefresh() {
|
||||||
|
|||||||
Reference in New Issue
Block a user