Implement WebGL page reserve navigation
This commit is contained in:
+291
-24
@@ -187,7 +187,8 @@ const book = new THREE.Group();
|
||||
scene.add(book);
|
||||
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 bookPageCount = snapProceduralPageCount(urlParams.get('pages') ?? appInitialState.pageCount ?? '300');
|
||||
let pageReserve = clampPageReserve(appInitialState.pageReserve ?? 50, bookPageCount);
|
||||
let currentProceduralBookModel = null;
|
||||
const progressInput = document.getElementById('progress_control');
|
||||
const progressValue = document.getElementById('progress_value');
|
||||
@@ -197,6 +198,12 @@ const fastBackwardButton = document.getElementById('fast_backward');
|
||||
const backwardButton = document.getElementById('flip_backward');
|
||||
const forwardButton = document.getElementById('flip_forward');
|
||||
const fastForwardButton = document.getElementById('fast_forward');
|
||||
let bottomNavigation = null;
|
||||
let bookPaginationState = {
|
||||
spreadIndex: 0,
|
||||
spreadCount: 1,
|
||||
writtenPageLimit: 0
|
||||
};
|
||||
const normalFlipDuration = 900;
|
||||
const fastFlipDuration = 520;
|
||||
const fastFlipCount = 10;
|
||||
@@ -399,6 +406,14 @@ const materials = {
|
||||
envMapIntensity: 0
|
||||
})
|
||||
};
|
||||
materials.flipPageBackSurface = materials.flipPageSurface.clone();
|
||||
materials.flipPageBackSurface.map = getBlankPageTexture();
|
||||
materials.flipPageBackSurface.side = THREE.DoubleSide;
|
||||
materials.flipPageEdge = materials.pageSurface.clone();
|
||||
materials.flipPageEdge.map = paperTextures.edge;
|
||||
materials.flipPageEdge.normalMap = paperTextures.normal;
|
||||
materials.flipPageEdge.roughnessMap = paperTextures.roughness;
|
||||
materials.flipPageEdge.side = THREE.DoubleSide;
|
||||
materials.leftPage.userData.bookPageReveal = {
|
||||
side: 'left'
|
||||
};
|
||||
@@ -421,6 +436,8 @@ configureBookShadowReceiver(materials.pageBlock, 0.18);
|
||||
configureBookShadowReceiver(materials.pageEdge, 0.16);
|
||||
configureBookShadowReceiver(materials.pageSurface, 0.11);
|
||||
configureBookShadowReceiver(materials.flipPageSurface, 0.11);
|
||||
configureBookShadowReceiver(materials.flipPageBackSurface, 0.11);
|
||||
configureBookShadowReceiver(materials.flipPageEdge, 0.09);
|
||||
configureBookShadowReceiver(materials.leftPage, 0.08);
|
||||
configureBookShadowReceiver(materials.rightPage, 0.08);
|
||||
configureBookShadowReceiver(materials.spineCloth, 0.48);
|
||||
@@ -498,6 +515,23 @@ window.BookLabDebug = {
|
||||
setBookPageCount(value);
|
||||
return bookPageCount;
|
||||
},
|
||||
setPageReserve(value) {
|
||||
setPageReserve(value);
|
||||
return pageReserve;
|
||||
},
|
||||
getBookState() {
|
||||
return {
|
||||
pageCount: bookPageCount,
|
||||
pageReserve,
|
||||
progress: readingProgress,
|
||||
pagePosition: getCurrentPagePosition(),
|
||||
spreadIndex: bookPaginationState.spreadIndex,
|
||||
writtenPageLimit: bookPaginationState.writtenPageLimit
|
||||
};
|
||||
},
|
||||
navigateToPagePosition(value) {
|
||||
return navigateToPagePosition(value);
|
||||
},
|
||||
redrawPageTextures() {
|
||||
window.BookTextureRenderer?.publishSpread?.();
|
||||
return true;
|
||||
@@ -533,6 +567,31 @@ document.addEventListener('webgl-book:page-reveal-fast-forward', (event) => {
|
||||
document.addEventListener('webgl-book:reveal-committed', (event) => {
|
||||
handleRevealCommittedForPageFlip(event.detail || {});
|
||||
});
|
||||
document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||
const detail = event.detail || {};
|
||||
const previousPageCount = bookPageCount;
|
||||
bookPaginationState = {
|
||||
spreadIndex: Math.max(0, Number(detail.spreadIndex || 0)),
|
||||
spreadCount: Math.max(1, Number(detail.spreadCount || 1)),
|
||||
writtenPageLimit: Math.max(0, Number(detail.writtenPageLimit || 0))
|
||||
};
|
||||
growBookIfWritableLimitReached();
|
||||
if (bookPageCount !== previousPageCount) {
|
||||
buildBook();
|
||||
notifyBookPageCountChanged();
|
||||
window.WebGLBookPreferenceBridge?.updatePageCount?.(bookPageCount);
|
||||
}
|
||||
syncBookControls();
|
||||
});
|
||||
document.addEventListener('webgl-book:page-reserve-directive', (event) => {
|
||||
const detail = event.detail || {};
|
||||
const value = Number(detail.value);
|
||||
if (!Number.isFinite(value)) return;
|
||||
const nextReserve = detail.unit === 'percent'
|
||||
? Math.round(bookPageCount * (value / 100))
|
||||
: Math.round(value);
|
||||
setPageReserve(nextReserve);
|
||||
});
|
||||
document.addEventListener('ui:command', (event) => {
|
||||
if (event.detail?.type === 'continue' && pendingRightPageFlip) {
|
||||
pendingRightPageFlip = false;
|
||||
@@ -1640,16 +1699,68 @@ function setReadingProgress(value) {
|
||||
window.WebGLBookPreferenceBridge?.updateProgress?.(readingProgress);
|
||||
}
|
||||
|
||||
function clampPageReserve(value, pageCount = bookPageCount) {
|
||||
const parsed = Math.round(Number(value));
|
||||
if (!Number.isFinite(parsed)) return 50;
|
||||
return THREE.MathUtils.clamp(parsed, 0, Math.max(0, Math.floor(Number(pageCount) || 0)));
|
||||
}
|
||||
|
||||
function pageToSpreadIndex(pagePosition) {
|
||||
const page = Math.max(0, Math.round(Number(pagePosition || 0)));
|
||||
return page <= 0 ? 0 : Math.ceil(page / 2);
|
||||
}
|
||||
|
||||
function spreadIndexToPagePosition(spreadIndex) {
|
||||
const spread = Math.max(0, Math.round(Number(spreadIndex || 0)));
|
||||
return spread <= 0 ? 0 : Math.max(1, spread * 2 - 1);
|
||||
}
|
||||
|
||||
function getWritablePageLimit() {
|
||||
return Math.max(0, bookPageCount - pageReserve);
|
||||
}
|
||||
|
||||
function getCurrentPagePosition() {
|
||||
return spreadIndexToPagePosition(bookPaginationState.spreadIndex);
|
||||
}
|
||||
|
||||
function syncReadingProgressToCurrentPage() {
|
||||
const nextProgress = THREE.MathUtils.clamp(getCurrentPagePosition() / Math.max(1, bookPageCount), 0, 1);
|
||||
if (Math.abs(nextProgress - readingProgress) < 0.0001) return;
|
||||
readingProgress = nextProgress;
|
||||
buildBook();
|
||||
notifyBookPageCountChanged();
|
||||
window.WebGLBookPreferenceBridge?.updateProgress?.(readingProgress);
|
||||
}
|
||||
|
||||
function growBookIfWritableLimitReached() {
|
||||
const writtenLimit = Math.max(0, bookPaginationState.writtenPageLimit || 0);
|
||||
while (writtenLimit >= getWritablePageLimit() && bookPageCount < PROCEDURAL_BOOK.PAGE_COUNT_MAX) {
|
||||
bookPageCount = snapProceduralPageCount(bookPageCount + PROCEDURAL_BOOK.PAGE_COUNT_STEP);
|
||||
}
|
||||
}
|
||||
|
||||
function setBookPageCount(value) {
|
||||
const nextPageCount = snapProceduralPageCount(value);
|
||||
if (!Number.isFinite(nextPageCount)) return;
|
||||
bookPageCount = nextPageCount;
|
||||
bookPageCount = Math.max(nextPageCount, bookPageCount);
|
||||
pageReserve = clampPageReserve(pageReserve, bookPageCount);
|
||||
growBookIfWritableLimitReached();
|
||||
buildBook();
|
||||
notifyBookPageCountChanged();
|
||||
syncBookControls();
|
||||
window.WebGLBookPreferenceBridge?.updatePageCount?.(bookPageCount);
|
||||
}
|
||||
|
||||
function setPageReserve(value) {
|
||||
pageReserve = clampPageReserve(value, bookPageCount);
|
||||
growBookIfWritableLimitReached();
|
||||
buildBook();
|
||||
notifyBookPageCountChanged();
|
||||
syncBookControls();
|
||||
window.WebGLBookPreferenceBridge?.updatePageReserve?.(pageReserve);
|
||||
window.WebGLBookPreferenceBridge?.updatePageCount?.(bookPageCount);
|
||||
}
|
||||
|
||||
function notifyBookPageCountChanged() {
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-count-changed', {
|
||||
detail: {
|
||||
@@ -1664,15 +1775,18 @@ function stepReadingProgress(pageDelta) {
|
||||
}
|
||||
|
||||
function installBookControls() {
|
||||
if (!progressInput || !pageCountInput) return;
|
||||
progressInput.value = readingProgress.toFixed(3);
|
||||
pageCountInput.min = String(PROCEDURAL_BOOK.PAGE_COUNT_MIN);
|
||||
pageCountInput.max = String(PROCEDURAL_BOOK.PAGE_COUNT_MAX);
|
||||
pageCountInput.step = String(PROCEDURAL_BOOK.PAGE_COUNT_STEP);
|
||||
pageCountInput.value = String(bookPageCount);
|
||||
|
||||
progressInput.addEventListener('input', () => setReadingProgress(progressInput.value));
|
||||
pageCountInput.addEventListener('input', () => setBookPageCount(pageCountInput.value));
|
||||
ensureBottomNavigation();
|
||||
if (progressInput) {
|
||||
progressInput.value = readingProgress.toFixed(3);
|
||||
progressInput.addEventListener('input', () => setReadingProgress(progressInput.value));
|
||||
}
|
||||
if (pageCountInput) {
|
||||
pageCountInput.min = String(PROCEDURAL_BOOK.PAGE_COUNT_MIN);
|
||||
pageCountInput.max = String(PROCEDURAL_BOOK.PAGE_COUNT_MAX);
|
||||
pageCountInput.step = String(PROCEDURAL_BOOK.PAGE_COUNT_STEP);
|
||||
pageCountInput.value = String(bookPageCount);
|
||||
pageCountInput.addEventListener('input', () => setBookPageCount(pageCountInput.value));
|
||||
}
|
||||
backwardButton?.addEventListener('click', () => startPageFlip(-1));
|
||||
forwardButton?.addEventListener('click', () => startPageFlip(1));
|
||||
fastBackwardButton?.addEventListener('click', () => startFastPageFlip(-1));
|
||||
@@ -1680,6 +1794,93 @@ function installBookControls() {
|
||||
syncBookControls();
|
||||
}
|
||||
|
||||
function ensureBottomNavigation() {
|
||||
if (bottomNavigation) return bottomNavigation;
|
||||
const root = document.createElement('nav');
|
||||
root.id = 'webgl_book_navigation';
|
||||
root.setAttribute('aria-label', appInitialState.t?.('webgl.bookControls') || 'Book controls');
|
||||
|
||||
const makeButton = (id, label, icon) => {
|
||||
const button = document.createElement('button');
|
||||
button.id = id;
|
||||
button.type = 'button';
|
||||
button.className = 'webgl-book-nav-button';
|
||||
button.setAttribute('aria-label', label);
|
||||
button.title = label;
|
||||
button.textContent = icon;
|
||||
root.appendChild(button);
|
||||
return button;
|
||||
};
|
||||
|
||||
const startButton = makeButton('webgl_book_nav_start', appInitialState.t?.('webgl.returnToBeginning') || 'Return to beginning', '⏮');
|
||||
const backButton = makeButton('webgl_book_nav_back', appInitialState.t?.('webgl.backward') || 'Backward', '◀');
|
||||
const sliderWrap = document.createElement('div');
|
||||
sliderWrap.className = 'webgl-book-nav-slider-wrap';
|
||||
const pageLabel = document.createElement('output');
|
||||
pageLabel.id = 'webgl_book_nav_page_label';
|
||||
pageLabel.className = 'webgl-book-nav-page-label';
|
||||
pageLabel.textContent = '0';
|
||||
const slider = document.createElement('input');
|
||||
slider.id = 'webgl_book_nav_position';
|
||||
slider.type = 'range';
|
||||
slider.min = '0';
|
||||
slider.step = '1';
|
||||
slider.value = '0';
|
||||
sliderWrap.appendChild(slider);
|
||||
sliderWrap.appendChild(pageLabel);
|
||||
root.appendChild(sliderWrap);
|
||||
const forwardButton = makeButton('webgl_book_nav_forward', appInitialState.t?.('webgl.forward') || 'Forward', '▶');
|
||||
const endButton = makeButton('webgl_book_nav_end', appInitialState.t?.('webgl.goToEnd') || 'Go to end', '⏭');
|
||||
|
||||
startButton.addEventListener('click', () => navigateToPagePosition(0));
|
||||
backButton.addEventListener('click', () => navigateByPageDelta(-1));
|
||||
forwardButton.addEventListener('click', () => navigateByPageDelta(1));
|
||||
endButton.addEventListener('click', () => navigateToPagePosition(bookPaginationState.writtenPageLimit));
|
||||
slider.addEventListener('input', () => {
|
||||
const requested = Number(slider.value);
|
||||
const clamped = Math.min(requested, Math.max(0, bookPaginationState.writtenPageLimit || 0), getWritablePageLimit());
|
||||
if (requested !== clamped) slider.value = String(clamped);
|
||||
pageLabel.textContent = `${appInitialState.t?.('webgl.page') || 'Page'} ${clamped}`;
|
||||
});
|
||||
slider.addEventListener('change', () => navigateToPagePosition(Number(slider.value)));
|
||||
|
||||
document.body.appendChild(root);
|
||||
bottomNavigation = {
|
||||
root,
|
||||
startButton,
|
||||
backButton,
|
||||
slider,
|
||||
pageLabel,
|
||||
forwardButton,
|
||||
endButton
|
||||
};
|
||||
return bottomNavigation;
|
||||
}
|
||||
|
||||
function navigateByPageDelta(delta) {
|
||||
const current = getCurrentPagePosition();
|
||||
const next = Math.max(0, current + Math.sign(Number(delta || 0)));
|
||||
return navigateToPagePosition(next);
|
||||
}
|
||||
|
||||
function navigateToPagePosition(pagePosition) {
|
||||
const writableLimit = getWritablePageLimit();
|
||||
const writtenLimit = Math.max(0, bookPaginationState.writtenPageLimit || 0);
|
||||
const targetPage = THREE.MathUtils.clamp(Math.round(Number(pagePosition || 0)), 0, Math.min(writableLimit, writtenLimit));
|
||||
const currentPage = getCurrentPagePosition();
|
||||
if (targetPage === currentPage) {
|
||||
syncBookControls();
|
||||
return false;
|
||||
}
|
||||
const targetSpread = pageToSpreadIndex(targetPage);
|
||||
const currentSpread = bookPaginationState.spreadIndex;
|
||||
const spreadDelta = targetSpread - currentSpread;
|
||||
if (Math.abs(spreadDelta) === 1) {
|
||||
return startPageFlip(Math.sign(spreadDelta), { targetSpread });
|
||||
}
|
||||
return startFastPageFlip(Math.sign(spreadDelta), { targetSpread, skippedSpreads: Math.abs(spreadDelta) });
|
||||
}
|
||||
|
||||
function syncBookControls() {
|
||||
const busy = activeFlips.length > 0;
|
||||
if (progressInput) progressInput.value = readingProgress.toFixed(3);
|
||||
@@ -1690,6 +1891,28 @@ function syncBookControls() {
|
||||
if (fastBackwardButton) fastBackwardButton.disabled = busy || !canPageFlip(-1);
|
||||
if (forwardButton) forwardButton.disabled = busy || !canPageFlip(1);
|
||||
if (fastForwardButton) fastForwardButton.disabled = busy || !canPageFlip(1);
|
||||
syncBottomNavigation();
|
||||
}
|
||||
|
||||
function syncBottomNavigation() {
|
||||
if (!bottomNavigation) return;
|
||||
const currentPage = getCurrentPagePosition();
|
||||
const writtenLimit = Math.max(0, bookPaginationState.writtenPageLimit || 0);
|
||||
const writableLimit = getWritablePageLimit();
|
||||
const navigableLimit = Math.min(writtenLimit, writableLimit);
|
||||
const reservedStart = Math.max(0, writableLimit);
|
||||
bottomNavigation.slider.max = String(Math.max(0, bookPageCount));
|
||||
bottomNavigation.slider.value = String(Math.min(currentPage, navigableLimit));
|
||||
bottomNavigation.pageLabel.textContent = `${appInitialState.t?.('webgl.page') || 'Page'} ${Math.min(currentPage, navigableLimit)}`;
|
||||
bottomNavigation.root.style.setProperty('--book-nav-position', `${bookPageCount > 0 ? currentPage / bookPageCount : 0}`);
|
||||
bottomNavigation.root.style.setProperty('--book-nav-written', `${bookPageCount > 0 ? writtenLimit / bookPageCount : 0}`);
|
||||
bottomNavigation.root.style.setProperty('--book-nav-reserve-start', `${bookPageCount > 0 ? reservedStart / bookPageCount : 1}`);
|
||||
bottomNavigation.root.dataset.bookSize = String(bookPageCount);
|
||||
bottomNavigation.root.dataset.pageReserve = String(pageReserve);
|
||||
bottomNavigation.startButton.disabled = activeFlips.length > 0 || currentPage <= 0;
|
||||
bottomNavigation.backButton.disabled = activeFlips.length > 0 || currentPage <= 0;
|
||||
bottomNavigation.forwardButton.disabled = activeFlips.length > 0 || currentPage >= navigableLimit;
|
||||
bottomNavigation.endButton.disabled = activeFlips.length > 0 || currentPage >= navigableLimit;
|
||||
}
|
||||
|
||||
function handlePageCanvases(event) {
|
||||
@@ -2137,12 +2360,13 @@ function textureHitPageSide(hit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function startPageFlip(direction) {
|
||||
function startPageFlip(direction, options = {}) {
|
||||
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;
|
||||
flip.targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
||||
prepareStaticPageForFlip(flip);
|
||||
activeFlips.push(flip);
|
||||
syncBookControls();
|
||||
@@ -2150,20 +2374,23 @@ function startPageFlip(direction) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function startFastPageFlip(direction) {
|
||||
function startFastPageFlip(direction, options = {}) {
|
||||
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) {
|
||||
const skippedSpreads = Math.max(2, Number(options.skippedSpreads || fastFlipCount));
|
||||
const visibleFlipCount = THREE.MathUtils.clamp(Math.round(skippedSpreads), 2, 5);
|
||||
for (let index = 0; index < visibleFlipCount; index += 1) {
|
||||
activeFlips.push({
|
||||
...firstFlip,
|
||||
mesh: null,
|
||||
startTime: startTime + index * interval,
|
||||
pageOffset: index * 0.002,
|
||||
commitBundleOnFinish: index === fastFlipCount - 1,
|
||||
targetSpread: Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null,
|
||||
commitBundleOnFinish: index === visibleFlipCount - 1,
|
||||
countAsPending: false
|
||||
});
|
||||
}
|
||||
@@ -2195,12 +2422,19 @@ function createPageFlip(direction, startTime, duration) {
|
||||
function prepareStaticPageForFlip(flip) {
|
||||
if (!flip) return;
|
||||
const sourceMaterial = flip.sourcePageSide === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const oppositeMaterial = flip.sourcePageSide === 'left' ? materials.rightPage : materials.leftPage;
|
||||
const sourceTexture = sourceMaterial?.map || (flip.sourcePageSide === 'left' ? leftTexture : rightTexture);
|
||||
const backTexture = oppositeMaterial?.map || getBlankPageTexture();
|
||||
materials.flipPageSurface.map = sourceTexture;
|
||||
materials.flipPageBackSurface.map = backTexture;
|
||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||
materials.flipPageBackSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||
materials.flipPageSurface.needsUpdate = true;
|
||||
materials.flipPageBackSurface.needsUpdate = true;
|
||||
flip.sourceTexture = sourceTexture;
|
||||
flip.backTexture = backTexture;
|
||||
if (flip.direction > 0) {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
||||
@@ -2208,13 +2442,22 @@ function prepareStaticPageForFlip(flip) {
|
||||
materials.rightPage.map = blankTexture;
|
||||
materials.rightPage.needsUpdate = true;
|
||||
}
|
||||
} else if (flip.direction < 0) {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
if (blankTexture && materials.leftPage.map !== blankTexture) {
|
||||
clearPageReveal('left', 'page-flip-start');
|
||||
materials.leftPage.map = blankTexture;
|
||||
materials.leftPage.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function canPageFlip(direction) {
|
||||
if (!currentProceduralBookModel) return false;
|
||||
if (direction > 0) return readingProgress < 1;
|
||||
return readingProgress > 0;
|
||||
const currentPage = getCurrentPagePosition();
|
||||
const maxNavigablePage = Math.min(Math.max(0, bookPaginationState.writtenPageLimit || 0), getWritablePageLimit());
|
||||
if (direction > 0) return currentPage < maxNavigablePage;
|
||||
return currentPage > 0;
|
||||
}
|
||||
|
||||
function handleRevealCommittedForPageFlip(detail = {}) {
|
||||
@@ -2268,10 +2511,14 @@ function updateActiveFlips(now) {
|
||||
setActivePageGeometry(flip, surface);
|
||||
if (!flip.spreadAdvanced && t >= 0.82) {
|
||||
flip.spreadAdvanced = true;
|
||||
const targetSpread = Number.isFinite(Number(flip.targetSpread))
|
||||
? Math.max(0, Math.round(Number(flip.targetSpread)))
|
||||
: null;
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-near-end', {
|
||||
detail: {
|
||||
direction: flip.direction,
|
||||
sourceSide: flip.sourcePageSide || (flip.direction > 0 ? 'right' : 'left')
|
||||
sourceSide: flip.sourcePageSide || (flip.direction > 0 ? 'right' : 'left'),
|
||||
targetSpread
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -2395,7 +2642,11 @@ function lineYAtX(points, x) {
|
||||
function setActivePageGeometry(flip, surface) {
|
||||
const geometry = createFlippingPageGeometry(surface);
|
||||
if (!flip.mesh) {
|
||||
flip.mesh = new THREE.Mesh(geometry, materials.flipPageSurface);
|
||||
flip.mesh = new THREE.Mesh(geometry, [
|
||||
materials.flipPageSurface,
|
||||
materials.flipPageBackSurface,
|
||||
materials.flipPageEdge
|
||||
]);
|
||||
flip.mesh.castShadow = false;
|
||||
flip.mesh.receiveShadow = false;
|
||||
flip.mesh.userData.bookPart = 'flippingPage';
|
||||
@@ -2411,9 +2662,12 @@ function createFlippingPageGeometry(surface) {
|
||||
const positions = [];
|
||||
const uvs = [];
|
||||
const indices = [];
|
||||
const topIndices = [];
|
||||
const bottomIndices = [];
|
||||
const wallIndices = [];
|
||||
const topGrid = [];
|
||||
const bottomGrid = [];
|
||||
const pageThickness = 0.006;
|
||||
const pageThickness = Math.max(0.0008, Number(PROCEDURAL_BOOK.SHEET_THICKNESS_MODEL || 0.001));
|
||||
const widthSegments = surface.length - 1;
|
||||
const depthSegments = surface[0].length - 1;
|
||||
const push = (point, yOffset, u, v) => {
|
||||
@@ -2445,8 +2699,8 @@ function createFlippingPageGeometry(surface) {
|
||||
const bottomB = bottomGrid[index + 1][zIndex];
|
||||
const bottomC = bottomGrid[index][zIndex + 1];
|
||||
const bottomD = bottomGrid[index + 1][zIndex + 1];
|
||||
indices.push(a, c, b, b, c, d);
|
||||
indices.push(bottomA, bottomB, bottomC, bottomB, bottomD, bottomC);
|
||||
topIndices.push(a, c, b, b, c, d);
|
||||
bottomIndices.push(bottomA, bottomB, bottomC, bottomB, bottomD, bottomC);
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < widthSegments; index += 1) {
|
||||
@@ -2458,15 +2712,21 @@ function createFlippingPageGeometry(surface) {
|
||||
addWall(topGrid[widthSegments][zIndex], topGrid[widthSegments][zIndex + 1], bottomGrid[widthSegments][zIndex], bottomGrid[widthSegments][zIndex + 1]);
|
||||
}
|
||||
|
||||
indices.push(...topIndices, ...bottomIndices, ...wallIndices);
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setIndex(indices);
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
||||
geometry.clearGroups();
|
||||
geometry.addGroup(0, topIndices.length, 0);
|
||||
geometry.addGroup(topIndices.length, bottomIndices.length, 1);
|
||||
geometry.addGroup(topIndices.length + bottomIndices.length, wallIndices.length, 2);
|
||||
geometry.computeVertexNormals();
|
||||
return geometry;
|
||||
|
||||
function addWall(topA, topB, bottomA, bottomB) {
|
||||
indices.push(topA, bottomA, topB, topB, bottomA, bottomB);
|
||||
wallIndices.push(topA, bottomA, topB, topB, bottomA, bottomB);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2479,8 +2739,15 @@ function finishActiveFlip(flip) {
|
||||
sourceSide: flip.sourcePageSide || (flip.direction > 0 ? 'right' : 'left')
|
||||
}
|
||||
}));
|
||||
if (activeFlips.length === 0 && Number.isFinite(Number(flip.targetSpread))) {
|
||||
syncReadingProgressToCurrentPage();
|
||||
}
|
||||
if (flip.commitBundleOnFinish) {
|
||||
shiftReadingProgressByBundle(flip.direction);
|
||||
if (Number.isFinite(Number(flip.targetSpread))) {
|
||||
syncBookControls();
|
||||
} else {
|
||||
shiftReadingProgressByBundle(flip.direction);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!flip.countAsPending) {
|
||||
|
||||
Reference in New Issue
Block a user