Add shader page reveal checkpoint
This commit is contained in:
+1
-1
@@ -280,6 +280,6 @@
|
|||||||
console.log(message);
|
console.log(message);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="/js/loader.js?v=20260607-webgl-paper-loader-fix"></script>
|
<script type="module" src="/js/loader.js?v=20260607-webgl-shader-reveal"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Defines the canonical page geometry used by the WebGL book renderer.
|
* Defines the canonical page geometry used by the WebGL book renderer.
|
||||||
*/
|
*/
|
||||||
import { BaseModule } from './base-module.js';
|
import { BaseModule } from './base-module.js';
|
||||||
import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-paper-loader-fix';
|
import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-shader-reveal';
|
||||||
|
|
||||||
export const BOOK_TEXTURE_WIDTH = 3072;
|
export const BOOK_TEXTURE_WIDTH = 3072;
|
||||||
|
|
||||||
@@ -20,13 +20,13 @@ class BookPageFormatModule extends BaseModule {
|
|||||||
margins: Object.freeze({
|
margins: Object.freeze({
|
||||||
topIn: 0.46,
|
topIn: 0.46,
|
||||||
bottomIn: 0.58,
|
bottomIn: 0.58,
|
||||||
innerBaseIn: 0.375,
|
innerBaseIn: 0.42,
|
||||||
innerMinIn: 0.44,
|
innerMinIn: 0.48,
|
||||||
innerMaxIn: 0.68,
|
innerMaxIn: 0.74,
|
||||||
innerThicknessFactor: 0.25,
|
innerThicknessFactor: 0.32,
|
||||||
outerBaseIn: 0.44,
|
outerBaseIn: 0.36,
|
||||||
outerThicknessFactor: 0.04,
|
outerThicknessFactor: 0.02,
|
||||||
outerMaxIn: 0.5
|
outerMaxIn: 0.42
|
||||||
}),
|
}),
|
||||||
typography: Object.freeze({
|
typography: Object.freeze({
|
||||||
fontFamily: '"EB Garamond", "EB Garamond 12", serif',
|
fontFamily: '"EB Garamond", "EB Garamond 12", serif',
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.currentSpread = null;
|
this.currentSpread = null;
|
||||||
this.activeAnimations = new Map();
|
this.activeAnimations = new Map();
|
||||||
this.revealedBlockIds = new Set();
|
this.revealedBlockIds = new Set();
|
||||||
|
this.pendingRevealBlockIds = new Set();
|
||||||
|
this.revealBounds = null;
|
||||||
|
this.revealPublishBlockIds = null;
|
||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
this.lastAnimationFrameAt = 0;
|
this.lastAnimationFrameAt = 0;
|
||||||
this.targetFrameDurationMs = 1000 / 30;
|
this.targetFrameDurationMs = 1000 / 30;
|
||||||
@@ -40,6 +43,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'drawPageLines',
|
'drawPageLines',
|
||||||
'drawLine',
|
'drawLine',
|
||||||
'drawWord',
|
'drawWord',
|
||||||
|
'recordRevealRect',
|
||||||
'getPageContent',
|
'getPageContent',
|
||||||
'buildLineSegments',
|
'buildLineSegments',
|
||||||
'startRevealAnimation',
|
'startRevealAnimation',
|
||||||
@@ -70,10 +74,18 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.addEventListener(document, 'webgl-book:page-count-changed', this.handlePageCountChanged);
|
this.addEventListener(document, 'webgl-book:page-count-changed', this.handlePageCountChanged);
|
||||||
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 spread = event.detail?.spread || this.pagination?.getCurrentSpread?.();
|
||||||
const latestBlockId = event.detail?.latestBlockId;
|
const latestBlockId = event.detail?.latestBlockId;
|
||||||
const latestRenderedBlockId = Math.max(0, Number(event.detail?.latestRenderedBlockId || 0));
|
const latestRenderedBlockId = Math.max(0, Number(event.detail?.latestRenderedBlockId || 0));
|
||||||
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) this.markPendingReveal(latestBlockId);
|
this.currentSpread = spread || { left: [], right: [] };
|
||||||
this.drawSpread(event.detail?.spread || this.pagination?.getCurrentSpread?.());
|
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.addEventListener(document, 'book-texture:reveal-block', (event) => {
|
||||||
this.startRevealAnimation(event.detail || {});
|
this.startRevealAnimation(event.detail || {});
|
||||||
@@ -108,12 +120,15 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
drawSpread(spread = null, sides = null) {
|
drawSpread(spread = null, sides = null) {
|
||||||
this.currentSpread = spread || { left: [], right: [] };
|
this.currentSpread = spread || { left: [], right: [] };
|
||||||
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||||
|
this.revealBounds = { left: null, right: null };
|
||||||
sidesToDraw.forEach((side) => {
|
sidesToDraw.forEach((side) => {
|
||||||
if (!this.canvases[side]) return;
|
if (!this.canvases[side]) return;
|
||||||
this.drawPageBase(side);
|
this.drawPageBase(side);
|
||||||
this.drawPageLines(side, this.currentSpread?.[side] || []);
|
this.drawPageLines(side, this.currentSpread?.[side] || []);
|
||||||
});
|
});
|
||||||
this.publishSpread(sidesToDraw);
|
this.publishSpread(sidesToDraw);
|
||||||
|
this.revealBounds = null;
|
||||||
|
this.revealPublishBlockIds = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPageBase(side) {
|
drawPageBase(side) {
|
||||||
@@ -181,26 +196,20 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
ctx.font = `${fontStyle}${smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
ctx.font = `${fontStyle}${smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||||
if (lineRecord.dropCapText) {
|
if (lineRecord.dropCapText) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
const alpha = this.getWordAlpha(lineRecord, 0);
|
const dropCapFontPx = Math.round(fontPx * 2.68);
|
||||||
if (alpha <= 0) {
|
const dropCapX = content.x;
|
||||||
ctx.restore();
|
const dropCapY = content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) + (fontPx * 0.25);
|
||||||
} else {
|
ctx.font = `${dropCapFontPx}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
||||||
ctx.globalAlpha *= alpha;
|
|
||||||
ctx.font = `${Math.round(fontPx * 2.68)}px "EB Garamond Initials", ${metrics.typography.fontFamily}`;
|
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
ctx.fillText(
|
ctx.fillText(String(lineRecord.dropCapText), dropCapX, dropCapY);
|
||||||
String(lineRecord.dropCapText),
|
this.recordRevealRect(side, lineRecord, dropCapX, dropCapY, fontPx * 2.9, dropCapFontPx * 0.9);
|
||||||
content.x,
|
|
||||||
content.y + (Number(lineRecord.pageLine || 0) * lineHeightPx) + (fontPx * 0.25)
|
|
||||||
);
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
|
||||||
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = smallCaps ? 'all-small-caps' : 'normal';
|
if ('fontVariantCaps' in ctx) ctx.fontVariantCaps = smallCaps ? 'all-small-caps' : 'normal';
|
||||||
if ('letterSpacing' in ctx) ctx.letterSpacing = smallCaps ? `${fontPx * 0.012}px` : '0px';
|
if ('letterSpacing' in ctx) ctx.letterSpacing = smallCaps ? `${fontPx * 0.012}px` : '0px';
|
||||||
ctx.font = `${fontStyle}${smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
ctx.font = `${fontStyle}${smallCaps ? 'small-caps ' : ''}${fontPx}px ${metrics.typography.fontFamily}`;
|
||||||
}
|
}
|
||||||
this.buildLineSegments(ctx, nodes, line, ratio).forEach((segment) => {
|
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 ('fontVariantCaps' in ctx) ctx.fontVariantCaps = previousVariantCaps || 'normal';
|
||||||
if ('letterSpacing' in ctx) ctx.letterSpacing = previousLetterSpacing || '0px';
|
if ('letterSpacing' in ctx) ctx.letterSpacing = previousLetterSpacing || '0px';
|
||||||
@@ -262,33 +271,36 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawWord(ctx, value, x, baseY, lineRecord, localWordIndex) {
|
drawWord(ctx, value, x, baseY, lineRecord, localWordIndex, side, fontPx, lineHeightPx) {
|
||||||
const alpha = this.getWordAlpha(lineRecord, localWordIndex);
|
|
||||||
if (alpha <= 0) return;
|
|
||||||
const previousAlpha = ctx.globalAlpha;
|
|
||||||
ctx.globalAlpha = previousAlpha * alpha;
|
|
||||||
ctx.fillText(value, x, baseY);
|
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) {
|
recordRevealRect(side, lineRecord, x, y, width, height, localWordIndex = 0) {
|
||||||
const animation = this.activeAnimations.get(String(lineRecord.blockId ?? ''));
|
if (!this.revealBounds || !this.revealPublishBlockIds) return;
|
||||||
if (!animation) {
|
const blockId = String(lineRecord?.blockId ?? '');
|
||||||
return 1;
|
if (!blockId || !this.revealPublishBlockIds.has(blockId)) return;
|
||||||
}
|
const animation = this.activeAnimations.get(blockId);
|
||||||
|
if (!animation || animation.completed) return;
|
||||||
const globalWordIndex = Number(lineRecord.blockWordStart || 0) + localWordIndex;
|
const padding = Math.max(2, Number(lineRecord.fontPx || 18) * 0.12);
|
||||||
const timing = animation.wordTimings[globalWordIndex];
|
const nextRect = {
|
||||||
if (!timing) {
|
x: Math.max(0, x - padding),
|
||||||
return animation.completed ? 1 : 0;
|
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 elapsed = animation.completed
|
};
|
||||||
? Number.POSITIVE_INFINITY
|
const current = this.revealBounds[side];
|
||||||
: performance.now() - animation.startedAt;
|
this.revealBounds[side] = current ? {
|
||||||
const duration = Math.max(1, Number(timing.duration || 1));
|
x: Math.min(current.x, nextRect.x),
|
||||||
const progress = Math.max(0, Math.min(1, (elapsed - Number(timing.delay || 0)) / duration));
|
y: Math.min(current.y, nextRect.y),
|
||||||
return progress;
|
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 = {}) {
|
startRevealAnimation(detail = {}) {
|
||||||
@@ -298,8 +310,14 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
blockId,
|
blockId,
|
||||||
wordTimings: detail.wordTimings,
|
wordTimings: detail.wordTimings,
|
||||||
startedAt: performance.now(),
|
startedAt: performance.now(),
|
||||||
|
totalDuration: Math.max(
|
||||||
|
Number(detail.totalDuration || 0),
|
||||||
|
...detail.wordTimings.map(timing => Number(timing.delay || 0) + Number(timing.duration || 0))
|
||||||
|
),
|
||||||
completed: false
|
completed: false
|
||||||
});
|
});
|
||||||
|
this.pendingRevealBlockIds.delete(String(blockId));
|
||||||
|
this.revealPublishBlockIds = new Set([String(blockId)]);
|
||||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
|
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getBlockSides(blockId));
|
||||||
this.requestAnimationFrame();
|
this.requestAnimationFrame();
|
||||||
}
|
}
|
||||||
@@ -314,12 +332,14 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
this.pendingRevealBlockIds.clear();
|
||||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getAnimatedSides(true));
|
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getAnimatedSides(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopAnimations() {
|
stopAnimations() {
|
||||||
this.activeAnimations.clear();
|
this.activeAnimations.clear();
|
||||||
|
this.pendingRevealBlockIds.clear();
|
||||||
if (this.animationFrameId) {
|
if (this.animationFrameId) {
|
||||||
clearTimeout(this.animationFrameId);
|
clearTimeout(this.animationFrameId);
|
||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
@@ -352,12 +372,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
markPendingReveal(blockId) {
|
markPendingReveal(blockId) {
|
||||||
const id = String(blockId ?? '');
|
const id = String(blockId ?? '');
|
||||||
if (!id || this.activeAnimations.has(id) || this.revealedBlockIds.has(id)) return;
|
if (!id || this.activeAnimations.has(id) || this.revealedBlockIds.has(id)) return;
|
||||||
this.activeAnimations.set(id, {
|
this.pendingRevealBlockIds.add(id);
|
||||||
blockId,
|
|
||||||
wordTimings: [],
|
|
||||||
startedAt: performance.now(),
|
|
||||||
completed: false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame() {
|
requestAnimationFrame() {
|
||||||
@@ -387,7 +402,6 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
hasActive = true;
|
hasActive = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.drawSpread(this.currentSpread || this.pagination?.getCurrentSpread?.(), this.getAnimatedSides(true));
|
|
||||||
if (hasActive) this.requestAnimationFrame();
|
if (hasActive) this.requestAnimationFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +413,28 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
};
|
};
|
||||||
if (sidesToPublish.includes('left')) detail.left = this.canvases.left;
|
if (sidesToPublish.includes('left')) detail.left = this.canvases.left;
|
||||||
if (sidesToPublish.includes('right')) detail.right = this.canvases.right;
|
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', {
|
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
||||||
detail
|
detail
|
||||||
}));
|
}));
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260607-webgl-paper-loader-fix';
|
const MODULE_CACHE_BUSTER = '20260607-webgl-shader-reveal';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+161
-7
@@ -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=20260607-webgl-paper-loader-fix';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-shader-reveal';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@@ -188,15 +188,31 @@ const inkColor = '#1a1009';
|
|||||||
await reportLabStep(48, 'Preparing high-resolution page textures');
|
await reportLabStep(48, 'Preparing high-resolution page textures');
|
||||||
const leftCanvas = createPageCanvas('left');
|
const leftCanvas = createPageCanvas('left');
|
||||||
const rightCanvas = createPageCanvas('right');
|
const rightCanvas = createPageCanvas('right');
|
||||||
|
const leftRevealCanvas = createPageCanvas('left');
|
||||||
|
const rightRevealCanvas = createPageCanvas('right');
|
||||||
const leftTexture = new THREE.CanvasTexture(leftCanvas);
|
const leftTexture = new THREE.CanvasTexture(leftCanvas);
|
||||||
const rightTexture = new THREE.CanvasTexture(rightCanvas);
|
const rightTexture = new THREE.CanvasTexture(rightCanvas);
|
||||||
[leftTexture, rightTexture].forEach((texture) => {
|
const leftRevealTexture = new THREE.CanvasTexture(leftRevealCanvas);
|
||||||
|
const rightRevealTexture = new THREE.CanvasTexture(rightRevealCanvas);
|
||||||
|
[leftTexture, rightTexture, leftRevealTexture, rightRevealTexture].forEach((texture) => {
|
||||||
texture.colorSpace = THREE.SRGBColorSpace;
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
texture.anisotropy = maxTextureAnisotropy;
|
texture.anisotropy = maxTextureAnisotropy;
|
||||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
texture.magFilter = THREE.LinearFilter;
|
texture.magFilter = THREE.LinearFilter;
|
||||||
texture.generateMipmaps = true;
|
texture.generateMipmaps = true;
|
||||||
});
|
});
|
||||||
|
const pageRevealState = {
|
||||||
|
left: null,
|
||||||
|
right: null
|
||||||
|
};
|
||||||
|
const pageRevealCanvases = {
|
||||||
|
left: leftRevealCanvas,
|
||||||
|
right: rightRevealCanvas
|
||||||
|
};
|
||||||
|
const pageRevealTextures = {
|
||||||
|
left: leftRevealTexture,
|
||||||
|
right: rightRevealTexture
|
||||||
|
};
|
||||||
await reportLabStep(52, 'Generating leather texture set');
|
await reportLabStep(52, 'Generating leather texture set');
|
||||||
const leatherTextures = createLeatherTextures();
|
const leatherTextures = createLeatherTextures();
|
||||||
await reportLabStep(56, 'Generating spine cloth texture set');
|
await reportLabStep(56, 'Generating spine cloth texture set');
|
||||||
@@ -340,6 +356,14 @@ const materials = {
|
|||||||
envMapIntensity: 0
|
envMapIntensity: 0
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
materials.leftPage.userData.bookPageReveal = {
|
||||||
|
side: 'left',
|
||||||
|
texture: leftRevealTexture
|
||||||
|
};
|
||||||
|
materials.rightPage.userData.bookPageReveal = {
|
||||||
|
side: 'right',
|
||||||
|
texture: rightRevealTexture
|
||||||
|
};
|
||||||
materials.spineCloth.userData.isSpineCloth = true;
|
materials.spineCloth.userData.isSpineCloth = true;
|
||||||
materials.headband.userData.isHeadband = true;
|
materials.headband.userData.isHeadband = true;
|
||||||
configureHardcoverPaperMaterial(materials.pageBlock);
|
configureHardcoverPaperMaterial(materials.pageBlock);
|
||||||
@@ -535,13 +559,22 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
const isSpineCloth = material.userData?.isSpineCloth === true;
|
const isSpineCloth = material.userData?.isSpineCloth === true;
|
||||||
const isHardcoverPaper = material.userData?.isHardcoverPaper === true;
|
const isHardcoverPaper = material.userData?.isHardcoverPaper === true;
|
||||||
const isHeadband = material.userData?.isHeadband === true;
|
const isHeadband = material.userData?.isHeadband === true;
|
||||||
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}-${isHeadband ? 'headband-v1' : isSpineCloth ? 'spine-cloth-v4' : isHardcoverPaper ? 'hardcover-paper-v1' : 'plain'}`;
|
const pageReveal = material.userData?.bookPageReveal || null;
|
||||||
|
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}-${pageReveal ? 'page-reveal-v1' : isHeadband ? 'headband-v1' : isSpineCloth ? 'spine-cloth-v4' : isHardcoverPaper ? 'hardcover-paper-v1' : 'plain'}`;
|
||||||
material.onBeforeCompile = (shader) => {
|
material.onBeforeCompile = (shader) => {
|
||||||
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
||||||
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
||||||
shader.uniforms.bookShadowMapTexelSize = { value: new THREE.Vector2(1 / bookShadowMapSize, 1 / bookShadowMapSize) };
|
shader.uniforms.bookShadowMapTexelSize = { value: new THREE.Vector2(1 / bookShadowMapSize, 1 / bookShadowMapSize) };
|
||||||
shader.uniforms.bookShadowReceiverStrength = { value: strength };
|
shader.uniforms.bookShadowReceiverStrength = { value: strength };
|
||||||
shader.uniforms.bookTableTopY = { value: tableTopY };
|
shader.uniforms.bookTableTopY = { value: tableTopY };
|
||||||
|
if (pageReveal) {
|
||||||
|
shader.uniforms.bookRevealMap = { value: pageReveal.texture };
|
||||||
|
shader.uniforms.bookRevealActive = { value: 0 };
|
||||||
|
shader.uniforms.bookRevealProgress = { value: 1 };
|
||||||
|
shader.uniforms.bookRevealBounds = { value: new THREE.Vector4(0, 0, 1, 1) };
|
||||||
|
shader.uniforms.bookRevealSoftness = { value: 0.035 };
|
||||||
|
material.userData.bookRevealShader = shader;
|
||||||
|
}
|
||||||
|
|
||||||
shader.vertexShader = shader.vertexShader
|
shader.vertexShader = shader.vertexShader
|
||||||
.replace(
|
.replace(
|
||||||
@@ -575,6 +608,19 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
uniform vec2 bookShadowMapTexelSize;
|
uniform vec2 bookShadowMapTexelSize;
|
||||||
uniform float bookShadowReceiverStrength;
|
uniform float bookShadowReceiverStrength;
|
||||||
uniform float bookTableTopY;
|
uniform float bookTableTopY;
|
||||||
|
${pageReveal ? `uniform sampler2D bookRevealMap;
|
||||||
|
uniform float bookRevealActive;
|
||||||
|
uniform float bookRevealProgress;
|
||||||
|
uniform vec4 bookRevealBounds;
|
||||||
|
uniform float bookRevealSoftness;
|
||||||
|
|
||||||
|
float bookRevealMask(vec2 uv) {
|
||||||
|
vec2 local = (uv - bookRevealBounds.xy) / max(bookRevealBounds.zw, vec2(0.0001));
|
||||||
|
float inside = step(0.0, local.x) * step(0.0, local.y) * step(local.x, 1.0) * step(local.y, 1.0);
|
||||||
|
float diagonal = clamp((local.x + (1.0 - local.y)) * 0.5, 0.0, 1.0);
|
||||||
|
float feather = max(0.0001, bookRevealSoftness);
|
||||||
|
return inside * smoothstep(diagonal - feather, diagonal + feather, bookRevealProgress);
|
||||||
|
}` : ''}
|
||||||
varying vec3 vBookReceiverWorldPosition;
|
varying vec3 vBookReceiverWorldPosition;
|
||||||
varying vec3 vBookReceiverWorldNormal;
|
varying vec3 vBookReceiverWorldNormal;
|
||||||
${isSpineCloth || isHardcoverPaper || isHeadband ? 'varying vec2 vBookSurfaceUv;' : ''}
|
${isSpineCloth || isHardcoverPaper || isHeadband ? 'varying vec2 vBookSurfaceUv;' : ''}
|
||||||
@@ -712,6 +758,19 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
outgoingLight += bookLocalBounce(vBookReceiverWorldPosition, normalize(vBookReceiverWorldNormal), bookReceiverShadow, diffuseColor.rgb);
|
outgoingLight += bookLocalBounce(vBookReceiverWorldPosition, normalize(vBookReceiverWorldNormal), bookReceiverShadow, diffuseColor.rgb);
|
||||||
#include <opaque_fragment>`
|
#include <opaque_fragment>`
|
||||||
);
|
);
|
||||||
|
if (pageReveal) {
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
'#include <map_fragment>',
|
||||||
|
`#ifdef USE_MAP
|
||||||
|
vec4 sampledDiffuseColor = texture2D(map, vMapUv);
|
||||||
|
if (bookRevealActive > 0.5) {
|
||||||
|
vec4 revealDiffuseColor = texture2D(bookRevealMap, vMapUv);
|
||||||
|
sampledDiffuseColor = mix(sampledDiffuseColor, revealDiffuseColor, bookRevealMask(vMapUv));
|
||||||
|
}
|
||||||
|
diffuseColor *= sampledDiffuseColor;
|
||||||
|
#endif`
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1551,12 +1610,18 @@ function syncBookControls() {
|
|||||||
function handlePageCanvases(event) {
|
function handlePageCanvases(event) {
|
||||||
const detail = event.detail || {};
|
const detail = event.detail || {};
|
||||||
if (detail.left) {
|
if (detail.left) {
|
||||||
drawCanvasPageTexture(leftCanvas, detail.left, 'left');
|
if (detail.reveal?.left) {
|
||||||
leftTexture.needsUpdate = true;
|
beginPageReveal('left', detail.left, detail.reveal.left);
|
||||||
|
} else {
|
||||||
|
uploadPageTextureDirect('left', detail.left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (detail.right) {
|
if (detail.right) {
|
||||||
drawCanvasPageTexture(rightCanvas, detail.right, 'right');
|
if (detail.reveal?.right) {
|
||||||
rightTexture.needsUpdate = true;
|
beginPageReveal('right', detail.right, detail.reveal.right);
|
||||||
|
} else {
|
||||||
|
uploadPageTextureDirect('right', detail.right);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
markStaticSceneBuffersDirty();
|
markStaticSceneBuffersDirty();
|
||||||
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
|
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
|
||||||
@@ -1566,6 +1631,94 @@ function handlePageCanvases(event) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uploadPageTextureDirect(side, sourceCanvas) {
|
||||||
|
const canvas = side === 'left' ? leftCanvas : rightCanvas;
|
||||||
|
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||||
|
clearPageReveal(side);
|
||||||
|
drawCanvasPageTexture(canvas, sourceCanvas, side);
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||||
|
const revealCanvas = pageRevealCanvases[side];
|
||||||
|
const revealTexture = pageRevealTextures[side];
|
||||||
|
if (!revealCanvas || !revealTexture) {
|
||||||
|
uploadPageTextureDirect(side, sourceCanvas);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCanvasPageTexture(revealCanvas, sourceCanvas, side);
|
||||||
|
const shader = getPageRevealShader(side);
|
||||||
|
if (!shader?.uniforms) {
|
||||||
|
uploadPageTextureDirect(side, sourceCanvas);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = revealDetail.bounds || {};
|
||||||
|
const x = THREE.MathUtils.clamp(Number(bounds.x || 0), 0, 1);
|
||||||
|
const y = THREE.MathUtils.clamp(Number(bounds.y || 0), 0, 1);
|
||||||
|
const width = THREE.MathUtils.clamp(Number(bounds.width || 1), 0.001, 1);
|
||||||
|
const height = THREE.MathUtils.clamp(Number(bounds.height || 1), 0.001, 1);
|
||||||
|
shader.uniforms.bookRevealBounds.value.set(
|
||||||
|
x,
|
||||||
|
THREE.MathUtils.clamp(1 - y - height, 0, 1),
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
);
|
||||||
|
shader.uniforms.bookRevealProgress.value = 0;
|
||||||
|
shader.uniforms.bookRevealActive.value = 1;
|
||||||
|
shader.uniforms.bookRevealMap.value = revealTexture;
|
||||||
|
revealTexture.needsUpdate = true;
|
||||||
|
|
||||||
|
pageRevealState[side] = {
|
||||||
|
startedAt: performance.now(),
|
||||||
|
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||||
|
revealCanvas,
|
||||||
|
blockIds: Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageRevealShader(side) {
|
||||||
|
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||||
|
return material?.userData?.bookRevealShader || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPageReveal(side) {
|
||||||
|
pageRevealState[side] = null;
|
||||||
|
const shader = getPageRevealShader(side);
|
||||||
|
if (shader?.uniforms?.bookRevealActive) {
|
||||||
|
shader.uniforms.bookRevealActive.value = 0;
|
||||||
|
shader.uniforms.bookRevealProgress.value = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePageRevealAnimations(now) {
|
||||||
|
['left', 'right'].forEach((side) => {
|
||||||
|
const state = pageRevealState[side];
|
||||||
|
if (!state) return;
|
||||||
|
const shader = getPageRevealShader(side);
|
||||||
|
if (!shader?.uniforms) {
|
||||||
|
clearPageReveal(side);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const progress = THREE.MathUtils.clamp((now - state.startedAt) / state.durationMs, 0, 1);
|
||||||
|
shader.uniforms.bookRevealProgress.value = progress;
|
||||||
|
if (progress < 1) return;
|
||||||
|
|
||||||
|
const canvas = side === 'left' ? leftCanvas : rightCanvas;
|
||||||
|
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||||
|
drawCanvasPageTexture(canvas, state.revealCanvas, side);
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
clearPageReveal(side);
|
||||||
|
document.dispatchEvent(new CustomEvent('webgl-book:reveal-committed', {
|
||||||
|
detail: {
|
||||||
|
side,
|
||||||
|
blockIds: state.blockIds
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function drawCanvasPageTexture(canvas, sourceCanvas, side) {
|
function drawCanvasPageTexture(canvas, sourceCanvas, side) {
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.fillStyle = '#f2ead0';
|
ctx.fillStyle = '#f2ead0';
|
||||||
@@ -2998,6 +3151,7 @@ function animate(now = performance.now()) {
|
|||||||
const hadActiveFlips = activeFlips.length > 0;
|
const hadActiveFlips = activeFlips.length > 0;
|
||||||
updateActiveFlips(performance.now());
|
updateActiveFlips(performance.now());
|
||||||
if (hadActiveFlips) markStaticSceneBuffersDirty();
|
if (hadActiveFlips) markStaticSceneBuffersDirty();
|
||||||
|
updatePageRevealAnimations(now);
|
||||||
updateCandleShadowUniforms();
|
updateCandleShadowUniforms();
|
||||||
renderedFrameCount += 1;
|
renderedFrameCount += 1;
|
||||||
const shadowStartedAt = performance.now();
|
const shadowStartedAt = performance.now();
|
||||||
|
|||||||
Reference in New Issue
Block a user