Implement WebGL page reserve navigation
This commit is contained in:
Vendored
+2
@@ -6,6 +6,8 @@ export interface GameMetadata {
|
|||||||
version?: string;
|
version?: string;
|
||||||
copyright?: string;
|
copyright?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
bookPageCount?: number;
|
||||||
|
pageReserve?: number;
|
||||||
}
|
}
|
||||||
export interface GamePaths {
|
export interface GamePaths {
|
||||||
mainGameFile: string;
|
mainGameFile: string;
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"game-config.js","sourceRoot":"","sources":["../../src/config/game-config.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"game-config.js","sourceRoot":"","sources":["../../src/config/game-config.ts"],"names":[],"mappings":"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"}
|
||||||
@@ -2068,6 +2068,71 @@ body.webgl-mode .choice-list kbd {
|
|||||||
opacity: 0.38;
|
opacity: 0.38;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#webgl_book_navigation {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 52;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 18px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: min(820px, calc(100vw - 32px));
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 34px 34px minmax(180px, 1fr) 34px 34px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: rgba(246, 231, 201, 0.94);
|
||||||
|
background: rgba(12, 9, 7, 0.62);
|
||||||
|
border: 1px solid rgba(246, 231, 201, 0.24);
|
||||||
|
border-radius: 6px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webgl-book-nav-button {
|
||||||
|
width: 34px;
|
||||||
|
height: 30px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: rgba(246, 231, 201, 0.94);
|
||||||
|
background: rgba(44, 28, 17, 0.74);
|
||||||
|
border: 1px solid rgba(246, 231, 201, 0.26);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webgl-book-nav-button:disabled {
|
||||||
|
opacity: 0.36;
|
||||||
|
cursor: var(--default-cursor, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webgl-book-nav-slider-wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(72px, auto);
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#webgl_book_nav_position {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: #f0cd8e;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg,
|
||||||
|
rgba(240, 205, 142, 0.48) 0%,
|
||||||
|
rgba(240, 205, 142, 0.48) calc(var(--book-nav-written, 0) * 100%),
|
||||||
|
rgba(246, 231, 201, 0.18) calc(var(--book-nav-written, 0) * 100%),
|
||||||
|
rgba(246, 231, 201, 0.18) calc(var(--book-nav-reserve-start, 1) * 100%),
|
||||||
|
rgba(80, 40, 34, 0.58) calc(var(--book-nav-reserve-start, 1) * 100%),
|
||||||
|
rgba(80, 40, 34, 0.58) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webgl-book-nav-page-label {
|
||||||
|
min-width: 72px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#modal_overview {
|
#modal_overview {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 45;
|
z-index: 45;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
this.refreshToken = 0;
|
this.refreshToken = 0;
|
||||||
this.latestBlockId = 0;
|
this.latestBlockId = 0;
|
||||||
this.latestRenderedBlockId = 0;
|
this.latestRenderedBlockId = 0;
|
||||||
|
this.appliedPageReserveBlocks = new Set();
|
||||||
|
|
||||||
this.bindMethods([
|
this.bindMethods([
|
||||||
'initialize',
|
'initialize',
|
||||||
@@ -26,6 +27,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
'buildSpreads',
|
'buildSpreads',
|
||||||
'buildPages',
|
'buildPages',
|
||||||
'buildSpreadsFromPages',
|
'buildSpreadsFromPages',
|
||||||
|
'applyPageReserveDirective',
|
||||||
'createBlankPage',
|
'createBlankPage',
|
||||||
'createTitlePage',
|
'createTitlePage',
|
||||||
'ensurePage',
|
'ensurePage',
|
||||||
@@ -76,7 +78,12 @@ class BookPaginationModule extends BaseModule {
|
|||||||
});
|
});
|
||||||
this.addEventListener(document, 'webgl-book:page-flip-near-end', (event) => {
|
this.addEventListener(document, 'webgl-book:page-flip-near-end', (event) => {
|
||||||
const direction = Math.sign(Number(event.detail?.direction || 0));
|
const direction = Math.sign(Number(event.detail?.direction || 0));
|
||||||
if (direction !== 0) this.setCurrentSpread(this.currentSpreadIndex + direction);
|
const targetSpread = Number(event.detail?.targetSpread);
|
||||||
|
if (Number.isFinite(targetSpread)) {
|
||||||
|
this.setCurrentSpread(targetSpread);
|
||||||
|
} else if (direction !== 0) {
|
||||||
|
this.setCurrentSpread(this.currentSpreadIndex + direction);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.reportProgress(100, 'Book pagination ready');
|
this.reportProgress(100, 'Book pagination ready');
|
||||||
return true;
|
return true;
|
||||||
@@ -102,6 +109,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
this.latestBlockId = 0;
|
this.latestBlockId = 0;
|
||||||
this.latestRenderedBlockId = 0;
|
this.latestRenderedBlockId = 0;
|
||||||
this.currentSpreadIndex = 0;
|
this.currentSpreadIndex = 0;
|
||||||
|
this.appliedPageReserveBlocks.clear();
|
||||||
this.publish();
|
this.publish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -196,6 +204,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
|
|
||||||
source.forEach((block) => {
|
source.forEach((block) => {
|
||||||
const type = block?.kind || block?.type || 'paragraph';
|
const type = block?.kind || block?.type || 'paragraph';
|
||||||
|
this.applyPageReserveDirective(block);
|
||||||
if (type === 'image') {
|
if (type === 'image') {
|
||||||
({ pageIndex, pageLine, contentPageNumber } = this.layoutImageBlock(
|
({ pageIndex, pageLine, contentPageNumber } = this.layoutImageBlock(
|
||||||
pages,
|
pages,
|
||||||
@@ -295,6 +304,24 @@ class BookPaginationModule extends BaseModule {
|
|||||||
return spreads.filter(Boolean);
|
return spreads.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyPageReserveDirective(block = {}) {
|
||||||
|
const directive = block?.metadata?.pageReserve || block?.pageReserve || null;
|
||||||
|
const blockId = Number(block?.blockId || block?.metadata?.blockId || 0);
|
||||||
|
const gameId = block?.gameId || block?.metadata?.gameId || this.storyHistory?.currentGameId || 'default';
|
||||||
|
const key = `${gameId}:${blockId}`;
|
||||||
|
if (!directive || blockId <= 0 || this.appliedPageReserveBlocks.has(key)) return;
|
||||||
|
const value = Number(directive.value);
|
||||||
|
if (!Number.isFinite(value)) return;
|
||||||
|
this.appliedPageReserveBlocks.add(key);
|
||||||
|
document.dispatchEvent(new CustomEvent('webgl-book:page-reserve-directive', {
|
||||||
|
detail: {
|
||||||
|
blockId,
|
||||||
|
value,
|
||||||
|
unit: directive.unit === 'percent' ? 'percent' : 'pages'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
createBlankPage(index = 0, options = {}) {
|
createBlankPage(index = 0, options = {}) {
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
@@ -841,11 +868,13 @@ class BookPaginationModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
publish() {
|
publish() {
|
||||||
|
const writtenPageLimit = Math.max(0, (Math.max(0, this.spreads.length - 1) * 2) - 1);
|
||||||
document.dispatchEvent(new CustomEvent('book-pagination:spread-updated', {
|
document.dispatchEvent(new CustomEvent('book-pagination:spread-updated', {
|
||||||
detail: {
|
detail: {
|
||||||
spread: this.getCurrentSpread(),
|
spread: this.getCurrentSpread(),
|
||||||
spreadIndex: this.currentSpreadIndex,
|
spreadIndex: this.currentSpreadIndex,
|
||||||
spreadCount: this.spreads.length,
|
spreadCount: this.spreads.length,
|
||||||
|
writtenPageLimit,
|
||||||
latestBlockId: this.latestBlockId,
|
latestBlockId: this.latestBlockId,
|
||||||
latestRenderedBlockId: this.latestRenderedBlockId
|
latestRenderedBlockId: this.latestRenderedBlockId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ class GameLoopModule extends BaseModule {
|
|||||||
'requestStartGame',
|
'requestStartGame',
|
||||||
'requestSaveGame',
|
'requestSaveGame',
|
||||||
'requestLoadGame',
|
'requestLoadGame',
|
||||||
'resetClientPlaybackAndDisplay'
|
'resetClientPlaybackAndDisplay',
|
||||||
|
'getWebGLBookState',
|
||||||
|
'applyWebGLBookState'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +324,7 @@ class GameLoopModule extends BaseModule {
|
|||||||
if (typeof storyHistory.saveSlot === 'function') {
|
if (typeof storyHistory.saveSlot === 'function') {
|
||||||
await storyHistory.saveSlot(this.autoSaveSlot, {
|
await storyHistory.saveSlot(this.autoSaveSlot, {
|
||||||
inkState: null,
|
inkState: null,
|
||||||
|
webglBookState: this.getWebGLBookState(),
|
||||||
choices: [],
|
choices: [],
|
||||||
inputMode: 'none',
|
inputMode: 'none',
|
||||||
running: false
|
running: false
|
||||||
@@ -347,6 +350,7 @@ class GameLoopModule extends BaseModule {
|
|||||||
if (!isCurrentOperation()) return;
|
if (!isCurrentOperation()) return;
|
||||||
await storyHistory.saveSlot(this.autoSaveSlot, {
|
await storyHistory.saveSlot(this.autoSaveSlot, {
|
||||||
inkState: response.savedState,
|
inkState: response.savedState,
|
||||||
|
webglBookState: this.getWebGLBookState(),
|
||||||
choices: [],
|
choices: [],
|
||||||
inputMode: 'none',
|
inputMode: 'none',
|
||||||
running: true
|
running: true
|
||||||
@@ -372,6 +376,7 @@ class GameLoopModule extends BaseModule {
|
|||||||
latestRenderedBlockId: storyHistory.latestRenderedBlockId || 0,
|
latestRenderedBlockId: storyHistory.latestRenderedBlockId || 0,
|
||||||
renderedLineCount: storyHistory.renderedLineCount || 0,
|
renderedLineCount: storyHistory.renderedLineCount || 0,
|
||||||
musicState: audioManager?.getMusicState?.() || null,
|
musicState: audioManager?.getMusicState?.() || null,
|
||||||
|
webglBookState: this.getWebGLBookState(),
|
||||||
choices: this.currentChoices,
|
choices: this.currentChoices,
|
||||||
inputMode: this.currentInputMode,
|
inputMode: this.currentInputMode,
|
||||||
running: this.gameState.started && !this.gameState.ended
|
running: this.gameState.started && !this.gameState.ended
|
||||||
@@ -453,6 +458,7 @@ class GameLoopModule extends BaseModule {
|
|||||||
browserSave.renderedLineCount || 0
|
browserSave.renderedLineCount || 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.applyWebGLBookState(browserSave.webglBookState);
|
||||||
const uiController = this.getModule('ui-controller');
|
const uiController = this.getModule('ui-controller');
|
||||||
if (browserSave && uiController?.displayHandler?.restoreFromHistory) {
|
if (browserSave && uiController?.displayHandler?.restoreFromHistory) {
|
||||||
await uiController.displayHandler.restoreFromHistory(browserSave);
|
await uiController.displayHandler.restoreFromHistory(browserSave);
|
||||||
@@ -516,6 +522,17 @@ class GameLoopModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWebGLBookState() {
|
||||||
|
return window.WebGLBookPreferenceBridge?.getBookState?.()
|
||||||
|
|| window.BookLabDebug?.getBookState?.()
|
||||||
|
|| null;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyWebGLBookState(state = null) {
|
||||||
|
if (!state || typeof state !== 'object') return;
|
||||||
|
window.WebGLBookPreferenceBridge?.applyBookState?.(state);
|
||||||
|
}
|
||||||
|
|
||||||
hasUnrenderedHistory(browserSave) {
|
hasUnrenderedHistory(browserSave) {
|
||||||
return Boolean(browserSave) &&
|
return Boolean(browserSave) &&
|
||||||
Number(browserSave.latestBlockId || 0) > Number(browserSave.latestRenderedBlockId || 0);
|
Number(browserSave.latestBlockId || 0) > Number(browserSave.latestRenderedBlockId || 0);
|
||||||
@@ -565,6 +582,7 @@ class GameLoopModule extends BaseModule {
|
|||||||
latestRenderedBlockId: storyHistory.latestRenderedBlockId || 0,
|
latestRenderedBlockId: storyHistory.latestRenderedBlockId || 0,
|
||||||
renderedLineCount: storyHistory.renderedLineCount || 0,
|
renderedLineCount: storyHistory.renderedLineCount || 0,
|
||||||
musicState: audioManager?.getMusicState?.() || null,
|
musicState: audioManager?.getMusicState?.() || null,
|
||||||
|
webglBookState: this.getWebGLBookState(),
|
||||||
choices: this.currentChoices,
|
choices: this.currentChoices,
|
||||||
inputMode: this.currentInputMode,
|
inputMode: this.currentInputMode,
|
||||||
running: this.gameState.started && !this.gameState.ended
|
running: this.gameState.started && !this.gameState.ended
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class MarkupParserModule extends BaseModule {
|
|||||||
'parseImageOptions',
|
'parseImageOptions',
|
||||||
'parseSfxOptions',
|
'parseSfxOptions',
|
||||||
'parseMusicOptions',
|
'parseMusicOptions',
|
||||||
|
'parsePageReserveDirective',
|
||||||
'markdownToHtml',
|
'markdownToHtml',
|
||||||
'markdownToPlainText',
|
'markdownToPlainText',
|
||||||
'smartypants',
|
'smartypants',
|
||||||
@@ -178,11 +179,14 @@ class MarkupParserModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseParagraph(rawText) {
|
parseParagraph(rawText) {
|
||||||
const inline = this.parseInline(this.normalizeParagraph(rawText));
|
const normalized = this.normalizeParagraph(rawText);
|
||||||
|
const reserveDirective = this.parsePageReserveDirective(normalized);
|
||||||
|
const inline = this.parseInline(reserveDirective.text);
|
||||||
return {
|
return {
|
||||||
text: this.markdownToPlainText(inline.text),
|
text: this.markdownToPlainText(inline.text),
|
||||||
layoutText: this.markdownToHtml(inline.text),
|
layoutText: this.markdownToHtml(inline.text),
|
||||||
cueMarkers: inline.cueMarkers
|
cueMarkers: inline.cueMarkers,
|
||||||
|
pageReserve: reserveDirective.directive
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,12 +197,34 @@ class MarkupParserModule extends BaseModule {
|
|||||||
layoutText: paragraph.layoutText,
|
layoutText: paragraph.layoutText,
|
||||||
cueMarkers: paragraph.cueMarkers,
|
cueMarkers: paragraph.cueMarkers,
|
||||||
role,
|
role,
|
||||||
|
metadata: {
|
||||||
|
...(paragraph.pageReserve ? { pageReserve: paragraph.pageReserve } : {})
|
||||||
|
},
|
||||||
isFirstParagraphInChapter: role === 'chapter-first' || role === 'textblock-first',
|
isFirstParagraphInChapter: role === 'chapter-first' || role === 'textblock-first',
|
||||||
dropCap: role === 'chapter-first',
|
dropCap: role === 'chapter-first',
|
||||||
addTopSpace: role === 'textblock-first'
|
addTopSpace: role === 'textblock-first'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsePageReserveDirective(text) {
|
||||||
|
const source = String(text || '');
|
||||||
|
const match = source.match(/#pagereserve\[\s*([0-9]+(?:\.[0-9]+)?)\s*(%)?\s*\]/i);
|
||||||
|
if (!match) {
|
||||||
|
return { text: source, directive: null };
|
||||||
|
}
|
||||||
|
const value = Number(match[1]);
|
||||||
|
const directive = Number.isFinite(value)
|
||||||
|
? {
|
||||||
|
value,
|
||||||
|
unit: match[2] === '%' ? 'percent' : 'pages'
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
text: source.replace(match[0], '').replace(/\s{2,}/g, ' ').trim(),
|
||||||
|
directive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
parseInline(text) {
|
parseInline(text) {
|
||||||
return {
|
return {
|
||||||
text: String(text || '').replace(/\s{2,}/g, ' ').trim(),
|
text: String(text || '').replace(/\s{2,}/g, ' ').trim(),
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ class OptionsUIModule extends BaseModule {
|
|||||||
'setupApiUrlFields',
|
'setupApiUrlFields',
|
||||||
'setupInitialState',
|
'setupInitialState',
|
||||||
'dispatchApiChangeEvent',
|
'dispatchApiChangeEvent',
|
||||||
|
'getMetadataNumber',
|
||||||
|
'hasFixedBookPageCount',
|
||||||
|
'hasFixedPageReserve',
|
||||||
'getPreference',
|
'getPreference',
|
||||||
'updatePreference',
|
'updatePreference',
|
||||||
'updateUIText',
|
'updateUIText',
|
||||||
@@ -92,6 +95,25 @@ class OptionsUIModule extends BaseModule {
|
|||||||
detail: { provider, [valueType]: value }
|
detail: { provider, [valueType]: value }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMetadataNumber(keys = []) {
|
||||||
|
const gameConfig = this.getModule('game-config');
|
||||||
|
const metadata = gameConfig?.getMetadata?.() || {};
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(metadata, key)) continue;
|
||||||
|
const value = Number(metadata[key]);
|
||||||
|
if (Number.isFinite(value)) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFixedBookPageCount() {
|
||||||
|
return Number.isFinite(this.getMetadataNumber(['bookPageCount', 'defaultBookPageCount', 'webglBookPageCount']));
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFixedPageReserve() {
|
||||||
|
return Number.isFinite(this.getMetadataNumber(['pageReserve', 'defaultPageReserve', 'webglPageReserve']));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a preference from the persistence manager
|
* Gets a preference from the persistence manager
|
||||||
@@ -302,32 +324,36 @@ class OptionsUIModule extends BaseModule {
|
|||||||
'data-pref-transform': 'integer:40,500'
|
'data-pref-transform': 'integer:40,500'
|
||||||
}, null, bookSizeContainer);
|
}, null, bookSizeContainer);
|
||||||
this.elements.webglBookSize.addEventListener('input', () => this.updateWebGLDisplays());
|
this.elements.webglBookSize.addEventListener('input', () => this.updateWebGLDisplays());
|
||||||
webglSection.appendChild(bookSizeContainer);
|
if (!this.hasFixedBookPageCount()) {
|
||||||
|
webglSection.appendChild(bookSizeContainer);
|
||||||
|
}
|
||||||
|
|
||||||
const bookProgressContainer = document.createElement('div');
|
const pageReserveContainer = document.createElement('div');
|
||||||
bookProgressContainer.className = 'option-item';
|
pageReserveContainer.className = 'option-item';
|
||||||
|
|
||||||
const bookProgressLabel = document.createElement('label');
|
const pageReserveLabel = document.createElement('label');
|
||||||
bookProgressLabel.textContent = this.t('options.bookProgress') + ':';
|
pageReserveLabel.textContent = this.t('options.pageReserve') + ':';
|
||||||
bookProgressContainer.appendChild(bookProgressLabel);
|
pageReserveContainer.appendChild(pageReserveLabel);
|
||||||
|
|
||||||
const bookProgressValue = document.createElement('span');
|
const pageReserveValue = document.createElement('span');
|
||||||
bookProgressValue.className = 'slider-value';
|
pageReserveValue.className = 'slider-value';
|
||||||
bookProgressValue.textContent = '50%';
|
pageReserveValue.textContent = '50';
|
||||||
this.elements.webglBookProgressValue = bookProgressValue;
|
this.elements.webglPageReserveValue = pageReserveValue;
|
||||||
bookProgressContainer.appendChild(bookProgressValue);
|
pageReserveContainer.appendChild(pageReserveValue);
|
||||||
|
|
||||||
this.elements.webglBookProgress = createUIElement('input', {
|
this.elements.webglPageReserve = createUIElement('input', {
|
||||||
type: 'range',
|
type: 'range',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 500,
|
||||||
step: 1,
|
step: 1,
|
||||||
value: 50,
|
value: 50,
|
||||||
'data-pref-bind': 'webgl.bookProgress',
|
'data-pref-bind': 'webgl.pageReserve',
|
||||||
'data-pref-transform': 'range:0,1'
|
'data-pref-transform': 'integer:0,500'
|
||||||
}, null, bookProgressContainer);
|
}, null, pageReserveContainer);
|
||||||
this.elements.webglBookProgress.addEventListener('input', () => this.updateWebGLDisplays());
|
this.elements.webglPageReserve.addEventListener('input', () => this.updateWebGLDisplays());
|
||||||
webglSection.appendChild(bookProgressContainer);
|
if (!this.hasFixedPageReserve()) {
|
||||||
|
webglSection.appendChild(pageReserveContainer);
|
||||||
|
}
|
||||||
|
|
||||||
body.appendChild(webglSection);
|
body.appendChild(webglSection);
|
||||||
|
|
||||||
@@ -1246,8 +1272,14 @@ class OptionsUIModule extends BaseModule {
|
|||||||
if (this.elements.webglBookSize && this.elements.webglBookSizeValue) {
|
if (this.elements.webglBookSize && this.elements.webglBookSizeValue) {
|
||||||
this.elements.webglBookSizeValue.textContent = String(this.elements.webglBookSize.value);
|
this.elements.webglBookSizeValue.textContent = String(this.elements.webglBookSize.value);
|
||||||
}
|
}
|
||||||
if (this.elements.webglBookProgress && this.elements.webglBookProgressValue) {
|
if (this.elements.webglPageReserve && this.elements.webglPageReserveValue) {
|
||||||
this.elements.webglBookProgressValue.textContent = `${this.elements.webglBookProgress.value}%`;
|
const bookSize = Number(this.elements.webglBookSize?.value || this.getPreference('webgl', 'bookPageCount', 300));
|
||||||
|
const maxReserve = Number.isFinite(bookSize) ? Math.max(0, Math.floor(bookSize)) : 500;
|
||||||
|
this.elements.webglPageReserve.max = String(maxReserve);
|
||||||
|
if (Number(this.elements.webglPageReserve.value) > maxReserve) {
|
||||||
|
this.elements.webglPageReserve.value = String(maxReserve);
|
||||||
|
}
|
||||||
|
this.elements.webglPageReserveValue.textContent = String(this.elements.webglPageReserve.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ class PersistenceManagerModule extends BaseModule {
|
|||||||
webgl: {
|
webgl: {
|
||||||
mode: null,
|
mode: null,
|
||||||
bookPageCount: 300,
|
bookPageCount: 300,
|
||||||
bookProgress: 0.5
|
bookProgress: 0,
|
||||||
|
pageReserve: 50
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+291
-24
@@ -187,7 +187,8 @@ const book = new THREE.Group();
|
|||||||
scene.add(book);
|
scene.add(book);
|
||||||
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0'), 0, 1);
|
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0'), 0, 1);
|
||||||
let readingProgress = Number.isFinite(initialReadingProgress) ? initialReadingProgress : 0;
|
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;
|
let currentProceduralBookModel = null;
|
||||||
const progressInput = document.getElementById('progress_control');
|
const progressInput = document.getElementById('progress_control');
|
||||||
const progressValue = document.getElementById('progress_value');
|
const progressValue = document.getElementById('progress_value');
|
||||||
@@ -197,6 +198,12 @@ const fastBackwardButton = document.getElementById('fast_backward');
|
|||||||
const backwardButton = document.getElementById('flip_backward');
|
const backwardButton = document.getElementById('flip_backward');
|
||||||
const forwardButton = document.getElementById('flip_forward');
|
const forwardButton = document.getElementById('flip_forward');
|
||||||
const fastForwardButton = document.getElementById('fast_forward');
|
const fastForwardButton = document.getElementById('fast_forward');
|
||||||
|
let bottomNavigation = null;
|
||||||
|
let bookPaginationState = {
|
||||||
|
spreadIndex: 0,
|
||||||
|
spreadCount: 1,
|
||||||
|
writtenPageLimit: 0
|
||||||
|
};
|
||||||
const normalFlipDuration = 900;
|
const normalFlipDuration = 900;
|
||||||
const fastFlipDuration = 520;
|
const fastFlipDuration = 520;
|
||||||
const fastFlipCount = 10;
|
const fastFlipCount = 10;
|
||||||
@@ -399,6 +406,14 @@ const materials = {
|
|||||||
envMapIntensity: 0
|
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 = {
|
materials.leftPage.userData.bookPageReveal = {
|
||||||
side: 'left'
|
side: 'left'
|
||||||
};
|
};
|
||||||
@@ -421,6 +436,8 @@ configureBookShadowReceiver(materials.pageBlock, 0.18);
|
|||||||
configureBookShadowReceiver(materials.pageEdge, 0.16);
|
configureBookShadowReceiver(materials.pageEdge, 0.16);
|
||||||
configureBookShadowReceiver(materials.pageSurface, 0.11);
|
configureBookShadowReceiver(materials.pageSurface, 0.11);
|
||||||
configureBookShadowReceiver(materials.flipPageSurface, 0.11);
|
configureBookShadowReceiver(materials.flipPageSurface, 0.11);
|
||||||
|
configureBookShadowReceiver(materials.flipPageBackSurface, 0.11);
|
||||||
|
configureBookShadowReceiver(materials.flipPageEdge, 0.09);
|
||||||
configureBookShadowReceiver(materials.leftPage, 0.08);
|
configureBookShadowReceiver(materials.leftPage, 0.08);
|
||||||
configureBookShadowReceiver(materials.rightPage, 0.08);
|
configureBookShadowReceiver(materials.rightPage, 0.08);
|
||||||
configureBookShadowReceiver(materials.spineCloth, 0.48);
|
configureBookShadowReceiver(materials.spineCloth, 0.48);
|
||||||
@@ -498,6 +515,23 @@ window.BookLabDebug = {
|
|||||||
setBookPageCount(value);
|
setBookPageCount(value);
|
||||||
return bookPageCount;
|
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() {
|
redrawPageTextures() {
|
||||||
window.BookTextureRenderer?.publishSpread?.();
|
window.BookTextureRenderer?.publishSpread?.();
|
||||||
return true;
|
return true;
|
||||||
@@ -533,6 +567,31 @@ document.addEventListener('webgl-book:page-reveal-fast-forward', (event) => {
|
|||||||
document.addEventListener('webgl-book:reveal-committed', (event) => {
|
document.addEventListener('webgl-book:reveal-committed', (event) => {
|
||||||
handleRevealCommittedForPageFlip(event.detail || {});
|
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) => {
|
document.addEventListener('ui:command', (event) => {
|
||||||
if (event.detail?.type === 'continue' && pendingRightPageFlip) {
|
if (event.detail?.type === 'continue' && pendingRightPageFlip) {
|
||||||
pendingRightPageFlip = false;
|
pendingRightPageFlip = false;
|
||||||
@@ -1640,16 +1699,68 @@ function setReadingProgress(value) {
|
|||||||
window.WebGLBookPreferenceBridge?.updateProgress?.(readingProgress);
|
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) {
|
function setBookPageCount(value) {
|
||||||
const nextPageCount = snapProceduralPageCount(value);
|
const nextPageCount = snapProceduralPageCount(value);
|
||||||
if (!Number.isFinite(nextPageCount)) return;
|
if (!Number.isFinite(nextPageCount)) return;
|
||||||
bookPageCount = nextPageCount;
|
bookPageCount = Math.max(nextPageCount, bookPageCount);
|
||||||
|
pageReserve = clampPageReserve(pageReserve, bookPageCount);
|
||||||
|
growBookIfWritableLimitReached();
|
||||||
buildBook();
|
buildBook();
|
||||||
notifyBookPageCountChanged();
|
notifyBookPageCountChanged();
|
||||||
syncBookControls();
|
syncBookControls();
|
||||||
window.WebGLBookPreferenceBridge?.updatePageCount?.(bookPageCount);
|
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() {
|
function notifyBookPageCountChanged() {
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:page-count-changed', {
|
document.dispatchEvent(new CustomEvent('webgl-book:page-count-changed', {
|
||||||
detail: {
|
detail: {
|
||||||
@@ -1664,15 +1775,18 @@ function stepReadingProgress(pageDelta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function installBookControls() {
|
function installBookControls() {
|
||||||
if (!progressInput || !pageCountInput) return;
|
ensureBottomNavigation();
|
||||||
progressInput.value = readingProgress.toFixed(3);
|
if (progressInput) {
|
||||||
pageCountInput.min = String(PROCEDURAL_BOOK.PAGE_COUNT_MIN);
|
progressInput.value = readingProgress.toFixed(3);
|
||||||
pageCountInput.max = String(PROCEDURAL_BOOK.PAGE_COUNT_MAX);
|
progressInput.addEventListener('input', () => setReadingProgress(progressInput.value));
|
||||||
pageCountInput.step = String(PROCEDURAL_BOOK.PAGE_COUNT_STEP);
|
}
|
||||||
pageCountInput.value = String(bookPageCount);
|
if (pageCountInput) {
|
||||||
|
pageCountInput.min = String(PROCEDURAL_BOOK.PAGE_COUNT_MIN);
|
||||||
progressInput.addEventListener('input', () => setReadingProgress(progressInput.value));
|
pageCountInput.max = String(PROCEDURAL_BOOK.PAGE_COUNT_MAX);
|
||||||
pageCountInput.addEventListener('input', () => setBookPageCount(pageCountInput.value));
|
pageCountInput.step = String(PROCEDURAL_BOOK.PAGE_COUNT_STEP);
|
||||||
|
pageCountInput.value = String(bookPageCount);
|
||||||
|
pageCountInput.addEventListener('input', () => setBookPageCount(pageCountInput.value));
|
||||||
|
}
|
||||||
backwardButton?.addEventListener('click', () => startPageFlip(-1));
|
backwardButton?.addEventListener('click', () => startPageFlip(-1));
|
||||||
forwardButton?.addEventListener('click', () => startPageFlip(1));
|
forwardButton?.addEventListener('click', () => startPageFlip(1));
|
||||||
fastBackwardButton?.addEventListener('click', () => startFastPageFlip(-1));
|
fastBackwardButton?.addEventListener('click', () => startFastPageFlip(-1));
|
||||||
@@ -1680,6 +1794,93 @@ function installBookControls() {
|
|||||||
syncBookControls();
|
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() {
|
function syncBookControls() {
|
||||||
const busy = activeFlips.length > 0;
|
const busy = activeFlips.length > 0;
|
||||||
if (progressInput) progressInput.value = readingProgress.toFixed(3);
|
if (progressInput) progressInput.value = readingProgress.toFixed(3);
|
||||||
@@ -1690,6 +1891,28 @@ function syncBookControls() {
|
|||||||
if (fastBackwardButton) fastBackwardButton.disabled = busy || !canPageFlip(-1);
|
if (fastBackwardButton) fastBackwardButton.disabled = busy || !canPageFlip(-1);
|
||||||
if (forwardButton) forwardButton.disabled = busy || !canPageFlip(1);
|
if (forwardButton) forwardButton.disabled = busy || !canPageFlip(1);
|
||||||
if (fastForwardButton) fastForwardButton.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) {
|
function handlePageCanvases(event) {
|
||||||
@@ -2137,12 +2360,13 @@ function textureHitPageSide(hit) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPageFlip(direction) {
|
function startPageFlip(direction, options = {}) {
|
||||||
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
||||||
pendingRightPageFlip = false;
|
pendingRightPageFlip = false;
|
||||||
delete document.documentElement.dataset.webglPendingPageFlip;
|
delete document.documentElement.dataset.webglPendingPageFlip;
|
||||||
const flip = createPageFlip(direction, performance.now(), normalFlipDuration);
|
const flip = createPageFlip(direction, performance.now(), normalFlipDuration);
|
||||||
if (!flip) return false;
|
if (!flip) return false;
|
||||||
|
flip.targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
||||||
prepareStaticPageForFlip(flip);
|
prepareStaticPageForFlip(flip);
|
||||||
activeFlips.push(flip);
|
activeFlips.push(flip);
|
||||||
syncBookControls();
|
syncBookControls();
|
||||||
@@ -2150,20 +2374,23 @@ function startPageFlip(direction) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startFastPageFlip(direction) {
|
function startFastPageFlip(direction, options = {}) {
|
||||||
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
||||||
const firstFlip = createPageFlip(direction, performance.now(), fastFlipDuration);
|
const firstFlip = createPageFlip(direction, performance.now(), fastFlipDuration);
|
||||||
if (!firstFlip) return false;
|
if (!firstFlip) return false;
|
||||||
prepareStaticPageForFlip(firstFlip);
|
prepareStaticPageForFlip(firstFlip);
|
||||||
const startTime = firstFlip.startTime;
|
const startTime = firstFlip.startTime;
|
||||||
const interval = fastFlipDuration / fastFlipOverlap;
|
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({
|
activeFlips.push({
|
||||||
...firstFlip,
|
...firstFlip,
|
||||||
mesh: null,
|
mesh: null,
|
||||||
startTime: startTime + index * interval,
|
startTime: startTime + index * interval,
|
||||||
pageOffset: index * 0.002,
|
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
|
countAsPending: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2195,12 +2422,19 @@ function createPageFlip(direction, startTime, duration) {
|
|||||||
function prepareStaticPageForFlip(flip) {
|
function prepareStaticPageForFlip(flip) {
|
||||||
if (!flip) return;
|
if (!flip) return;
|
||||||
const sourceMaterial = flip.sourcePageSide === 'left' ? materials.leftPage : materials.rightPage;
|
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 sourceTexture = sourceMaterial?.map || (flip.sourcePageSide === 'left' ? leftTexture : rightTexture);
|
||||||
|
const backTexture = oppositeMaterial?.map || getBlankPageTexture();
|
||||||
materials.flipPageSurface.map = sourceTexture;
|
materials.flipPageSurface.map = sourceTexture;
|
||||||
|
materials.flipPageBackSurface.map = backTexture;
|
||||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||||
|
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
|
||||||
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||||
|
materials.flipPageBackSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||||
materials.flipPageSurface.needsUpdate = true;
|
materials.flipPageSurface.needsUpdate = true;
|
||||||
|
materials.flipPageBackSurface.needsUpdate = true;
|
||||||
flip.sourceTexture = sourceTexture;
|
flip.sourceTexture = sourceTexture;
|
||||||
|
flip.backTexture = backTexture;
|
||||||
if (flip.direction > 0) {
|
if (flip.direction > 0) {
|
||||||
const blankTexture = getBlankPageTexture();
|
const blankTexture = getBlankPageTexture();
|
||||||
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
||||||
@@ -2208,13 +2442,22 @@ function prepareStaticPageForFlip(flip) {
|
|||||||
materials.rightPage.map = blankTexture;
|
materials.rightPage.map = blankTexture;
|
||||||
materials.rightPage.needsUpdate = true;
|
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) {
|
function canPageFlip(direction) {
|
||||||
if (!currentProceduralBookModel) return false;
|
if (!currentProceduralBookModel) return false;
|
||||||
if (direction > 0) return readingProgress < 1;
|
const currentPage = getCurrentPagePosition();
|
||||||
return readingProgress > 0;
|
const maxNavigablePage = Math.min(Math.max(0, bookPaginationState.writtenPageLimit || 0), getWritablePageLimit());
|
||||||
|
if (direction > 0) return currentPage < maxNavigablePage;
|
||||||
|
return currentPage > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRevealCommittedForPageFlip(detail = {}) {
|
function handleRevealCommittedForPageFlip(detail = {}) {
|
||||||
@@ -2268,10 +2511,14 @@ function updateActiveFlips(now) {
|
|||||||
setActivePageGeometry(flip, surface);
|
setActivePageGeometry(flip, surface);
|
||||||
if (!flip.spreadAdvanced && t >= 0.82) {
|
if (!flip.spreadAdvanced && t >= 0.82) {
|
||||||
flip.spreadAdvanced = true;
|
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', {
|
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-near-end', {
|
||||||
detail: {
|
detail: {
|
||||||
direction: flip.direction,
|
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) {
|
function setActivePageGeometry(flip, surface) {
|
||||||
const geometry = createFlippingPageGeometry(surface);
|
const geometry = createFlippingPageGeometry(surface);
|
||||||
if (!flip.mesh) {
|
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.castShadow = false;
|
||||||
flip.mesh.receiveShadow = false;
|
flip.mesh.receiveShadow = false;
|
||||||
flip.mesh.userData.bookPart = 'flippingPage';
|
flip.mesh.userData.bookPart = 'flippingPage';
|
||||||
@@ -2411,9 +2662,12 @@ function createFlippingPageGeometry(surface) {
|
|||||||
const positions = [];
|
const positions = [];
|
||||||
const uvs = [];
|
const uvs = [];
|
||||||
const indices = [];
|
const indices = [];
|
||||||
|
const topIndices = [];
|
||||||
|
const bottomIndices = [];
|
||||||
|
const wallIndices = [];
|
||||||
const topGrid = [];
|
const topGrid = [];
|
||||||
const bottomGrid = [];
|
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 widthSegments = surface.length - 1;
|
||||||
const depthSegments = surface[0].length - 1;
|
const depthSegments = surface[0].length - 1;
|
||||||
const push = (point, yOffset, u, v) => {
|
const push = (point, yOffset, u, v) => {
|
||||||
@@ -2445,8 +2699,8 @@ function createFlippingPageGeometry(surface) {
|
|||||||
const bottomB = bottomGrid[index + 1][zIndex];
|
const bottomB = bottomGrid[index + 1][zIndex];
|
||||||
const bottomC = bottomGrid[index][zIndex + 1];
|
const bottomC = bottomGrid[index][zIndex + 1];
|
||||||
const bottomD = bottomGrid[index + 1][zIndex + 1];
|
const bottomD = bottomGrid[index + 1][zIndex + 1];
|
||||||
indices.push(a, c, b, b, c, d);
|
topIndices.push(a, c, b, b, c, d);
|
||||||
indices.push(bottomA, bottomB, bottomC, bottomB, bottomD, bottomC);
|
bottomIndices.push(bottomA, bottomB, bottomC, bottomB, bottomD, bottomC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let index = 0; index < widthSegments; index += 1) {
|
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]);
|
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();
|
const geometry = new THREE.BufferGeometry();
|
||||||
geometry.setIndex(indices);
|
geometry.setIndex(indices);
|
||||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||||
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
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();
|
geometry.computeVertexNormals();
|
||||||
return geometry;
|
return geometry;
|
||||||
|
|
||||||
function addWall(topA, topB, bottomA, bottomB) {
|
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')
|
sourceSide: flip.sourcePageSide || (flip.direction > 0 ? 'right' : 'left')
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
if (activeFlips.length === 0 && Number.isFinite(Number(flip.targetSpread))) {
|
||||||
|
syncReadingProgressToCurrentPage();
|
||||||
|
}
|
||||||
if (flip.commitBundleOnFinish) {
|
if (flip.commitBundleOnFinish) {
|
||||||
shiftReadingProgressByBundle(flip.direction);
|
if (Number.isFinite(Number(flip.targetSpread))) {
|
||||||
|
syncBookControls();
|
||||||
|
} else {
|
||||||
|
shiftReadingProgressByBundle(flip.direction);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!flip.countAsPending) {
|
if (!flip.countAsPending) {
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import { BaseModule } from './base-module.js';
|
|||||||
|
|
||||||
const DEFAULT_BOOK_PAGE_COUNT = 300;
|
const DEFAULT_BOOK_PAGE_COUNT = 300;
|
||||||
const DEFAULT_BOOK_PROGRESS = 0;
|
const DEFAULT_BOOK_PROGRESS = 0;
|
||||||
|
const DEFAULT_PAGE_RESERVE = 50;
|
||||||
|
|
||||||
class WebGLBookSceneModule extends BaseModule {
|
class WebGLBookSceneModule extends BaseModule {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('webgl-book-scene', 'WebGL Book Scene');
|
super('webgl-book-scene', 'WebGL Book Scene');
|
||||||
this.dependencies = ['persistence-manager', 'localization', 'book-texture-renderer'];
|
this.dependencies = ['persistence-manager', 'localization', 'game-config', 'book-texture-renderer'];
|
||||||
this.persistenceManager = null;
|
this.persistenceManager = null;
|
||||||
this.localization = null;
|
this.localization = null;
|
||||||
|
this.gameConfig = null;
|
||||||
this.mode = '2d';
|
this.mode = '2d';
|
||||||
this.is3dSupported = false;
|
this.is3dSupported = false;
|
||||||
this.labImportPromise = null;
|
this.labImportPromise = null;
|
||||||
@@ -29,6 +31,9 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
'ensureShell',
|
'ensureShell',
|
||||||
'initializeScene',
|
'initializeScene',
|
||||||
'detectWebGLSupport',
|
'detectWebGLSupport',
|
||||||
|
'getMetadataNumber',
|
||||||
|
'getFixedBookPageCount',
|
||||||
|
'getFixedPageReserve',
|
||||||
'createLabHost',
|
'createLabHost',
|
||||||
'installPreferenceBridge',
|
'installPreferenceBridge',
|
||||||
'installTextureEventBridge',
|
'installTextureEventBridge',
|
||||||
@@ -49,6 +54,7 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
async initialize() {
|
async initialize() {
|
||||||
this.persistenceManager = this.getModule('persistence-manager');
|
this.persistenceManager = this.getModule('persistence-manager');
|
||||||
this.localization = this.getModule('localization');
|
this.localization = this.getModule('localization');
|
||||||
|
this.gameConfig = this.getModule('game-config');
|
||||||
|
|
||||||
this.reportProgress(15, 'Checking WebGL support');
|
this.reportProgress(15, 'Checking WebGL support');
|
||||||
this.is3dSupported = this.detectWebGLSupport();
|
this.is3dSupported = this.detectWebGLSupport();
|
||||||
@@ -76,14 +82,42 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
|
|
||||||
initializeScenePreferences() {
|
initializeScenePreferences() {
|
||||||
if (!this.persistenceManager) return;
|
if (!this.persistenceManager) return;
|
||||||
|
const fixedPageCount = this.getFixedBookPageCount();
|
||||||
|
const fixedPageReserve = this.getFixedPageReserve();
|
||||||
const scenePrefs = this.persistenceManager.getPreference('webgl', 'bookPageCount', null);
|
const scenePrefs = this.persistenceManager.getPreference('webgl', 'bookPageCount', null);
|
||||||
if (!Number.isFinite(Number(scenePrefs))) {
|
if (Number.isFinite(fixedPageCount)) {
|
||||||
|
this.persistenceManager.updatePreference('webgl', 'bookPageCount', fixedPageCount);
|
||||||
|
} else if (!Number.isFinite(Number(scenePrefs))) {
|
||||||
this.persistenceManager.updatePreference('webgl', 'bookPageCount', DEFAULT_BOOK_PAGE_COUNT);
|
this.persistenceManager.updatePreference('webgl', 'bookPageCount', DEFAULT_BOOK_PAGE_COUNT);
|
||||||
}
|
}
|
||||||
const progress = this.persistenceManager.getPreference('webgl', 'bookProgress', null);
|
const progress = this.persistenceManager.getPreference('webgl', 'bookProgress', null);
|
||||||
if (!Number.isFinite(Number(progress))) {
|
if (!Number.isFinite(Number(progress))) {
|
||||||
this.persistenceManager.updatePreference('webgl', 'bookProgress', DEFAULT_BOOK_PROGRESS);
|
this.persistenceManager.updatePreference('webgl', 'bookProgress', DEFAULT_BOOK_PROGRESS);
|
||||||
}
|
}
|
||||||
|
const pageReserve = this.persistenceManager.getPreference('webgl', 'pageReserve', null);
|
||||||
|
if (Number.isFinite(fixedPageReserve)) {
|
||||||
|
this.persistenceManager.updatePreference('webgl', 'pageReserve', fixedPageReserve);
|
||||||
|
} else if (!Number.isFinite(Number(pageReserve))) {
|
||||||
|
this.persistenceManager.updatePreference('webgl', 'pageReserve', DEFAULT_PAGE_RESERVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadataNumber(keys = []) {
|
||||||
|
const metadata = this.gameConfig?.getMetadata?.() || {};
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(metadata, key)) continue;
|
||||||
|
const value = Number(metadata[key]);
|
||||||
|
if (Number.isFinite(value)) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFixedBookPageCount() {
|
||||||
|
return this.getMetadataNumber(['bookPageCount', 'defaultBookPageCount', 'webglBookPageCount']);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFixedPageReserve() {
|
||||||
|
return this.getMetadataNumber(['pageReserve', 'defaultPageReserve', 'webglPageReserve']);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveInitialMode() {
|
resolveInitialMode() {
|
||||||
@@ -166,10 +200,15 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
|
|
||||||
const pageCount = this.persistenceManager?.getPreference?.('webgl', 'bookPageCount', DEFAULT_BOOK_PAGE_COUNT) ?? DEFAULT_BOOK_PAGE_COUNT;
|
const pageCount = this.persistenceManager?.getPreference?.('webgl', 'bookPageCount', DEFAULT_BOOK_PAGE_COUNT) ?? DEFAULT_BOOK_PAGE_COUNT;
|
||||||
const progress = this.persistenceManager?.getPreference?.('webgl', 'bookProgress', DEFAULT_BOOK_PROGRESS) ?? DEFAULT_BOOK_PROGRESS;
|
const progress = this.persistenceManager?.getPreference?.('webgl', 'bookProgress', DEFAULT_BOOK_PROGRESS) ?? DEFAULT_BOOK_PROGRESS;
|
||||||
|
const pageReserve = this.persistenceManager?.getPreference?.('webgl', 'pageReserve', DEFAULT_PAGE_RESERVE) ?? DEFAULT_PAGE_RESERVE;
|
||||||
window.WebGLBookInitialState = {
|
window.WebGLBookInitialState = {
|
||||||
appMode: true,
|
appMode: true,
|
||||||
pageCount,
|
pageCount,
|
||||||
progress,
|
progress,
|
||||||
|
pageReserve,
|
||||||
|
fixedPageCount: this.getFixedBookPageCount(),
|
||||||
|
fixedPageReserve: this.getFixedPageReserve(),
|
||||||
|
t: (key, params = {}) => this.t(key, params),
|
||||||
reportProgress: (percent, message) => {
|
reportProgress: (percent, message) => {
|
||||||
this.reportProgress(percent, message);
|
this.reportProgress(percent, message);
|
||||||
}
|
}
|
||||||
@@ -283,6 +322,34 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
this.preferenceWriteGuard = true;
|
this.preferenceWriteGuard = true;
|
||||||
this.persistenceManager?.updatePreference?.('webgl', 'bookPageCount', value);
|
this.persistenceManager?.updatePreference?.('webgl', 'bookPageCount', value);
|
||||||
this.preferenceWriteGuard = false;
|
this.preferenceWriteGuard = false;
|
||||||
|
},
|
||||||
|
updatePageReserve: (value) => {
|
||||||
|
if (this.preferenceWriteGuard) return;
|
||||||
|
this.preferenceWriteGuard = true;
|
||||||
|
this.persistenceManager?.updatePreference?.('webgl', 'pageReserve', value);
|
||||||
|
this.preferenceWriteGuard = false;
|
||||||
|
},
|
||||||
|
getBookState: () => window.BookLabDebug?.getBookState?.() || {
|
||||||
|
pageCount: this.persistenceManager?.getPreference?.('webgl', 'bookPageCount', DEFAULT_BOOK_PAGE_COUNT) ?? DEFAULT_BOOK_PAGE_COUNT,
|
||||||
|
pageReserve: this.persistenceManager?.getPreference?.('webgl', 'pageReserve', DEFAULT_PAGE_RESERVE) ?? DEFAULT_PAGE_RESERVE,
|
||||||
|
progress: this.persistenceManager?.getPreference?.('webgl', 'bookProgress', DEFAULT_BOOK_PROGRESS) ?? DEFAULT_BOOK_PROGRESS
|
||||||
|
},
|
||||||
|
applyBookState: (state = {}) => {
|
||||||
|
const pageCount = Number(state.pageCount);
|
||||||
|
const pageReserve = Number(state.pageReserve);
|
||||||
|
const progress = Number(state.progress);
|
||||||
|
if (Number.isFinite(pageCount)) {
|
||||||
|
this.persistenceManager?.updatePreference?.('webgl', 'bookPageCount', pageCount);
|
||||||
|
window.BookLabDebug?.setBookPageCount?.(pageCount);
|
||||||
|
}
|
||||||
|
if (Number.isFinite(pageReserve)) {
|
||||||
|
this.persistenceManager?.updatePreference?.('webgl', 'pageReserve', pageReserve);
|
||||||
|
window.BookLabDebug?.setPageReserve?.(pageReserve);
|
||||||
|
}
|
||||||
|
if (Number.isFinite(progress)) {
|
||||||
|
this.persistenceManager?.updatePreference?.('webgl', 'bookProgress', progress);
|
||||||
|
window.BookLabDebug?.setReadingProgress?.(progress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -456,6 +523,8 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
window.BookLabDebug?.setReadingProgress?.(value);
|
window.BookLabDebug?.setReadingProgress?.(value);
|
||||||
} else if (key === 'bookPageCount' && !this.preferenceWriteGuard) {
|
} else if (key === 'bookPageCount' && !this.preferenceWriteGuard) {
|
||||||
window.BookLabDebug?.setBookPageCount?.(value);
|
window.BookLabDebug?.setBookPageCount?.(value);
|
||||||
|
} else if (key === 'pageReserve' && !this.preferenceWriteGuard) {
|
||||||
|
window.BookLabDebug?.setPageReserve?.(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"options.displayMode2d": "2D",
|
"options.displayMode2d": "2D",
|
||||||
"options.bookSize": "Buchgröße",
|
"options.bookSize": "Buchgröße",
|
||||||
"options.bookProgress": "Seitenstapel",
|
"options.bookProgress": "Seitenstapel",
|
||||||
|
"options.pageReserve": "Seitenreserve",
|
||||||
"options.volume": "Lautstärke",
|
"options.volume": "Lautstärke",
|
||||||
"options.masterVolume": "Gesamtlautstärke",
|
"options.masterVolume": "Gesamtlautstärke",
|
||||||
"options.speechVolume": "Sprachlautstärke",
|
"options.speechVolume": "Sprachlautstärke",
|
||||||
@@ -66,6 +67,9 @@
|
|||||||
"webgl.status2d": "2D-Szene",
|
"webgl.status2d": "2D-Szene",
|
||||||
"webgl.bookSize": "Seiten",
|
"webgl.bookSize": "Seiten",
|
||||||
"webgl.pageStackProgress": "Fortschritt",
|
"webgl.pageStackProgress": "Fortschritt",
|
||||||
|
"webgl.page": "Seite",
|
||||||
|
"webgl.returnToBeginning": "Zum Anfang",
|
||||||
|
"webgl.goToEnd": "Zum Ende",
|
||||||
"webgl.fastBackward": "Schnell zurück",
|
"webgl.fastBackward": "Schnell zurück",
|
||||||
"webgl.backward": "Zurück",
|
"webgl.backward": "Zurück",
|
||||||
"webgl.forward": "Vorwärts",
|
"webgl.forward": "Vorwärts",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"options.displayMode2d": "2D",
|
"options.displayMode2d": "2D",
|
||||||
"options.bookSize": "Book Size",
|
"options.bookSize": "Book Size",
|
||||||
"options.bookProgress": "Page Stack",
|
"options.bookProgress": "Page Stack",
|
||||||
|
"options.pageReserve": "Page Reserve",
|
||||||
"options.volume": "Volume",
|
"options.volume": "Volume",
|
||||||
"options.masterVolume": "Master Volume",
|
"options.masterVolume": "Master Volume",
|
||||||
"options.speechVolume": "Speech Volume",
|
"options.speechVolume": "Speech Volume",
|
||||||
@@ -66,6 +67,9 @@
|
|||||||
"webgl.status2d": "2D scene",
|
"webgl.status2d": "2D scene",
|
||||||
"webgl.bookSize": "Pages",
|
"webgl.bookSize": "Pages",
|
||||||
"webgl.pageStackProgress": "Progress",
|
"webgl.pageStackProgress": "Progress",
|
||||||
|
"webgl.page": "Page",
|
||||||
|
"webgl.returnToBeginning": "Return to beginning",
|
||||||
|
"webgl.goToEnd": "Go to end",
|
||||||
"webgl.fastBackward": "Fast backward",
|
"webgl.fastBackward": "Fast backward",
|
||||||
"webgl.backward": "Backward",
|
"webgl.backward": "Backward",
|
||||||
"webgl.forward": "Forward",
|
"webgl.forward": "Forward",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface GameMetadata {
|
|||||||
version?: string;
|
version?: string;
|
||||||
copyright?: string;
|
copyright?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
bookPageCount?: number;
|
||||||
|
pageReserve?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GamePaths {
|
export interface GamePaths {
|
||||||
|
|||||||
Reference in New Issue
Block a user