Start texture-space book renderer

This commit is contained in:
2026-06-06 14:51:07 +02:00
parent 326f812b22
commit 62215b280f
6 changed files with 259 additions and 100 deletions
+26 -96
View File
@@ -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 { 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 { 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');
canvas.style.cursor = 'grab';
@@ -25,9 +25,6 @@ const appInitialState = window.WebGLBookInitialState || {};
const tableDebugName = urlParams.get('tableDebug') || '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');
if (labStatus && tableDebugMode !== tableDebugModes.none) {
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
@@ -44,13 +41,9 @@ const generatedTextureCanvases = {};
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2);
const pageTextureWidth = isAppIntegrationMode ? 1280 : 3200;
const appPageTextureInset = 0;
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 composer = null;
let sceneRenderPass = null;
@@ -402,14 +395,13 @@ window.BookLabDebug = {
return bookPageCount;
},
redrawPageTextures() {
redrawPageTexturesFromDom();
window.BookTextureRenderer?.publishSpread?.();
return true;
},
getTextureInfo() {
return {
pageTextureWidth,
pageTextureHeight: leftCanvas.height,
appPageTextureInset,
debug: getPageTextureDebugState()
};
},
@@ -424,10 +416,11 @@ window.BookLabDebug = {
};
window.addEventListener('resize', resize);
document.addEventListener('webgl-book:redraw-pages', redrawPageTexturesFromDom);
document.addEventListener('webgl-book:page-canvases', handlePageCanvases);
installBookControls();
installCameraControls();
resize();
document.dispatchEvent(new CustomEvent('webgl-book:scene-ready'));
animate();
function buildTable() {
@@ -1444,35 +1437,24 @@ function syncBookControls() {
if (fastForwardButton) fastForwardButton.disabled = busy || !canPageFlip(1);
}
function redrawPageTexturesFromDom() {
if (pageTextureRenderInProgress) {
pageTextureRenderPending = true;
return;
function handlePageCanvases(event) {
const detail = event.detail || {};
if (detail.left) {
drawCanvasPageTexture(leftCanvas, detail.left, 'left');
leftTexture.needsUpdate = true;
}
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();
}
}
})();
if (detail.right) {
drawCanvasPageTexture(rightCanvas, detail.right, 'right');
rightTexture.needsUpdate = true;
}
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
width: leftCanvas.width,
height: leftCanvas.height,
source: 'book-texture-renderer'
});
}
async function drawDomPageTexture(canvas, source, side) {
function drawCanvasPageTexture(canvas, sourceCanvas, side) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fffaf0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
@@ -1484,9 +1466,9 @@ async function drawDomPageTexture(canvas, source, side) {
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;
ctx.drawImage(sourceCanvas, 0, 0, canvas.width, canvas.height);
updatePageTextureDebugState(side, canvas, sourceCanvas, true);
return true;
}
function getPageTextureDebugState() {
@@ -1505,8 +1487,8 @@ function updatePageTextureDebugState(side, canvas, source, painted) {
painted,
width: canvas.width,
height: canvas.height,
sourceId: source.id || '',
sourceTextLength: source.textContent?.trim().length || 0,
sourceId: source?.id || 'book-texture-renderer',
sourceTextLength: 0,
darkPixels: countPageTextureDarkPixels(canvas)
};
document.documentElement.dataset.webglPageTextures = JSON.stringify(state);
@@ -1530,56 +1512,6 @@ function countPageTextureDarkPixels(canvas) {
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;
@@ -1592,10 +1524,8 @@ function projectPointerToPage(clientX, clientY) {
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);
const mappedX = THREE.MathUtils.clamp(hit.uv.x, 0, 1);
const mappedY = 1 - THREE.MathUtils.clamp(hit.uv.y, 0, 1);
return {
pageId: pageSide === 'left' ? 'page_left' : 'page_right',
x: mappedX,