Start texture-space book renderer
This commit is contained in:
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* Book Page Format Module
|
||||||
|
* Defines the canonical page geometry used by the WebGL book renderer.
|
||||||
|
*/
|
||||||
|
import { BaseModule } from './base-module.js';
|
||||||
|
|
||||||
|
class BookPageFormatModule extends BaseModule {
|
||||||
|
constructor() {
|
||||||
|
super('book-page-format', 'Book Page Format');
|
||||||
|
this.dependencies = [];
|
||||||
|
this.format = Object.freeze({
|
||||||
|
id: 'us-mass-market-hardcover',
|
||||||
|
trim: Object.freeze({
|
||||||
|
widthIn: 4.25,
|
||||||
|
heightIn: 6.375
|
||||||
|
}),
|
||||||
|
margins: Object.freeze({
|
||||||
|
topIn: 0.46,
|
||||||
|
bottomIn: 0.58,
|
||||||
|
innerIn: 0.68,
|
||||||
|
outerIn: 0.46
|
||||||
|
}),
|
||||||
|
typography: Object.freeze({
|
||||||
|
fontFamily: '"EB Garamond", "EB Garamond 12", serif',
|
||||||
|
bodyFontSizePt: 10.8,
|
||||||
|
lineHeightPt: 14.9,
|
||||||
|
headingFontSizePt: 13.2,
|
||||||
|
dropCapLines: 2
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bindMethods([
|
||||||
|
'getFormat',
|
||||||
|
'getAspectRatio',
|
||||||
|
'getTextureMetrics',
|
||||||
|
'inchesToTexture'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
this.reportProgress(100, 'Book page format ready');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormat() {
|
||||||
|
return this.format;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAspectRatio() {
|
||||||
|
return this.format.trim.widthIn / this.format.trim.heightIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
inchesToTexture(valueIn, textureHeight) {
|
||||||
|
return (Number(valueIn) / this.format.trim.heightIn) * textureHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextureMetrics(textureWidth = 1280) {
|
||||||
|
const width = Math.max(1, Math.round(Number(textureWidth) || 1280));
|
||||||
|
const height = Math.round(width / this.getAspectRatio());
|
||||||
|
const margins = {
|
||||||
|
top: this.inchesToTexture(this.format.margins.topIn, height),
|
||||||
|
bottom: this.inchesToTexture(this.format.margins.bottomIn, height),
|
||||||
|
inner: this.inchesToTexture(this.format.margins.innerIn, height),
|
||||||
|
outer: this.inchesToTexture(this.format.margins.outerIn, height)
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
aspectRatio: this.getAspectRatio(),
|
||||||
|
margins,
|
||||||
|
content: {
|
||||||
|
x: margins.outer,
|
||||||
|
y: margins.top,
|
||||||
|
width: Math.max(1, width - margins.outer - margins.inner),
|
||||||
|
height: Math.max(1, height - margins.top - margins.bottom)
|
||||||
|
},
|
||||||
|
typography: this.format.typography
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookPageFormat = new BookPageFormatModule();
|
||||||
|
|
||||||
|
export { bookPageFormat as BookPageFormat };
|
||||||
|
|
||||||
|
if (window.moduleRegistry) {
|
||||||
|
window.moduleRegistry.register(bookPageFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.BookPageFormat = bookPageFormat;
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* Book Texture Renderer Module
|
||||||
|
* Draws the virtual book pages directly into texture-space canvases.
|
||||||
|
*/
|
||||||
|
import { BaseModule } from './base-module.js';
|
||||||
|
|
||||||
|
class BookTextureRendererModule extends BaseModule {
|
||||||
|
constructor() {
|
||||||
|
super('book-texture-renderer', 'Book Texture Renderer');
|
||||||
|
this.dependencies = ['book-page-format', 'localization'];
|
||||||
|
this.pageFormat = null;
|
||||||
|
this.localization = null;
|
||||||
|
this.metrics = null;
|
||||||
|
this.canvases = {
|
||||||
|
left: null,
|
||||||
|
right: null
|
||||||
|
};
|
||||||
|
this.contexts = {
|
||||||
|
left: null,
|
||||||
|
right: null
|
||||||
|
};
|
||||||
|
this.hitMaps = {
|
||||||
|
left: [],
|
||||||
|
right: []
|
||||||
|
};
|
||||||
|
|
||||||
|
this.bindMethods([
|
||||||
|
'initialize',
|
||||||
|
'createPageCanvases',
|
||||||
|
'drawEmptySpread',
|
||||||
|
'drawPageBase',
|
||||||
|
'drawDebugText',
|
||||||
|
'publishSpread',
|
||||||
|
'getPageCanvas',
|
||||||
|
'getHitMap',
|
||||||
|
'handleSceneReady'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
this.pageFormat = this.getModule('book-page-format');
|
||||||
|
this.localization = this.getModule('localization');
|
||||||
|
this.reportProgress(20, 'Preparing page texture canvases');
|
||||||
|
this.createPageCanvases();
|
||||||
|
this.drawEmptySpread();
|
||||||
|
this.addEventListener(document, 'webgl-book:scene-ready', this.handleSceneReady);
|
||||||
|
this.reportProgress(100, 'Book texture renderer ready');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPageCanvases(textureWidth = 1280) {
|
||||||
|
this.metrics = this.pageFormat.getTextureMetrics(textureWidth);
|
||||||
|
['left', 'right'].forEach((side) => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = this.metrics.width;
|
||||||
|
canvas.height = this.metrics.height;
|
||||||
|
this.canvases[side] = canvas;
|
||||||
|
this.contexts[side] = canvas.getContext('2d');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
drawEmptySpread() {
|
||||||
|
this.drawPageBase('left');
|
||||||
|
this.drawPageBase('right');
|
||||||
|
this.drawDebugText('right', 'Book canvas renderer ready');
|
||||||
|
this.publishSpread();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPageBase(side) {
|
||||||
|
const canvas = this.canvases[side];
|
||||||
|
const ctx = this.contexts[side];
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.fillStyle = '#fff7dc';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const shade = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
||||||
|
if (side === 'left') {
|
||||||
|
shade.addColorStop(0, 'rgba(255, 255, 255, 0.10)');
|
||||||
|
shade.addColorStop(0.78, 'rgba(255, 255, 255, 0)');
|
||||||
|
shade.addColorStop(1, 'rgba(82, 42, 14, 0.16)');
|
||||||
|
} else {
|
||||||
|
shade.addColorStop(0, 'rgba(82, 42, 14, 0.16)');
|
||||||
|
shade.addColorStop(0.22, 'rgba(255, 255, 255, 0)');
|
||||||
|
shade.addColorStop(1, 'rgba(255, 255, 255, 0.10)');
|
||||||
|
}
|
||||||
|
ctx.fillStyle = shade;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
this.hitMaps[side] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
drawDebugText(side, text) {
|
||||||
|
const ctx = this.contexts[side];
|
||||||
|
const metrics = this.metrics;
|
||||||
|
if (!ctx || !metrics) return;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = 'rgba(31, 19, 10, 0.82)';
|
||||||
|
ctx.font = `${Math.round(metrics.typography.bodyFontSizePt * 1.55)}px ${metrics.typography.fontFamily}`;
|
||||||
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
ctx.fillText(String(text || ''), metrics.content.x, metrics.content.y + 44);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
publishSpread() {
|
||||||
|
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
||||||
|
detail: {
|
||||||
|
left: this.canvases.left,
|
||||||
|
right: this.canvases.right,
|
||||||
|
metrics: this.metrics,
|
||||||
|
hitMaps: this.hitMaps
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageCanvas(side) {
|
||||||
|
return this.canvases[side] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHitMap(side) {
|
||||||
|
return this.hitMaps[side] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSceneReady() {
|
||||||
|
this.publishSpread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookTextureRenderer = new BookTextureRendererModule();
|
||||||
|
|
||||||
|
export { bookTextureRenderer as BookTextureRenderer };
|
||||||
|
|
||||||
|
if (window.moduleRegistry) {
|
||||||
|
window.moduleRegistry.register(bookTextureRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.BookTextureRenderer = bookTextureRenderer;
|
||||||
+3
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260606-webgl-direct-page-crop-coords';
|
const MODULE_CACHE_BUSTER = '20260606-webgl-texture-renderer-foundation';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,6 +113,8 @@ const ModuleLoader = (function() {
|
|||||||
{ id: 'paragraph-layout', script: '/js/paragraph-layout-module.js', weight: 17 },
|
{ id: 'paragraph-layout', script: '/js/paragraph-layout-module.js', weight: 17 },
|
||||||
{ id: 'sentence-queue', script: '/js/sentence-queue-module.js', weight: 12 },
|
{ id: 'sentence-queue', script: '/js/sentence-queue-module.js', weight: 12 },
|
||||||
{ id: 'layout-renderer', script: '/js/layout-renderer-module.js', weight: 13 }, // Add Layout Renderer module
|
{ id: 'layout-renderer', script: '/js/layout-renderer-module.js', weight: 13 }, // Add Layout Renderer module
|
||||||
|
{ id: 'book-page-format', script: '/js/book-page-format-module.js', weight: 4 },
|
||||||
|
{ id: 'book-texture-renderer', script: '/js/book-texture-renderer-module.js', weight: 6 },
|
||||||
{ id: 'webgl-book-scene', script: '/js/webgl-book-scene-module.js', weight: 13 },
|
{ id: 'webgl-book-scene', script: '/js/webgl-book-scene-module.js', weight: 13 },
|
||||||
{ id: 'animation-queue', script: '/js/animation-queue-module.js', weight: 12 },
|
{ id: 'animation-queue', script: '/js/animation-queue-module.js', weight: 12 },
|
||||||
{ id: 'playback-coordinator', script: '/js/playback-coordinator-module.js', weight: 8 }, // Synchronizes animation + TTS
|
{ id: 'playback-coordinator', script: '/js/playback-coordinator-module.js', weight: 8 }, // Synchronizes animation + TTS
|
||||||
|
|||||||
@@ -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 * 0.806,
|
PAGE_WIDTH: 2.24 * 2 / 3,
|
||||||
COVER_DEPTH: 2.30,
|
COVER_DEPTH: 2.30,
|
||||||
OPEN_SEAM_GAP: 0.003,
|
OPEN_SEAM_GAP: 0.003,
|
||||||
PROFILE: {
|
PROFILE: {
|
||||||
|
|||||||
+26
-96
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
|
|||||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-webgl-no-menu-offscreen-dom';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260606-book-page-format-restore';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@@ -25,9 +25,6 @@ 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 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}`;
|
||||||
@@ -44,13 +41,9 @@ 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 = isAppIntegrationMode ? 1280 : 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 pageRaycaster = new THREE.Raycaster();
|
||||||
const pointerNdc = new THREE.Vector2();
|
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;
|
||||||
@@ -402,14 +395,13 @@ window.BookLabDebug = {
|
|||||||
return bookPageCount;
|
return bookPageCount;
|
||||||
},
|
},
|
||||||
redrawPageTextures() {
|
redrawPageTextures() {
|
||||||
redrawPageTexturesFromDom();
|
window.BookTextureRenderer?.publishSpread?.();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
getTextureInfo() {
|
getTextureInfo() {
|
||||||
return {
|
return {
|
||||||
pageTextureWidth,
|
pageTextureWidth,
|
||||||
pageTextureHeight: leftCanvas.height,
|
pageTextureHeight: leftCanvas.height,
|
||||||
appPageTextureInset,
|
|
||||||
debug: getPageTextureDebugState()
|
debug: getPageTextureDebugState()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -424,10 +416,11 @@ window.BookLabDebug = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
document.addEventListener('webgl-book:redraw-pages', redrawPageTexturesFromDom);
|
document.addEventListener('webgl-book:page-canvases', handlePageCanvases);
|
||||||
installBookControls();
|
installBookControls();
|
||||||
installCameraControls();
|
installCameraControls();
|
||||||
resize();
|
resize();
|
||||||
|
document.dispatchEvent(new CustomEvent('webgl-book:scene-ready'));
|
||||||
animate();
|
animate();
|
||||||
|
|
||||||
function buildTable() {
|
function buildTable() {
|
||||||
@@ -1444,35 +1437,24 @@ function syncBookControls() {
|
|||||||
if (fastForwardButton) fastForwardButton.disabled = busy || !canPageFlip(1);
|
if (fastForwardButton) fastForwardButton.disabled = busy || !canPageFlip(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function redrawPageTexturesFromDom() {
|
function handlePageCanvases(event) {
|
||||||
if (pageTextureRenderInProgress) {
|
const detail = event.detail || {};
|
||||||
pageTextureRenderPending = true;
|
if (detail.left) {
|
||||||
return;
|
drawCanvasPageTexture(leftCanvas, detail.left, 'left');
|
||||||
|
leftTexture.needsUpdate = true;
|
||||||
}
|
}
|
||||||
const leftSource = document.getElementById('page_left');
|
if (detail.right) {
|
||||||
const rightSource = document.getElementById('page_right');
|
drawCanvasPageTexture(rightCanvas, detail.right, 'right');
|
||||||
if (!leftSource && !rightSource) return;
|
rightTexture.needsUpdate = true;
|
||||||
pageTextureRenderInProgress = true;
|
}
|
||||||
const serial = ++pageTextureRenderSerial;
|
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
|
||||||
(async () => {
|
width: leftCanvas.width,
|
||||||
try {
|
height: leftCanvas.height,
|
||||||
if (leftSource && await drawDomPageTexture(leftCanvas, leftSource, 'left')) {
|
source: 'book-texture-renderer'
|
||||||
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) {
|
function drawCanvasPageTexture(canvas, sourceCanvas, side) {
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.fillStyle = '#fffaf0';
|
ctx.fillStyle = '#fffaf0';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
@@ -1484,9 +1466,9 @@ async function drawDomPageTexture(canvas, source, side) {
|
|||||||
ctx.fillStyle = shade;
|
ctx.fillStyle = shade;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
const painted = await paintRasterizedDomPage(ctx, canvas, source);
|
ctx.drawImage(sourceCanvas, 0, 0, canvas.width, canvas.height);
|
||||||
updatePageTextureDebugState(side, canvas, source, painted);
|
updatePageTextureDebugState(side, canvas, sourceCanvas, true);
|
||||||
return painted;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPageTextureDebugState() {
|
function getPageTextureDebugState() {
|
||||||
@@ -1505,8 +1487,8 @@ function updatePageTextureDebugState(side, canvas, source, painted) {
|
|||||||
painted,
|
painted,
|
||||||
width: canvas.width,
|
width: canvas.width,
|
||||||
height: canvas.height,
|
height: canvas.height,
|
||||||
sourceId: source.id || '',
|
sourceId: source?.id || 'book-texture-renderer',
|
||||||
sourceTextLength: source.textContent?.trim().length || 0,
|
sourceTextLength: 0,
|
||||||
darkPixels: countPageTextureDarkPixels(canvas)
|
darkPixels: countPageTextureDarkPixels(canvas)
|
||||||
};
|
};
|
||||||
document.documentElement.dataset.webglPageTextures = JSON.stringify(state);
|
document.documentElement.dataset.webglPageTextures = JSON.stringify(state);
|
||||||
@@ -1530,56 +1512,6 @@ function countPageTextureDarkPixels(canvas) {
|
|||||||
return darkPixels;
|
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) {
|
function projectPointerToPage(clientX, clientY) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
if (rect.width <= 0 || rect.height <= 0) return null;
|
if (rect.width <= 0 || rect.height <= 0) return null;
|
||||||
@@ -1592,10 +1524,8 @@ function projectPointerToPage(clientX, clientY) {
|
|||||||
for (const hit of intersections) {
|
for (const hit of intersections) {
|
||||||
const pageSide = textureHitPageSide(hit);
|
const pageSide = textureHitPageSide(hit);
|
||||||
if (!pageSide || !hit.uv) continue;
|
if (!pageSide || !hit.uv) continue;
|
||||||
const insetX = appPageTextureInset;
|
const mappedX = THREE.MathUtils.clamp(hit.uv.x, 0, 1);
|
||||||
const insetY = appPageTextureInset * 0.35;
|
const mappedY = 1 - THREE.MathUtils.clamp(hit.uv.y, 0, 1);
|
||||||
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 {
|
return {
|
||||||
pageId: pageSide === 'left' ? 'page_left' : 'page_right',
|
pageId: pageSide === 'left' ? 'page_left' : 'page_right',
|
||||||
x: mappedX,
|
x: mappedX,
|
||||||
|
|||||||
@@ -439,7 +439,6 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
triggerTextureRefresh() {
|
triggerTextureRefresh() {
|
||||||
clearTimeout(this.textureRefreshTimer);
|
clearTimeout(this.textureRefreshTimer);
|
||||||
this.textureRefreshTimer = setTimeout(() => {
|
this.textureRefreshTimer = setTimeout(() => {
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:redraw-pages'));
|
|
||||||
window.BookLabDebug?.redrawPageTextures?.();
|
window.BookLabDebug?.redrawPageTextures?.();
|
||||||
}, 60);
|
}, 60);
|
||||||
}
|
}
|
||||||
@@ -463,7 +462,6 @@ class WebGLBookSceneModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
if (now - this.lastAnimatedTextureRefresh > 100) {
|
if (now - this.lastAnimatedTextureRefresh > 100) {
|
||||||
this.lastAnimatedTextureRefresh = now;
|
this.lastAnimatedTextureRefresh = now;
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:redraw-pages'));
|
|
||||||
window.BookLabDebug?.redrawPageTextures?.();
|
window.BookLabDebug?.redrawPageTextures?.();
|
||||||
}
|
}
|
||||||
this.textureRefreshAnimationId = window.requestAnimationFrame(tick);
|
this.textureRefreshAnimationId = window.requestAnimationFrame(tick);
|
||||||
|
|||||||
Reference in New Issue
Block a user