Checkpoint WebGL book renderer work
This commit is contained in:
+32
-58
@@ -1940,7 +1940,8 @@ body.webgl-mode {
|
|||||||
background: #090705;
|
background: #090705;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webgl_canvas {
|
#webgl_canvas,
|
||||||
|
#scene {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1981,7 +1982,20 @@ body.webgl-mode {
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control_group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control_group label,
|
||||||
|
#lab_status {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
#top_menu_controls button,
|
#top_menu_controls button,
|
||||||
|
.transport_button,
|
||||||
.modal-overview-row {
|
.modal-overview-row {
|
||||||
font-family: 'EB Garamond', serif;
|
font-family: 'EB Garamond', serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -1995,10 +2009,24 @@ body.webgl-mode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#top_menu_controls button:hover,
|
#top_menu_controls button:hover,
|
||||||
|
.transport_button:hover,
|
||||||
.modal-overview-row:hover {
|
.modal-overview-row:hover {
|
||||||
background: rgba(87, 55, 31, 0.78);
|
background: rgba(87, 55, 31, 0.78);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transport_button {
|
||||||
|
width: 28px;
|
||||||
|
height: 26px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transport_button:disabled {
|
||||||
|
cursor: var(--default-cursor, default);
|
||||||
|
opacity: 0.38;
|
||||||
|
}
|
||||||
|
|
||||||
#modal_overview {
|
#modal_overview {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 45;
|
z-index: 45;
|
||||||
@@ -2038,47 +2066,6 @@ body.webgl-mode {
|
|||||||
color: rgba(246, 231, 201, 0.62);
|
color: rgba(246, 231, 201, 0.62);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.webgl-mode #book {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 20;
|
|
||||||
inset: 38px 0 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: calc(100vh - 38px);
|
|
||||||
max-width: none;
|
|
||||||
max-height: none;
|
|
||||||
background: transparent;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.webgl-mode #page_left,
|
|
||||||
body.webgl-mode #page_right {
|
|
||||||
pointer-events: none;
|
|
||||||
top: 10%;
|
|
||||||
bottom: auto;
|
|
||||||
height: 68vh;
|
|
||||||
max-height: 760px;
|
|
||||||
width: min(31vw, 500px);
|
|
||||||
background: #f2dfb8;
|
|
||||||
border: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: 1;
|
|
||||||
mix-blend-mode: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.webgl-mode #page_left {
|
|
||||||
left: calc(50vw - min(33vw, 530px));
|
|
||||||
transform: none;
|
|
||||||
transform-origin: right center;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.webgl-mode #page_right {
|
|
||||||
right: calc(50vw - min(33vw, 530px));
|
|
||||||
transform: none;
|
|
||||||
transform-origin: left center;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.webgl-mode #lighting {
|
body.webgl-mode #lighting {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -2099,22 +2086,9 @@ body.webgl-mode #lighting {
|
|||||||
padding: 6px 7px;
|
padding: 6px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.webgl-mode #book {
|
#lab_status,
|
||||||
inset: 46px 0 0;
|
.control_group label {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.webgl-mode #page_left,
|
|
||||||
body.webgl-mode #page_right {
|
|
||||||
width: 44vw;
|
|
||||||
height: 66vh;
|
|
||||||
top: 12%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.webgl-mode #page_left {
|
|
||||||
left: 6vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.webgl-mode #page_right {
|
|
||||||
right: 6vw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260603-webgl-right-page-text';
|
const MODULE_CACHE_BUSTER = '20260606-webgl-direct-page-crop-coords';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ class OptionsUIModule extends BaseModule {
|
|||||||
'getPreference',
|
'getPreference',
|
||||||
'updatePreference',
|
'updatePreference',
|
||||||
'updateUIText',
|
'updateUIText',
|
||||||
'renderProviderStatuses'
|
'renderProviderStatuses',
|
||||||
|
'updateWebGLDisplays'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +250,86 @@ class OptionsUIModule extends BaseModule {
|
|||||||
appSettingsSection.appendChild(speedContainer);
|
appSettingsSection.appendChild(speedContainer);
|
||||||
|
|
||||||
body.appendChild(appSettingsSection);
|
body.appendChild(appSettingsSection);
|
||||||
|
|
||||||
|
const webglSection = document.createElement('div');
|
||||||
|
webglSection.className = 'options-section';
|
||||||
|
|
||||||
|
const webglTitle = document.createElement('h3');
|
||||||
|
webglTitle.textContent = this.t('options.bookDisplay');
|
||||||
|
webglSection.appendChild(webglTitle);
|
||||||
|
|
||||||
|
const displayModeContainer = document.createElement('div');
|
||||||
|
displayModeContainer.className = 'option-item';
|
||||||
|
|
||||||
|
const displayModeLabel = document.createElement('label');
|
||||||
|
displayModeLabel.textContent = this.t('options.displayMode') + ':';
|
||||||
|
displayModeContainer.appendChild(displayModeLabel);
|
||||||
|
|
||||||
|
this.elements.webglMode = createUIElement('select', {
|
||||||
|
'data-pref-bind': 'webgl.mode'
|
||||||
|
}, null, displayModeContainer);
|
||||||
|
[
|
||||||
|
{ value: '3d', label: this.t('options.displayMode3d') },
|
||||||
|
{ value: '2d', label: this.t('options.displayMode2d') }
|
||||||
|
].forEach((optionConfig) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = optionConfig.value;
|
||||||
|
option.textContent = optionConfig.label;
|
||||||
|
this.elements.webglMode.appendChild(option);
|
||||||
|
});
|
||||||
|
webglSection.appendChild(displayModeContainer);
|
||||||
|
|
||||||
|
const bookSizeContainer = document.createElement('div');
|
||||||
|
bookSizeContainer.className = 'option-item';
|
||||||
|
|
||||||
|
const bookSizeLabel = document.createElement('label');
|
||||||
|
bookSizeLabel.textContent = this.t('options.bookSize') + ':';
|
||||||
|
bookSizeContainer.appendChild(bookSizeLabel);
|
||||||
|
|
||||||
|
const bookSizeValue = document.createElement('span');
|
||||||
|
bookSizeValue.className = 'slider-value';
|
||||||
|
bookSizeValue.textContent = '300';
|
||||||
|
this.elements.webglBookSizeValue = bookSizeValue;
|
||||||
|
bookSizeContainer.appendChild(bookSizeValue);
|
||||||
|
|
||||||
|
this.elements.webglBookSize = createUIElement('input', {
|
||||||
|
type: 'range',
|
||||||
|
min: 40,
|
||||||
|
max: 500,
|
||||||
|
step: 10,
|
||||||
|
value: 300,
|
||||||
|
'data-pref-bind': 'webgl.bookPageCount',
|
||||||
|
'data-pref-transform': 'integer:40,500'
|
||||||
|
}, null, bookSizeContainer);
|
||||||
|
this.elements.webglBookSize.addEventListener('input', () => this.updateWebGLDisplays());
|
||||||
|
webglSection.appendChild(bookSizeContainer);
|
||||||
|
|
||||||
|
const bookProgressContainer = document.createElement('div');
|
||||||
|
bookProgressContainer.className = 'option-item';
|
||||||
|
|
||||||
|
const bookProgressLabel = document.createElement('label');
|
||||||
|
bookProgressLabel.textContent = this.t('options.bookProgress') + ':';
|
||||||
|
bookProgressContainer.appendChild(bookProgressLabel);
|
||||||
|
|
||||||
|
const bookProgressValue = document.createElement('span');
|
||||||
|
bookProgressValue.className = 'slider-value';
|
||||||
|
bookProgressValue.textContent = '50%';
|
||||||
|
this.elements.webglBookProgressValue = bookProgressValue;
|
||||||
|
bookProgressContainer.appendChild(bookProgressValue);
|
||||||
|
|
||||||
|
this.elements.webglBookProgress = createUIElement('input', {
|
||||||
|
type: 'range',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 1,
|
||||||
|
value: 50,
|
||||||
|
'data-pref-bind': 'webgl.bookProgress',
|
||||||
|
'data-pref-transform': 'range:0,1'
|
||||||
|
}, null, bookProgressContainer);
|
||||||
|
this.elements.webglBookProgress.addEventListener('input', () => this.updateWebGLDisplays());
|
||||||
|
webglSection.appendChild(bookProgressContainer);
|
||||||
|
|
||||||
|
body.appendChild(webglSection);
|
||||||
|
|
||||||
// TTS Section
|
// TTS Section
|
||||||
const ttsSection = document.createElement('div');
|
const ttsSection = document.createElement('div');
|
||||||
@@ -1020,6 +1101,7 @@ class OptionsUIModule extends BaseModule {
|
|||||||
console.log('Options UI: Preference bindings set up', this.bindings.length);
|
console.log('Options UI: Preference bindings set up', this.bindings.length);
|
||||||
this.updateSpeedDisplay();
|
this.updateSpeedDisplay();
|
||||||
this.updateVolumeDisplays();
|
this.updateVolumeDisplays();
|
||||||
|
this.updateWebGLDisplays();
|
||||||
|
|
||||||
// Add event listeners for side effects when preferences change
|
// Add event listeners for side effects when preferences change
|
||||||
document.addEventListener('preference-updated', (event) => {
|
document.addEventListener('preference-updated', (event) => {
|
||||||
@@ -1115,6 +1197,10 @@ class OptionsUIModule extends BaseModule {
|
|||||||
this.populateVoices();
|
this.populateVoices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (category === 'webgl') {
|
||||||
|
this.updateWebGLDisplays();
|
||||||
|
}
|
||||||
if (key === 'speed' && this.elements.ttsSpeed) {
|
if (key === 'speed' && this.elements.ttsSpeed) {
|
||||||
this.updateSpeedDisplay();
|
this.updateSpeedDisplay();
|
||||||
}
|
}
|
||||||
@@ -1155,6 +1241,15 @@ class OptionsUIModule extends BaseModule {
|
|||||||
this.elements.musicDuckingAmountValue.textContent = `${this.elements.musicDuckingAmount.value}%`;
|
this.elements.musicDuckingAmountValue.textContent = `${this.elements.musicDuckingAmount.value}%`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateWebGLDisplays() {
|
||||||
|
if (this.elements.webglBookSize && this.elements.webglBookSizeValue) {
|
||||||
|
this.elements.webglBookSizeValue.textContent = String(this.elements.webglBookSize.value);
|
||||||
|
}
|
||||||
|
if (this.elements.webglBookProgress && this.elements.webglBookProgressValue) {
|
||||||
|
this.elements.webglBookProgressValue.textContent = `${this.elements.webglBookProgress.value}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the singleton instance
|
// Create the singleton instance
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ class PersistenceManagerModule extends BaseModule {
|
|||||||
localeUserOverride: false,
|
localeUserOverride: false,
|
||||||
speed: 1.0,
|
speed: 1.0,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
|
},
|
||||||
|
webgl: {
|
||||||
|
mode: null,
|
||||||
|
bookPageCount: 300,
|
||||||
|
bookProgress: 0.5
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const PROCEDURAL_BOOK = {
|
|||||||
PAGE_COUNT_STEP: 10,
|
PAGE_COUNT_STEP: 10,
|
||||||
PAGE_LINE_SEGMENTS: 48,
|
PAGE_LINE_SEGMENTS: 48,
|
||||||
PAGE_DEPTH: 2.24,
|
PAGE_DEPTH: 2.24,
|
||||||
PAGE_WIDTH: 2.24 * 2 / 3,
|
PAGE_WIDTH: 2.24 * 0.806,
|
||||||
COVER_DEPTH: 2.30,
|
COVER_DEPTH: 2.30,
|
||||||
OPEN_SEAM_GAP: 0.003,
|
OPEN_SEAM_GAP: 0.003,
|
||||||
PROFILE: {
|
PROFILE: {
|
||||||
|
|||||||
+261
-86
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
|
|||||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-no-menu-offscreen-dom';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@@ -21,8 +21,13 @@ const tableDebugModes = {
|
|||||||
mirror: 10
|
mirror: 10
|
||||||
};
|
};
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const appInitialState = window.WebGLBookInitialState || {};
|
||||||
const tableDebugName = urlParams.get('tableDebug') || 'none';
|
const tableDebugName = urlParams.get('tableDebug') || 'none';
|
||||||
const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none;
|
const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none;
|
||||||
|
const isAppIntegrationMode = appInitialState.appMode === true;
|
||||||
|
const html2CanvasPromise = isAppIntegrationMode
|
||||||
|
? import('https://esm.sh/html2canvas@1.4.1')
|
||||||
|
: null;
|
||||||
const labStatus = document.getElementById('lab_status');
|
const labStatus = document.getElementById('lab_status');
|
||||||
if (labStatus && tableDebugMode !== tableDebugModes.none) {
|
if (labStatus && tableDebugMode !== tableDebugModes.none) {
|
||||||
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
|
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
|
||||||
@@ -38,8 +43,14 @@ renderer.shadowMap.type = THREE.VSMShadowMap;
|
|||||||
const generatedTextureCanvases = {};
|
const generatedTextureCanvases = {};
|
||||||
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
|
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||||
const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2);
|
const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
const pageTextureWidth = 3200;
|
const pageTextureWidth = isAppIntegrationMode ? 1280 : 3200;
|
||||||
|
const appPageTextureInset = 0;
|
||||||
const reflectionTargetSize = new THREE.Vector2();
|
const reflectionTargetSize = new THREE.Vector2();
|
||||||
|
const pageRaycaster = new THREE.Raycaster();
|
||||||
|
const pointerNdc = new THREE.Vector2();
|
||||||
|
let pageTextureRenderSerial = 0;
|
||||||
|
let pageTextureRenderInProgress = false;
|
||||||
|
let pageTextureRenderPending = false;
|
||||||
let sceneComposerTarget = null;
|
let sceneComposerTarget = null;
|
||||||
let composer = null;
|
let composer = null;
|
||||||
let sceneRenderPass = null;
|
let sceneRenderPass = null;
|
||||||
@@ -47,6 +58,7 @@ let sceneAoPass = null;
|
|||||||
let sceneSmaaPass = null;
|
let sceneSmaaPass = null;
|
||||||
let sceneOutputPass = null;
|
let sceneOutputPass = null;
|
||||||
const aoExcludedObjects = new Set();
|
const aoExcludedObjects = new Set();
|
||||||
|
let renderedFrameCount = 0;
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
scene.background = new THREE.Color(0x080604);
|
scene.background = new THREE.Color(0x080604);
|
||||||
@@ -59,11 +71,13 @@ let tableDustTexture = null;
|
|||||||
let tableGreaseTexture = null;
|
let tableGreaseTexture = null;
|
||||||
const tableTopY = -0.02;
|
const tableTopY = -0.02;
|
||||||
const bookTableContactClearance = 0.002;
|
const bookTableContactClearance = 0.002;
|
||||||
const tableReflectionTarget = new THREE.WebGLRenderTarget(4096, 2304, {
|
const tableReflectionBaseWidth = isAppIntegrationMode ? 1280 : 4096;
|
||||||
|
const tableReflectionBaseHeight = isAppIntegrationMode ? 720 : 2304;
|
||||||
|
const tableReflectionTarget = new THREE.WebGLRenderTarget(tableReflectionBaseWidth, tableReflectionBaseHeight, {
|
||||||
colorSpace: THREE.SRGBColorSpace,
|
colorSpace: THREE.SRGBColorSpace,
|
||||||
depthBuffer: true,
|
depthBuffer: true,
|
||||||
stencilBuffer: false,
|
stencilBuffer: false,
|
||||||
samples: renderer.capabilities.isWebGL2 ? 8 : 0
|
samples: renderer.capabilities.isWebGL2 ? (isAppIntegrationMode ? 2 : 8) : 0
|
||||||
});
|
});
|
||||||
tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace;
|
tableReflectionTarget.texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
tableReflectionTarget.texture.minFilter = THREE.LinearFilter;
|
tableReflectionTarget.texture.minFilter = THREE.LinearFilter;
|
||||||
@@ -82,7 +96,7 @@ const reflectionUp = new THREE.Vector3();
|
|||||||
const candleShadowSources = [];
|
const candleShadowSources = [];
|
||||||
const candleWorldPosition = new THREE.Vector3();
|
const candleWorldPosition = new THREE.Vector3();
|
||||||
const flameWorldPosition = new THREE.Vector3();
|
const flameWorldPosition = new THREE.Vector3();
|
||||||
const bookShadowMapSize = 1536;
|
const bookShadowMapSize = isAppIntegrationMode ? 512 : 1536;
|
||||||
const bookShadowTargets = Array.from({ length: 3 }, () => {
|
const bookShadowTargets = Array.from({ length: 3 }, () => {
|
||||||
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
|
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
|
||||||
colorSpace: THREE.NoColorSpace,
|
colorSpace: THREE.NoColorSpace,
|
||||||
@@ -119,6 +133,7 @@ const cameraRig = {
|
|||||||
minRadius: 2.4,
|
minRadius: 2.4,
|
||||||
maxRadius: 9.0,
|
maxRadius: 9.0,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
|
navigationActive: false,
|
||||||
pointerX: 0,
|
pointerX: 0,
|
||||||
pointerY: 0,
|
pointerY: 0,
|
||||||
keys: new Set()
|
keys: new Set()
|
||||||
@@ -135,9 +150,9 @@ configureScenePostprocessing();
|
|||||||
const clock = new THREE.Clock();
|
const clock = new THREE.Clock();
|
||||||
const book = new THREE.Group();
|
const book = new THREE.Group();
|
||||||
scene.add(book);
|
scene.add(book);
|
||||||
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? '0.28'), 0, 1);
|
const initialReadingProgress = THREE.MathUtils.clamp(Number.parseFloat(urlParams.get('progress') ?? appInitialState.progress ?? '0.28'), 0, 1);
|
||||||
let readingProgress = Number.isFinite(initialReadingProgress) ? initialReadingProgress : 0.28;
|
let readingProgress = Number.isFinite(initialReadingProgress) ? initialReadingProgress : 0.28;
|
||||||
let bookPageCount = snapProceduralPageCount(urlParams.get('pages') ?? '240');
|
let bookPageCount = snapProceduralPageCount(urlParams.get('pages') ?? appInitialState.pageCount ?? '240');
|
||||||
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');
|
||||||
@@ -382,12 +397,34 @@ window.BookLabDebug = {
|
|||||||
setReadingProgress(value);
|
setReadingProgress(value);
|
||||||
return readingProgress;
|
return readingProgress;
|
||||||
},
|
},
|
||||||
|
setBookPageCount(value) {
|
||||||
|
setBookPageCount(value);
|
||||||
|
return bookPageCount;
|
||||||
|
},
|
||||||
|
redrawPageTextures() {
|
||||||
|
redrawPageTexturesFromDom();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getTextureInfo() {
|
||||||
|
return {
|
||||||
|
pageTextureWidth,
|
||||||
|
pageTextureHeight: leftCanvas.height,
|
||||||
|
appPageTextureInset,
|
||||||
|
debug: getPageTextureDebugState()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
projectPointerToPage(clientX, clientY) {
|
||||||
|
return projectPointerToPage(clientX, clientY);
|
||||||
|
},
|
||||||
exportTexture(name) {
|
exportTexture(name) {
|
||||||
|
if (name === 'left' || name === 'leftPage') return leftCanvas.toDataURL('image/png');
|
||||||
|
if (name === 'right' || name === 'rightPage') return rightCanvas.toDataURL('image/png');
|
||||||
return generatedTextureCanvases[name]?.toDataURL('image/png') || null;
|
return generatedTextureCanvases[name]?.toDataURL('image/png') || null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
|
document.addEventListener('webgl-book:redraw-pages', redrawPageTexturesFromDom);
|
||||||
installBookControls();
|
installBookControls();
|
||||||
installCameraControls();
|
installCameraControls();
|
||||||
resize();
|
resize();
|
||||||
@@ -1362,6 +1399,7 @@ function setReadingProgress(value) {
|
|||||||
readingProgress = nextProgress;
|
readingProgress = nextProgress;
|
||||||
buildBook();
|
buildBook();
|
||||||
syncBookControls();
|
syncBookControls();
|
||||||
|
window.WebGLBookPreferenceBridge?.updateProgress?.(readingProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBookPageCount(value) {
|
function setBookPageCount(value) {
|
||||||
@@ -1370,6 +1408,7 @@ function setBookPageCount(value) {
|
|||||||
bookPageCount = nextPageCount;
|
bookPageCount = nextPageCount;
|
||||||
buildBook();
|
buildBook();
|
||||||
syncBookControls();
|
syncBookControls();
|
||||||
|
window.WebGLBookPreferenceBridge?.updatePageCount?.(bookPageCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stepReadingProgress(pageDelta) {
|
function stepReadingProgress(pageDelta) {
|
||||||
@@ -1405,6 +1444,179 @@ function syncBookControls() {
|
|||||||
if (fastForwardButton) fastForwardButton.disabled = busy || !canPageFlip(1);
|
if (fastForwardButton) fastForwardButton.disabled = busy || !canPageFlip(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function redrawPageTexturesFromDom() {
|
||||||
|
if (pageTextureRenderInProgress) {
|
||||||
|
pageTextureRenderPending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const leftSource = document.getElementById('page_left');
|
||||||
|
const rightSource = document.getElementById('page_right');
|
||||||
|
if (!leftSource && !rightSource) return;
|
||||||
|
pageTextureRenderInProgress = true;
|
||||||
|
const serial = ++pageTextureRenderSerial;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (leftSource && await drawDomPageTexture(leftCanvas, leftSource, 'left')) {
|
||||||
|
leftTexture.needsUpdate = true;
|
||||||
|
}
|
||||||
|
if (rightSource && await drawDomPageTexture(rightCanvas, rightSource, 'right')) {
|
||||||
|
rightTexture.needsUpdate = true;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pageTextureRenderInProgress = false;
|
||||||
|
if (pageTextureRenderPending && serial === pageTextureRenderSerial) {
|
||||||
|
pageTextureRenderPending = false;
|
||||||
|
redrawPageTexturesFromDom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drawDomPageTexture(canvas, source, side) {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.fillStyle = '#fffaf0';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const shade = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
||||||
|
shade.addColorStop(0, 'rgba(93, 55, 24, 0.10)');
|
||||||
|
shade.addColorStop(side === 'left' ? 0.85 : 0.15, 'rgba(255, 255, 255, 0)');
|
||||||
|
shade.addColorStop(1, 'rgba(85, 49, 21, 0.08)');
|
||||||
|
ctx.fillStyle = shade;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const painted = await paintRasterizedDomPage(ctx, canvas, source);
|
||||||
|
updatePageTextureDebugState(side, canvas, source, painted);
|
||||||
|
return painted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageTextureDebugState() {
|
||||||
|
const rawState = document.documentElement.dataset.webglPageTextures;
|
||||||
|
if (!rawState) return {};
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawState);
|
||||||
|
} catch (error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePageTextureDebugState(side, canvas, source, painted) {
|
||||||
|
const state = getPageTextureDebugState();
|
||||||
|
state[side] = {
|
||||||
|
painted,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height,
|
||||||
|
sourceId: source.id || '',
|
||||||
|
sourceTextLength: source.textContent?.trim().length || 0,
|
||||||
|
darkPixels: countPageTextureDarkPixels(canvas)
|
||||||
|
};
|
||||||
|
document.documentElement.dataset.webglPageTextures = JSON.stringify(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countPageTextureDarkPixels(canvas) {
|
||||||
|
const sampleCanvas = document.createElement('canvas');
|
||||||
|
const sampleSize = 64;
|
||||||
|
sampleCanvas.width = sampleSize;
|
||||||
|
sampleCanvas.height = sampleSize;
|
||||||
|
const sampleContext = sampleCanvas.getContext('2d');
|
||||||
|
sampleContext.drawImage(canvas, 0, 0, sampleSize, sampleSize);
|
||||||
|
const pixels = sampleContext.getImageData(0, 0, sampleSize, sampleSize).data;
|
||||||
|
let darkPixels = 0;
|
||||||
|
for (let index = 0; index < pixels.length; index += 4) {
|
||||||
|
const alpha = pixels[index + 3];
|
||||||
|
if (alpha < 8) continue;
|
||||||
|
const luminance = pixels[index] * 0.2126 + pixels[index + 1] * 0.7152 + pixels[index + 2] * 0.0722;
|
||||||
|
if (luminance < 96) darkPixels += 1;
|
||||||
|
}
|
||||||
|
return darkPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function paintRasterizedDomPage(ctx, canvas, source) {
|
||||||
|
const pageRect = source.getBoundingClientRect();
|
||||||
|
if (pageRect.width <= 0 || pageRect.height <= 0) return false;
|
||||||
|
const captured = await captureDomPageWithHtml2Canvas(source, pageRect, canvas);
|
||||||
|
if (captured) {
|
||||||
|
drawCapturedPageCanvas(ctx, canvas, captured);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureDomPageWithHtml2Canvas(source, pageRect, targetCanvas) {
|
||||||
|
if (!html2CanvasPromise) return null;
|
||||||
|
try {
|
||||||
|
const module = await html2CanvasPromise;
|
||||||
|
const html2canvas = module.default || module;
|
||||||
|
return await html2canvas(source, {
|
||||||
|
backgroundColor: null,
|
||||||
|
logging: false,
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: false,
|
||||||
|
foreignObjectRendering: true,
|
||||||
|
x: pageRect.left,
|
||||||
|
y: pageRect.top,
|
||||||
|
width: pageRect.width,
|
||||||
|
height: pageRect.height,
|
||||||
|
scrollX: 0,
|
||||||
|
scrollY: 0,
|
||||||
|
windowWidth: Math.ceil(Math.max(window.innerWidth, pageRect.right)),
|
||||||
|
windowHeight: Math.ceil(Math.max(window.innerHeight, pageRect.bottom)),
|
||||||
|
scale: Math.max(1, targetCanvas.width / pageRect.width)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
document.documentElement.dataset.webglLastCaptureError = error?.message || String(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCapturedPageCanvas(ctx, canvas, captured) {
|
||||||
|
const insetX = canvas.width * appPageTextureInset;
|
||||||
|
const insetY = canvas.height * appPageTextureInset * 0.35;
|
||||||
|
ctx.drawImage(
|
||||||
|
captured,
|
||||||
|
insetX,
|
||||||
|
insetY,
|
||||||
|
canvas.width - insetX * 2,
|
||||||
|
canvas.height - insetY * 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectPointerToPage(clientX, clientY) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) return null;
|
||||||
|
pointerNdc.set(
|
||||||
|
((clientX - rect.left) / rect.width) * 2 - 1,
|
||||||
|
-(((clientY - rect.top) / rect.height) * 2 - 1)
|
||||||
|
);
|
||||||
|
pageRaycaster.setFromCamera(pointerNdc, camera);
|
||||||
|
const intersections = pageRaycaster.intersectObjects(book.children, true);
|
||||||
|
for (const hit of intersections) {
|
||||||
|
const pageSide = textureHitPageSide(hit);
|
||||||
|
if (!pageSide || !hit.uv) continue;
|
||||||
|
const insetX = appPageTextureInset;
|
||||||
|
const insetY = appPageTextureInset * 0.35;
|
||||||
|
const mappedX = THREE.MathUtils.clamp((hit.uv.x - insetX) / Math.max(0.001, 1 - insetX * 2), 0, 1);
|
||||||
|
const mappedY = 1 - THREE.MathUtils.clamp((hit.uv.y - insetY) / Math.max(0.001, 1 - insetY * 2), 0, 1);
|
||||||
|
return {
|
||||||
|
pageId: pageSide === 'left' ? 'page_left' : 'page_right',
|
||||||
|
x: mappedX,
|
||||||
|
y: mappedY,
|
||||||
|
uv: { x: hit.uv.x, y: hit.uv.y }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textureHitPageSide(hit) {
|
||||||
|
const material = Array.isArray(hit.object.material)
|
||||||
|
? hit.object.material[hit.face?.materialIndex ?? 0]
|
||||||
|
: hit.object.material;
|
||||||
|
if (material === materials.leftPage) return 'left';
|
||||||
|
if (material === materials.rightPage) return 'right';
|
||||||
|
if (material?.map === leftTexture) return 'left';
|
||||||
|
if (material?.map === rightTexture) return 'right';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function startPageFlip(direction) {
|
function startPageFlip(direction) {
|
||||||
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
||||||
const flip = createPageFlip(direction, performance.now(), normalFlipDuration);
|
const flip = createPageFlip(direction, performance.now(), normalFlipDuration);
|
||||||
@@ -1956,41 +2168,9 @@ function createPageCanvas(side) {
|
|||||||
shade.addColorStop(1, 'rgba(85, 49, 21, 0.08)');
|
shade.addColorStop(1, 'rgba(85, 49, 21, 0.08)');
|
||||||
ctx.fillStyle = shade;
|
ctx.fillStyle = shade;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
ctx.fillStyle = inkColor;
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
const layout = hardcoverPageLayout(canvas, side);
|
|
||||||
if (side === 'left') {
|
|
||||||
drawTitlePage(ctx, layout);
|
|
||||||
} else {
|
|
||||||
drawNovelPage(ctx, layout, 'Click on new game or load to start the game');
|
|
||||||
}
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hardcoverPageLayout(canvas, side) {
|
|
||||||
const inner = canvas.width * 0.125;
|
|
||||||
const outer = canvas.width * 0.075;
|
|
||||||
const top = canvas.height * 0.085;
|
|
||||||
const bottom = canvas.height * 0.115;
|
|
||||||
const margins = {
|
|
||||||
left: side === 'right' ? inner : outer,
|
|
||||||
right: side === 'right' ? outer : inner,
|
|
||||||
top,
|
|
||||||
bottom
|
|
||||||
};
|
|
||||||
const width = canvas.width - margins.left - margins.right;
|
|
||||||
const height = canvas.height - margins.top - margins.bottom;
|
|
||||||
return {
|
|
||||||
margins,
|
|
||||||
x: margins.left,
|
|
||||||
y: margins.top,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
em: width / 24
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createLeatherTextures() {
|
function createLeatherTextures() {
|
||||||
const size = 1024;
|
const size = 1024;
|
||||||
const colorCanvas = document.createElement('canvas');
|
const colorCanvas = document.createElement('canvas');
|
||||||
@@ -2424,49 +2604,6 @@ function tintAmbientFromCanvas(canvas) {
|
|||||||
candleBounceLight.intensity = 0.28;
|
candleBounceLight.intensity = 0.28;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTitlePage(ctx, layout) {
|
|
||||||
const titleX = layout.x;
|
|
||||||
const titleWidth = layout.width;
|
|
||||||
drawCentered(ctx, 'Georg Tomitsch', layout.y + layout.height * 0.18, layout.em * 0.62, titleX, titleWidth);
|
|
||||||
drawCentered(ctx, 'Eibenreith', layout.y + layout.height * 0.235, layout.em * 1.72, titleX, titleWidth);
|
|
||||||
drawCentered(ctx, 'Ein Kaiserpunk Abenteuer', layout.y + layout.height * 0.315, layout.em * 0.76, titleX, titleWidth);
|
|
||||||
drawCentered(ctx, 'speech | autoplay | speed | new game | save | load | options', layout.y + layout.height * 0.47, layout.em * 0.42, titleX, titleWidth);
|
|
||||||
drawCentered(ctx, 'click on page or press spacebar to fast forward text animation', layout.y + layout.height * 0.56, layout.em * 0.42, titleX, titleWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawNovelPage(ctx, layout, text) {
|
|
||||||
const projectedX = Math.max(layout.margins.left * 0.25, layout.x - layout.margins.left * 0.75);
|
|
||||||
const projectedWidth = layout.width * 0.7;
|
|
||||||
drawParagraph(ctx, text, projectedX, layout.y + layout.height * 0.1, projectedWidth, layout.em * 0.72, 1.36, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawCentered(ctx, text, y, size, x = 0, width = ctx.canvas.width) {
|
|
||||||
ctx.font = `${Math.round(size)}px Georgia, "Times New Roman", serif`;
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText(text, x + width * 0.5, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawParagraph(ctx, text, x, y, width, size, lineHeight, firstLineIndent = 0) {
|
|
||||||
const fontSize = Math.round(size);
|
|
||||||
ctx.font = `${fontSize}px Georgia, "Times New Roman", serif`;
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
const words = text.split(/\s+/);
|
|
||||||
let line = '';
|
|
||||||
let indent = firstLineIndent;
|
|
||||||
words.forEach((word) => {
|
|
||||||
const test = line ? `${line} ${word}` : word;
|
|
||||||
if (ctx.measureText(test).width > width - indent && line) {
|
|
||||||
ctx.fillText(line, x + indent, y);
|
|
||||||
line = word;
|
|
||||||
y += fontSize * lineHeight;
|
|
||||||
indent = 0;
|
|
||||||
} else {
|
|
||||||
line = test;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (line) ctx.fillText(line, x + indent, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
const width = Math.max(1, window.innerWidth);
|
const width = Math.max(1, window.innerWidth);
|
||||||
const height = Math.max(1, window.innerHeight);
|
const height = Math.max(1, window.innerHeight);
|
||||||
@@ -2481,8 +2618,8 @@ function resize() {
|
|||||||
4096 / width,
|
4096 / width,
|
||||||
2304 / height
|
2304 / height
|
||||||
));
|
));
|
||||||
const reflectionWidth = Math.floor(width * reflectionScale);
|
const reflectionWidth = Math.min(tableReflectionBaseWidth, Math.floor(width * reflectionScale));
|
||||||
const reflectionHeight = Math.floor(height * reflectionScale);
|
const reflectionHeight = Math.min(tableReflectionBaseHeight, Math.floor(height * reflectionScale));
|
||||||
reflectionTargetSize.set(reflectionWidth, reflectionHeight);
|
reflectionTargetSize.set(reflectionWidth, reflectionHeight);
|
||||||
tableReflectionTarget.setSize(
|
tableReflectionTarget.setSize(
|
||||||
reflectionTargetSize.x,
|
reflectionTargetSize.x,
|
||||||
@@ -2491,8 +2628,14 @@ function resize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function installCameraControls() {
|
function installCameraControls() {
|
||||||
|
canvas.addEventListener('contextmenu', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
canvas.addEventListener('pointerdown', (event) => {
|
canvas.addEventListener('pointerdown', (event) => {
|
||||||
|
if (event.button !== 2) return;
|
||||||
cameraRig.dragging = true;
|
cameraRig.dragging = true;
|
||||||
|
cameraRig.navigationActive = true;
|
||||||
canvas.style.cursor = 'grabbing';
|
canvas.style.cursor = 'grabbing';
|
||||||
cameraRig.pointerX = event.clientX;
|
cameraRig.pointerX = event.clientX;
|
||||||
cameraRig.pointerY = event.clientY;
|
cameraRig.pointerY = event.clientY;
|
||||||
@@ -2515,17 +2658,23 @@ function installCameraControls() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('pointerup', (event) => {
|
canvas.addEventListener('pointerup', (event) => {
|
||||||
|
if (event.button !== 2) return;
|
||||||
cameraRig.dragging = false;
|
cameraRig.dragging = false;
|
||||||
|
cameraRig.navigationActive = false;
|
||||||
|
cameraRig.keys.clear();
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
canvas.releasePointerCapture(event.pointerId);
|
canvas.releasePointerCapture(event.pointerId);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('pointercancel', () => {
|
canvas.addEventListener('pointercancel', () => {
|
||||||
cameraRig.dragging = false;
|
cameraRig.dragging = false;
|
||||||
|
cameraRig.navigationActive = false;
|
||||||
|
cameraRig.keys.clear();
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('wheel', (event) => {
|
canvas.addEventListener('wheel', (event) => {
|
||||||
|
if (!cameraRig.navigationActive) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const zoom = Math.exp(event.deltaY * 0.001);
|
const zoom = Math.exp(event.deltaY * 0.001);
|
||||||
cameraRig.radius = THREE.MathUtils.clamp(
|
cameraRig.radius = THREE.MathUtils.clamp(
|
||||||
@@ -2537,6 +2686,7 @@ function installCameraControls() {
|
|||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
window.addEventListener('keydown', (event) => {
|
window.addEventListener('keydown', (event) => {
|
||||||
|
if (!cameraRig.navigationActive) return;
|
||||||
if (['KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(event.code)) {
|
if (['KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(event.code)) {
|
||||||
cameraRig.keys.add(event.code);
|
cameraRig.keys.add(event.code);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -2679,6 +2829,7 @@ function updateTableReflection() {
|
|||||||
const previousXrEnabled = renderer.xr.enabled;
|
const previousXrEnabled = renderer.xr.enabled;
|
||||||
const previousShadowAutoUpdate = renderer.shadowMap.autoUpdate;
|
const previousShadowAutoUpdate = renderer.shadowMap.autoUpdate;
|
||||||
const previousToneMappingExposure = renderer.toneMappingExposure;
|
const previousToneMappingExposure = renderer.toneMappingExposure;
|
||||||
|
const pageTextureState = suppressPageContentMaps();
|
||||||
|
|
||||||
tableMesh.userData.wasVisibleForTableReflection = tableMesh.visible;
|
tableMesh.userData.wasVisibleForTableReflection = tableMesh.visible;
|
||||||
tableMesh.visible = false;
|
tableMesh.visible = false;
|
||||||
@@ -2692,10 +2843,29 @@ function updateTableReflection() {
|
|||||||
renderer.toneMappingExposure = previousToneMappingExposure;
|
renderer.toneMappingExposure = previousToneMappingExposure;
|
||||||
renderer.shadowMap.autoUpdate = previousShadowAutoUpdate;
|
renderer.shadowMap.autoUpdate = previousShadowAutoUpdate;
|
||||||
renderer.xr.enabled = previousXrEnabled;
|
renderer.xr.enabled = previousXrEnabled;
|
||||||
|
restorePageContentMaps(pageTextureState);
|
||||||
tableMesh.visible = tableMesh.userData.wasVisibleForTableReflection;
|
tableMesh.visible = tableMesh.userData.wasVisibleForTableReflection;
|
||||||
delete tableMesh.userData.wasVisibleForTableReflection;
|
delete tableMesh.userData.wasVisibleForTableReflection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function suppressPageContentMaps() {
|
||||||
|
if (!isAppIntegrationMode) return null;
|
||||||
|
return [materials.leftPage, materials.rightPage].map((material) => {
|
||||||
|
const previousMap = material.map;
|
||||||
|
material.map = null;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
return { material, previousMap };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restorePageContentMaps(state) {
|
||||||
|
if (!state) return;
|
||||||
|
state.forEach(({ material, previousMap }) => {
|
||||||
|
material.map = previousMap;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderMirrorDebugView() {
|
function renderMirrorDebugView() {
|
||||||
const hiddenObjects = [];
|
const hiddenObjects = [];
|
||||||
scene.traverse((object) => {
|
scene.traverse((object) => {
|
||||||
@@ -2746,8 +2916,13 @@ function animate() {
|
|||||||
});
|
});
|
||||||
updateActiveFlips(performance.now());
|
updateActiveFlips(performance.now());
|
||||||
updateCandleShadowUniforms();
|
updateCandleShadowUniforms();
|
||||||
updateBookShadowMaps();
|
renderedFrameCount += 1;
|
||||||
updateTableReflection();
|
if (!isAppIntegrationMode || renderedFrameCount % 6 === 1 || activeFlips.length > 0) {
|
||||||
|
updateBookShadowMaps();
|
||||||
|
}
|
||||||
|
if (!isAppIntegrationMode || renderedFrameCount % 4 === 1 || cameraRig.navigationActive || activeFlips.length > 0) {
|
||||||
|
updateTableReflection();
|
||||||
|
}
|
||||||
if (tableDebugMode === tableDebugModes.mirror) {
|
if (tableDebugMode === tableDebugModes.mirror) {
|
||||||
renderer.setRenderTarget(null);
|
renderer.setRenderTarget(null);
|
||||||
renderer.clear();
|
renderer.clear();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,12 @@
|
|||||||
"options.voice": "Stimme",
|
"options.voice": "Stimme",
|
||||||
"options.speed": "Tempo",
|
"options.speed": "Tempo",
|
||||||
"options.audio": "Audio",
|
"options.audio": "Audio",
|
||||||
|
"options.bookDisplay": "Buchanzeige",
|
||||||
|
"options.displayMode": "Anzeigemodus",
|
||||||
|
"options.displayMode3d": "3D",
|
||||||
|
"options.displayMode2d": "2D",
|
||||||
|
"options.bookSize": "Buchgröße",
|
||||||
|
"options.bookProgress": "Seitenstapel",
|
||||||
"options.volume": "Lautstärke",
|
"options.volume": "Lautstärke",
|
||||||
"options.masterVolume": "Gesamtlautstärke",
|
"options.masterVolume": "Gesamtlautstärke",
|
||||||
"options.speechVolume": "Sprachlautstärke",
|
"options.speechVolume": "Sprachlautstärke",
|
||||||
@@ -53,6 +59,17 @@
|
|||||||
"options.apiUrl": "API-URL",
|
"options.apiUrl": "API-URL",
|
||||||
"options.model": "Modell",
|
"options.model": "Modell",
|
||||||
"options.requestTimeoutMs": "Anfrage-Timeout (ms)",
|
"options.requestTimeoutMs": "Anfrage-Timeout (ms)",
|
||||||
|
"webgl.title": "Prozedurales Buch",
|
||||||
|
"webgl.sceneLabel": "3D-Buchszene",
|
||||||
|
"webgl.bookControls": "Buchsteuerung",
|
||||||
|
"webgl.status3d": "3D-Szene",
|
||||||
|
"webgl.status2d": "2D-Szene",
|
||||||
|
"webgl.bookSize": "Seiten",
|
||||||
|
"webgl.pageStackProgress": "Fortschritt",
|
||||||
|
"webgl.fastBackward": "Schnell zurück",
|
||||||
|
"webgl.backward": "Zurück",
|
||||||
|
"webgl.forward": "Vorwärts",
|
||||||
|
"webgl.fastForward": "Schnell vorwärts",
|
||||||
"credits.button": "Credits",
|
"credits.button": "Credits",
|
||||||
"credits.buttonTitle": "Mitwirkende und Lizenzen anzeigen",
|
"credits.buttonTitle": "Mitwirkende und Lizenzen anzeigen",
|
||||||
"credits.title": "Mitwirkende und Lizenzen",
|
"credits.title": "Mitwirkende und Lizenzen",
|
||||||
|
|||||||
@@ -29,6 +29,12 @@
|
|||||||
"options.voice": "Voice",
|
"options.voice": "Voice",
|
||||||
"options.speed": "Speed",
|
"options.speed": "Speed",
|
||||||
"options.audio": "Audio",
|
"options.audio": "Audio",
|
||||||
|
"options.bookDisplay": "Book Display",
|
||||||
|
"options.displayMode": "Display Mode",
|
||||||
|
"options.displayMode3d": "3D",
|
||||||
|
"options.displayMode2d": "2D",
|
||||||
|
"options.bookSize": "Book Size",
|
||||||
|
"options.bookProgress": "Page Stack",
|
||||||
"options.volume": "Volume",
|
"options.volume": "Volume",
|
||||||
"options.masterVolume": "Master Volume",
|
"options.masterVolume": "Master Volume",
|
||||||
"options.speechVolume": "Speech Volume",
|
"options.speechVolume": "Speech Volume",
|
||||||
@@ -53,6 +59,17 @@
|
|||||||
"options.apiUrl": "API URL",
|
"options.apiUrl": "API URL",
|
||||||
"options.model": "Model",
|
"options.model": "Model",
|
||||||
"options.requestTimeoutMs": "Request timeout (ms)",
|
"options.requestTimeoutMs": "Request timeout (ms)",
|
||||||
|
"webgl.title": "Procedural Book",
|
||||||
|
"webgl.sceneLabel": "3D book scene",
|
||||||
|
"webgl.bookControls": "Book controls",
|
||||||
|
"webgl.status3d": "3D scene",
|
||||||
|
"webgl.status2d": "2D scene",
|
||||||
|
"webgl.bookSize": "Pages",
|
||||||
|
"webgl.pageStackProgress": "Progress",
|
||||||
|
"webgl.fastBackward": "Fast backward",
|
||||||
|
"webgl.backward": "Backward",
|
||||||
|
"webgl.forward": "Forward",
|
||||||
|
"webgl.fastForward": "Fast forward",
|
||||||
"credits.button": "credits",
|
"credits.button": "credits",
|
||||||
"credits.buttonTitle": "Show credits and third-party licenses",
|
"credits.buttonTitle": "Show credits and third-party licenses",
|
||||||
"credits.title": "Credits and Licenses",
|
"credits.title": "Credits and Licenses",
|
||||||
|
|||||||
Reference in New Issue
Block a user