From 83b30000da1fe08d835a5619adfc1678fd47456c Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Sat, 6 Jun 2026 00:54:42 +0200 Subject: [PATCH] Checkpoint WebGL page and mirror debug fixes --- public/js/procedural-book-model.js | 38 +++++------ public/js/webgl-book-lab.js | 100 +++++++++++++++++++++-------- 2 files changed, 90 insertions(+), 48 deletions(-) diff --git a/public/js/procedural-book-model.js b/public/js/procedural-book-model.js index 26ba02f..c569b2d 100644 --- a/public/js/procedural-book-model.js +++ b/public/js/procedural-book-model.js @@ -329,15 +329,14 @@ function addSimulatedStackBodies(group, context, model) { const sideLines = model.lines.filter((line) => line.side === side); if (!sideLines.length) return; const isSinglePage = sideLines.length === 1; - const isHairOnlyPage = isSinglePage && sideLines[0].isHairPage === true; const bodyLines = isSinglePage ? createSinglePageBodyLines(context, model, sideLines[0]) : sideLines; - const mesh = new THREE.Mesh(createLoftedLineBody(model, bodyLines, model.pageDepth), createStackBodyMaterials(context, model, side, isSinglePage, isHairOnlyPage)); + const mesh = new THREE.Mesh(createLoftedLineBody(model, bodyLines, model.pageDepth), createStackBodyMaterials(context, model, side, isSinglePage)); mesh.userData.bookPart = side < 0 ? 'leftPages' : 'rightPages'; group.add(mesh); }); } -function createStackBodyMaterials(context, model, side, isSinglePage = false, isHairOnlyPage = false) { +function createStackBodyMaterials(context, model, side, isSinglePage = false) { const baseColor = side < 0 ? '#d8c7a4' : '#e7d6b4'; const lineColor = '#9a8058'; const layerTexture = createStackLayerTexture(context, model.bundleCount, baseColor, lineColor); @@ -351,9 +350,7 @@ function createStackBodyMaterials(context, model, side, isSinglePage = false, is const edge = surface.clone(); edge.map = layerTexture; const bottom = context.materials.pageTop.clone(); - const top = isHairOnlyPage - ? context.materials.pageTop.clone() - : side < 0 && context.materials.leftPage + const top = side < 0 && context.materials.leftPage ? context.materials.leftPage : side > 0 && context.materials.rightPage ? context.materials.rightPage @@ -406,7 +403,6 @@ function createLoftedLineBody(model, lines, depth) { const indices = []; const smoothLines = lines.map((line) => line.points); const bundleCount = model.bundleCount; - const allPagesOnOneSide = lines.length === model.bundleCount && lines.every((line) => line.isHairPage !== true); const push = (point, z, uv) => { const index = positions.length / 3; positions.push(point.x, point.y, z); @@ -434,13 +430,10 @@ function createLoftedLineBody(model, lines, depth) { }); const topCapUv = (point, z, col, row) => { const side = lines[row]?.side ?? 1; - const originalU = smoothLines[row].length <= 1 ? 0.5 : col / (smoothLines[row].length - 1); const pageDistance = side > 0 ? point.x - model.spineHalf : -model.spineHalf - point.x; - const pageU = allPagesOnOneSide - ? THREE.MathUtils.clamp(pageDistance / model.pageWidth, 0, 1) - : originalU; + const pageU = THREE.MathUtils.clamp(pageDistance / model.pageWidth, 0, 1); return { u: side < 0 ? 1 - pageU : pageU, v: 1 - ((z + depth * 0.5) / depth) @@ -476,8 +469,8 @@ function createLoftedLineBody(model, lines, depth) { indices.push(frontA, backA, frontB); indices.push(frontB, backA, backB); } - const bottomRow = allPagesOnOneSide ? smoothLines.length - 1 : 0; - const topRow = allPagesOnOneSide ? 0 : smoothLines.length - 1; + const bottomRow = 0; + const topRow = smoothLines.length - 1; const bottomStart = indices.length; const bottomFront = smoothLines[bottomRow].map((point, col) => push(point, depth * 0.5, capUv(point, depth * 0.5, col, bottomRow))); const bottomBack = smoothLines[bottomRow].map((point, col) => push(point, -depth * 0.5, capUv(point, -depth * 0.5, col, bottomRow))); @@ -513,14 +506,10 @@ function createLoftedLineBody(model, lines, depth) { indices.push(frontA, backA, frontB); indices.push(frontB, backA, backB); } - const pointA = smoothLines[topRow][col]; - const pointB = smoothLines[topRow][col + 1]; - const middleX = (pointA.x + pointB.x) * 0.5; - const isSpineArc = Math.abs(middleX) < model.spineHalf; topGroups.push({ start: groupStart, count: indices.length - groupStart, - materialIndex: allPagesOnOneSide && isSpineArc ? 4 : 3 + materialIndex: 3 }); } const geometry = new THREE.BufferGeometry(); @@ -537,13 +526,20 @@ function createLoftedLineBody(model, lines, depth) { } function createSinglePageBodyLines(context, model, line) { - const supportPoints = line.points.map((point) => ({ + const topPoints = line.points.map((point) => ({ + x: point.x, + y: Math.max( + coverTopYAtX(context, point.x) + coverClearance(model.bundleCount) + PROCEDURAL_BOOK.PROFILE.singlePageCoverGap + model.bundleSpacing, + point.y + ) + })); + const supportPoints = topPoints.map((point) => ({ x: point.x, y: Math.max(coverTopYAtX(context, point.x) + coverClearance(model.bundleCount) + PROCEDURAL_BOOK.PROFILE.singlePageCoverGap, point.y - model.bundleSpacing) })); return [ { ...line, points: supportPoints, endpoint: supportPoints[supportPoints.length - 1] }, - line + { ...line, points: topPoints, endpoint: topPoints[topPoints.length - 1] } ]; } @@ -756,7 +752,7 @@ function calculateBundleSpacing(bundleCount, spineWidth, leftCount) { } function calculateLeftBundleCount(context, bundleCount) { - return THREE.MathUtils.clamp(Math.round(bundleCount * context.readingProgress), 0, bundleCount); + return THREE.MathUtils.clamp(Math.round(bundleCount * context.readingProgress), 1, bundleCount - 1); } function buildSpineArcSamples(spineWidth) { diff --git a/public/js/webgl-book-lab.js b/public/js/webgl-book-lab.js index 5d78c57..735e2c0 100644 --- a/public/js/webgl-book-lab.js +++ b/public/js/webgl-book-lab.js @@ -17,7 +17,8 @@ const tableDebugModes = { scene: 5, mask: 6, ao: 7, - grease: 8 + grease: 8, + mirror: 10 }; const urlParams = new URLSearchParams(window.location.search); const tableDebugName = urlParams.get('tableDebug') || 'none'; @@ -37,6 +38,7 @@ renderer.shadowMap.type = THREE.VSMShadowMap; const generatedTextureCanvases = {}; const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy(); const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2); +const pageTextureWidth = 3200; const reflectionTargetSize = new THREE.Vector2(); let sceneComposerTarget = null; let composer = null; @@ -1151,6 +1153,7 @@ function configureTableShader(material) { if (tableDebugMode == 5) outgoingLight = sceneReflection; if (tableDebugMode == 6) outgoingLight = vec3(tableReflectionMask); if (tableDebugMode == 8) outgoingLight = vec3(grease); + if (tableDebugMode == 10) outgoingLight = combinedReflection; #include ` ); }; @@ -1787,8 +1790,8 @@ function createGutterGeometry(width, height, depth) { function createPageCanvas(side) { const canvas = document.createElement('canvas'); - canvas.width = 3000; - canvas.height = 4500; + canvas.width = pageTextureWidth; + canvas.height = Math.round(pageTextureWidth * PROCEDURAL_BOOK.PAGE_DEPTH / PROCEDURAL_BOOK.PAGE_WIDTH); const ctx = canvas.getContext('2d'); ctx.fillStyle = '#f5dfab'; ctx.fillRect(0, 0, canvas.width, canvas.height); @@ -1804,29 +1807,35 @@ function createPageCanvas(side) { ctx.fillStyle = inkColor; ctx.textBaseline = 'top'; - const margins = hardcoverPageMargins(canvas, side); - const contentWidth = canvas.width - margins.left - margins.right; - const contentHeight = canvas.height - margins.top - margins.bottom; + const layout = hardcoverPageLayout(canvas, side); if (side === 'left') { - drawCentered(ctx, 'Georg Tomitsch', margins.top + contentHeight * 0.07, 58, margins.left, contentWidth); - drawCentered(ctx, 'Eibenreith', margins.top + contentHeight * 0.12, 132, margins.left, contentWidth); - drawCentered(ctx, 'Ein Kaiserpunk Abenteuer', margins.top + contentHeight * 0.185, 70, margins.left, contentWidth); - drawCentered(ctx, 'speech | autoplay | speed | new game | save | load | options', margins.top + contentHeight * 0.30, 42, margins.left, contentWidth); - drawCentered(ctx, 'click on page or press spacebar to fast forward text animation', margins.top + contentHeight * 0.36, 42, margins.left, contentWidth); + drawTitlePage(ctx, layout); } else { - drawParagraph(ctx, 'Click on new game or load to start the game', margins.left, margins.top, contentWidth, 86, 1.42); + drawNovelPage(ctx, layout, 'Click on new game or load to start the game'); } return canvas; } -function hardcoverPageMargins(canvas, side) { - const gutter = canvas.width * 0.17; - const outer = canvas.width * 0.12; +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 { - left: side === 'right' ? gutter : outer, - right: side === 'right' ? outer : gutter, - top: canvas.height * 0.14, - bottom: canvas.height * 0.18 + margins, + x: margins.left, + y: margins.top, + width, + height, + em: width / 24 }; } @@ -1983,28 +1992,45 @@ function tintAmbientFromCanvas(canvas) { 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) { + drawParagraph(ctx, text, layout.x, layout.y, layout.width, layout.em * 0.98, 1.36, layout.em * 1.25); +} + function drawCentered(ctx, text, y, size, x = 0, width = ctx.canvas.width) { - ctx.font = `${size}px Georgia, "Times New Roman", serif`; + 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) { - ctx.font = `${size}px Georgia, "Times New Roman", serif`; +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 && line) { - ctx.fillText(line, x, y); + if (ctx.measureText(test).width > width - indent && line) { + ctx.fillText(line, x + indent, y); line = word; - y += size * lineHeight; + y += fontSize * lineHeight; + indent = 0; } else { line = test; } }); - if (line) ctx.fillText(line, x, y); + if (line) ctx.fillText(line, x + indent, y); } function resize() { @@ -2236,6 +2262,22 @@ function updateTableReflection() { delete tableMesh.userData.wasVisibleForTableReflection; } +function renderMirrorDebugView() { + const hiddenObjects = []; + scene.traverse((object) => { + if (object === tableMesh || !object.visible) return; + if (!object.isMesh && !object.isLine && !object.isPoints && !object.isSprite) return; + object.userData.wasVisibleForMirrorDebug = true; + object.visible = false; + hiddenObjects.push(object); + }); + renderer.render(scene, camera); + hiddenObjects.forEach((object) => { + object.visible = object.userData.wasVisibleForMirrorDebug; + delete object.userData.wasVisibleForMirrorDebug; + }); +} + function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); @@ -2272,7 +2314,11 @@ function animate() { updateCandleShadowUniforms(); updateBookShadowMaps(); updateTableReflection(); - if (composer) { + if (tableDebugMode === tableDebugModes.mirror) { + renderer.setRenderTarget(null); + renderer.clear(); + renderMirrorDebugView(); + } else if (composer) { composer.render(); } else { renderer.render(scene, camera);