Implement WebGL book spread flip groundwork
This commit is contained in:
+104
-3
@@ -185,8 +185,8 @@ function markPageTextureTiming(name, detail = {}) {
|
||||
|
||||
const book = new THREE.Group();
|
||||
scene.add(book);
|
||||
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0.28'), 0, 1);
|
||||
let readingProgress = Number.isFinite(initialReadingProgress) ? initialReadingProgress : 0.28;
|
||||
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0'), 0, 1);
|
||||
let readingProgress = Number.isFinite(initialReadingProgress) ? initialReadingProgress : 0;
|
||||
let bookPageCount = snapProceduralPageCount(urlParams.get('pages') ?? appInitialState.pageCount ?? '240');
|
||||
let currentProceduralBookModel = null;
|
||||
const progressInput = document.getElementById('progress_control');
|
||||
@@ -235,10 +235,22 @@ function createPageCanvasTexture(sourceCanvas) {
|
||||
return texture;
|
||||
}
|
||||
|
||||
function getBlankPageTexture() {
|
||||
if (blankPageTexture) return blankPageTexture;
|
||||
blankPageTexture = createPageCanvasTexture(createPageCanvas('blank'));
|
||||
return blankPageTexture;
|
||||
}
|
||||
|
||||
const preparedPageTextures = {
|
||||
left: new Map(),
|
||||
right: new Map()
|
||||
};
|
||||
let blankPageTexture = null;
|
||||
let currentPageMeta = {
|
||||
left: null,
|
||||
right: null
|
||||
};
|
||||
let pendingRightPageFlip = false;
|
||||
const pageRevealState = {
|
||||
left: null,
|
||||
right: null
|
||||
@@ -518,6 +530,15 @@ document.addEventListener('webgl-book:page-reveal-start', (event) => {
|
||||
document.addEventListener('webgl-book:page-reveal-fast-forward', (event) => {
|
||||
fastForwardPageReveals(event.detail?.blockIds || []);
|
||||
});
|
||||
document.addEventListener('webgl-book:reveal-committed', (event) => {
|
||||
handleRevealCommittedForPageFlip(event.detail || {});
|
||||
});
|
||||
document.addEventListener('ui:command', (event) => {
|
||||
if (event.detail?.type === 'continue' && pendingRightPageFlip) {
|
||||
pendingRightPageFlip = false;
|
||||
startPageFlip(1);
|
||||
}
|
||||
});
|
||||
installBookControls();
|
||||
installCameraControls();
|
||||
resize();
|
||||
@@ -1673,11 +1694,18 @@ function syncBookControls() {
|
||||
|
||||
function handlePageCanvases(event) {
|
||||
const detail = event.detail || {};
|
||||
if (detail.pageMeta) {
|
||||
currentPageMeta = {
|
||||
left: detail.pageMeta.left || currentPageMeta.left || null,
|
||||
right: detail.pageMeta.right || currentPageMeta.right || null
|
||||
};
|
||||
}
|
||||
markPageTextureTiming('handlePageCanvases:start', {
|
||||
hasLeft: Boolean(detail.left),
|
||||
hasRight: Boolean(detail.right),
|
||||
revealSides: Object.keys(detail.reveal || {}),
|
||||
preloadOnly: Boolean(detail.preloadOnly)
|
||||
preloadOnly: Boolean(detail.preloadOnly),
|
||||
pageMeta: currentPageMeta
|
||||
});
|
||||
if (detail.preloadOnly) {
|
||||
if (detail.left) preloadPageTexture('left', detail.left, detail.reveal?.left);
|
||||
@@ -2111,8 +2139,11 @@ function textureHitPageSide(hit) {
|
||||
|
||||
function startPageFlip(direction) {
|
||||
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
||||
pendingRightPageFlip = false;
|
||||
delete document.documentElement.dataset.webglPendingPageFlip;
|
||||
const flip = createPageFlip(direction, performance.now(), normalFlipDuration);
|
||||
if (!flip) return false;
|
||||
prepareStaticPageForFlip(flip);
|
||||
activeFlips.push(flip);
|
||||
syncBookControls();
|
||||
updateActiveFlips(flip.startTime);
|
||||
@@ -2123,6 +2154,7 @@ function startFastPageFlip(direction) {
|
||||
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
||||
const firstFlip = createPageFlip(direction, performance.now(), fastFlipDuration);
|
||||
if (!firstFlip) return false;
|
||||
prepareStaticPageForFlip(firstFlip);
|
||||
const startTime = firstFlip.startTime;
|
||||
const interval = fastFlipDuration / fastFlipOverlap;
|
||||
for (let index = 0; index < fastFlipCount; index += 1) {
|
||||
@@ -2142,11 +2174,13 @@ function startFastPageFlip(direction) {
|
||||
|
||||
function createPageFlip(direction, startTime, duration) {
|
||||
const sourceSide = direction > 0 ? 1 : -1;
|
||||
const sourcePageSide = direction > 0 ? 'right' : 'left';
|
||||
const sourceLine = topVisibleLine(sourceSide);
|
||||
const destinationLine = topVisibleLine(-sourceSide);
|
||||
if (!sourceLine || !destinationLine) return null;
|
||||
return {
|
||||
direction,
|
||||
sourcePageSide,
|
||||
sourceLine,
|
||||
destinationLine,
|
||||
startTime,
|
||||
@@ -2158,12 +2192,64 @@ function createPageFlip(direction, startTime, duration) {
|
||||
};
|
||||
}
|
||||
|
||||
function prepareStaticPageForFlip(flip) {
|
||||
if (!flip) return;
|
||||
const sourceMaterial = flip.sourcePageSide === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const sourceTexture = sourceMaterial?.map || (flip.sourcePageSide === 'left' ? leftTexture : rightTexture);
|
||||
materials.flipPageSurface.map = sourceTexture;
|
||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||
materials.flipPageSurface.needsUpdate = true;
|
||||
flip.sourceTexture = sourceTexture;
|
||||
if (flip.direction > 0) {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
||||
clearPageReveal('right', 'page-flip-start');
|
||||
materials.rightPage.map = blankTexture;
|
||||
materials.rightPage.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function canPageFlip(direction) {
|
||||
if (!currentProceduralBookModel) return false;
|
||||
if (direction > 0) return readingProgress < 1;
|
||||
return readingProgress > 0;
|
||||
}
|
||||
|
||||
function handleRevealCommittedForPageFlip(detail = {}) {
|
||||
if (detail.side !== 'right' || !isRightBodyPageComplete()) return;
|
||||
if (activeFlips.length > 0 || pendingRightPageFlip) return;
|
||||
if (isChoiceAwaitingPlayer()) return;
|
||||
if (isTtsPlaybackActive()) {
|
||||
startPageFlip(1);
|
||||
return;
|
||||
}
|
||||
pendingRightPageFlip = true;
|
||||
document.documentElement.dataset.webglPendingPageFlip = 'right';
|
||||
}
|
||||
|
||||
function isRightBodyPageComplete() {
|
||||
const meta = currentPageMeta?.right || null;
|
||||
if (!meta || meta.section !== 'body' || meta.kind === 'blank' || meta.kind === 'title') return false;
|
||||
const rendererDebug = window.BookTextureRenderer?.currentSpread || null;
|
||||
const rightLines = Array.isArray(rendererDebug?.right) ? rendererDebug.right : [];
|
||||
const maxLine = rightLines.reduce((max, line) => Math.max(max, Number(line?.pageLine || 0) + Math.max(1, Number(line?.lineCount || 1))), 0);
|
||||
const expectedLines = Math.max(1, Number(meta.linesPerPage || 25));
|
||||
return maxLine >= expectedLines;
|
||||
}
|
||||
|
||||
function isChoiceAwaitingPlayer() {
|
||||
return document.documentElement.dataset.choiceAwaiting === 'true'
|
||||
|| document.body?.dataset?.choiceAwaiting === 'true'
|
||||
|| Boolean(document.querySelector('#choice_menu:not([hidden]) .choice, #choice_menu.visible .choice'));
|
||||
}
|
||||
|
||||
function isTtsPlaybackActive() {
|
||||
const coordinator = window.moduleRegistry?.getModule?.('playback-coordinator') || window.PlaybackCoordinator || null;
|
||||
return Boolean(coordinator?.isPlaying || coordinator?.state === 'playing' || document.documentElement.dataset.ttsPlaying === 'true');
|
||||
}
|
||||
|
||||
function topVisibleLine(side) {
|
||||
const sideLines = currentProceduralBookModel.lines
|
||||
.filter((line) => line.side === side)
|
||||
@@ -2180,6 +2266,15 @@ function updateActiveFlips(now) {
|
||||
const t = THREE.MathUtils.clamp(elapsed, 0, 1);
|
||||
const surface = buildFlippingPageSurface(flip.sourceLine, flip.destinationLine, flip.direction, easeInOutCubic(t), flip.pageOffset);
|
||||
setActivePageGeometry(flip, surface);
|
||||
if (!flip.spreadAdvanced && t >= 0.82) {
|
||||
flip.spreadAdvanced = true;
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-near-end', {
|
||||
detail: {
|
||||
direction: flip.direction,
|
||||
sourceSide: flip.sourcePageSide || (flip.direction > 0 ? 'right' : 'left')
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (t >= 1) completed.push(flip);
|
||||
});
|
||||
completed.forEach((flip) => finishActiveFlip(flip));
|
||||
@@ -2378,6 +2473,12 @@ function createFlippingPageGeometry(surface) {
|
||||
function finishActiveFlip(flip) {
|
||||
removeFlipMesh(flip);
|
||||
activeFlips = activeFlips.filter((active) => active !== flip);
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-finished', {
|
||||
detail: {
|
||||
direction: flip.direction,
|
||||
sourceSide: flip.sourcePageSide || (flip.direction > 0 ? 'right' : 'left')
|
||||
}
|
||||
}));
|
||||
if (flip.commitBundleOnFinish) {
|
||||
shiftReadingProgressByBundle(flip.direction);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user