diff --git a/public/js/webgl-book-shape-lab.js b/public/js/webgl-book-shape-lab.js index c16d675..2b3d9f7 100644 --- a/public/js/webgl-book-shape-lab.js +++ b/public/js/webgl-book-shape-lab.js @@ -17,6 +17,8 @@ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setClearColor(0x202124, 1); +const GRID_Y = -0.12; + const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 30); if (urlParams.get('view') === 'profile') { @@ -38,7 +40,7 @@ const book = new THREE.Group(); scene.add(book); const guide = new THREE.GridHelper(5.6, 16, 0x4c4c4c, 0x343434); -guide.position.y = -0.12; +guide.position.y = GRID_Y; scene.add(guide); const materials = { @@ -60,6 +62,8 @@ const BOOK_PROFILE = { singlePageCoverGap: 0.006, bundleSpacing: 0.014 }; + +book.position.y = GRID_Y - BOOK_PROFILE.tableY; const NORMAL_FLIP_DURATION = 1800; const FAST_FLIP_DURATION = 900; const FAST_FLIP_COUNT = 10; @@ -74,7 +78,6 @@ const PAGE_DEPTH = 2.24; const COVER_DEPTH = 2.30; const COVER_OVERHANG = (COVER_DEPTH - PAGE_DEPTH) * 0.5; const COVER_SUPPORT_OVERHANG = COVER_OVERHANG; -const HINGE_INSET = 0.07; const SUPPORT_ANGLE_STEPS = 720; const SUPPORT_ANGLE_CANDIDATES = Array.from({ length: SUPPORT_ANGLE_STEPS }, (_, sample) => { const angle = sample / SUPPORT_ANGLE_STEPS * Math.PI * 2; @@ -93,6 +96,7 @@ let lastLengthError = 0; let lastSpacingError = 0; let lastBookModel = null; let activeSpineHalf = 0.08; +let activeCoverOuterX = activeSpineHalf + PAGE_WIDTH + COVER_OVERHANG; let activeFlips = []; let pendingPageFlips = 0; pageCountInput.max = String(maximumPageCount); @@ -144,6 +148,25 @@ window.BookShapeLab = { get lastSpacingError() { return lastSpacingError; }, + get pageMetrics() { + return visiblePageMetrics(); + }, + get frameMetrics() { + if (!lastBookModel) return null; + return { + spineHalf: spineArcHalf(lastBookModel.spineWidth), + hingeWidth: hingeInset(), + hingeHeight: BOOK_PROFILE.raisedHingeY - BOOK_PROFILE.coverThickness, + pageWidth: lastBookModel.pageWidth, + pageDepth: lastBookModel.pageDepth, + coverDepth: lastBookModel.coverDepth, + coverOuterX: lastBookModel.coverOuterX, + solvedStackOuterX: solvedStackOuterX(lastBookModel.lines), + coverWidthOverhang: COVER_OVERHANG, + actualCoverWidthOverhang: lastBookModel.coverOuterX - solvedStackOuterX(lastBookModel.lines), + coverDepthOverhang: (lastBookModel.coverDepth - lastBookModel.pageDepth) * 0.5 + }; + }, setReadingProgress(value) { setReadingProgress(value); return readingProgress; @@ -166,6 +189,29 @@ window.BookShapeLab = { } }; +function visiblePageMetrics() { + if (!lastBookModel) return null; + const result = {}; + [-1, 1].forEach((side) => { + const line = topVisibleLine(side); + if (!line) return; + let pathLength = 0; + for (let index = 0; index < line.points.length - 1; index += 1) { + pathLength += Math.hypot(line.points[index + 1].x - line.points[index].x, line.points[index + 1].y - line.points[index].y); + } + result[side < 0 ? 'left' : 'right'] = { + anchorX: line.anchor.x, + anchorY: line.anchor.y, + endpointX: line.endpoint.x, + endpointY: line.endpoint.y, + xSpan: Math.abs(line.endpoint.x - line.anchor.x), + ySpan: Math.abs(line.endpoint.y - line.anchor.y), + pathLength + }; + }); + return result; +} + function readInitialProgress() { const parsed = Number.parseFloat(urlParams.get('progress') ?? '0.25'); return Number.isFinite(parsed) ? THREE.MathUtils.clamp(parsed, 0, 1) : 0.25; @@ -216,16 +262,19 @@ function rebuildBook() { const spineWidth = calculateSpineWidth(bundleCount); const leftCount = calculateLeftBundleCount(bundleCount); const spineHalf = spineArcHalf(spineWidth); - const hingeX = spineHalf + HINGE_INSET; + const hingeX = spineHalf + hingeInset(); const foreEdgeX = spineHalf + pageWidth; const bundleSpacing = calculateBundleSpacing(bundleCount, spineWidth, leftCount); activeSpineHalf = spineHalf; + activeCoverOuterX = spineHalf + pageWidth + COVER_OVERHANG; const lines = simulatePageLines(bundleCount, pageWidth, pageSplineLength, spineWidth, foreEdgeX, bundleSpacing, leftCount); + const coverOuterX = solvedStackOuterX(lines) + COVER_OVERHANG; + activeCoverOuterX = coverOuterX; lastLengthError = measureLineLengthError(lines, pageSplineLength); lastSpacingError = measureStackSpacingError(lines, bundleSpacing); - lastBookModel = { coverDepth, pageWidth, pageSplineLength, pageDepth, bundleCount, spineWidth, hingeX, foreEdgeX, bundleSpacing, lines }; + lastBookModel = { coverDepth, pageWidth, pageSplineLength, pageDepth, bundleCount, spineWidth, hingeX, foreEdgeX, coverOuterX, bundleSpacing, lines }; - addCoverAssembly(pageWidth, coverDepth, coverThickness, spineWidth); + addCoverAssembly(pageWidth, coverDepth, coverThickness, spineWidth, coverOuterX); addClothSpine(pageDepth, spineWidth); addSimulatedStackBodies(lines, pageDepth); updateFlipControls(); @@ -241,15 +290,15 @@ function clearGroup(group) { } } -function addCoverAssembly(pageWidth, depth, thickness, spineWidth) { - const cover = new THREE.Mesh(createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth), materials.cover); +function addCoverAssembly(pageWidth, depth, thickness, spineWidth, coverOuterX) { + const cover = new THREE.Mesh(createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth, coverOuterX), materials.cover); book.add(cover); } -function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth) { +function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth, coverOuterX) { const spineHalf = spineArcHalf(spineWidth); - const hingeX = spineHalf + HINGE_INSET; - const outerX = spineHalf + pageWidth + COVER_OVERHANG; + const hingeX = spineHalf + hingeInset(); + const outerX = coverOuterX ?? spineHalf + pageWidth + COVER_OVERHANG; const outerTopY = BOOK_PROFILE.tableY + thickness; const connectionTopY = BOOK_PROFILE.raisedHingeY; const spineTopY = BOOK_PROFILE.tableY + thickness; @@ -356,6 +405,10 @@ function spineArcHalf(spineWidth) { return spineWidth * 0.42; } +function hingeInset() { + return Math.max(0.001, BOOK_PROFILE.raisedHingeY - BOOK_PROFILE.coverThickness); +} + function calculateSpineWidth(bundleCount) { const minimumWidth = 0.16; if (bundleCount <= 1) return minimumWidth; @@ -421,6 +474,12 @@ function calculateLeftBundleCount(bundleCount) { return THREE.MathUtils.clamp(Math.round(bundleCount * readingProgress), 0, bundleCount); } +function solvedStackOuterX(lines) { + return lines.reduce((outer, line) => { + return line.points.reduce((lineOuter, point) => Math.max(lineOuter, Math.abs(point.x)), outer); + }, 0); +} + function simulatePageLines(bundleCount, pageWidth, pageSplineLength, spineWidth, foreEdgeX, bundleSpacing, leftCount) { const lines = []; const segments = PAGE_LINE_SEGMENTS; @@ -836,8 +895,8 @@ function upwardNormalAt(points, index) { function coverTopYAtX(x) { const ax = Math.abs(x); const spineHalf = currentSpineHalf(); - const hingeX = spineHalf + HINGE_INSET; - const outerX = spineHalf + PAGE_WIDTH + COVER_SUPPORT_OVERHANG; + const hingeX = spineHalf + hingeInset(); + const outerX = activeCoverOuterX; if (ax <= spineHalf) return BOOK_PROFILE.coverThickness; if (ax <= hingeX) { const t = (ax - spineHalf) / (hingeX - spineHalf); @@ -969,6 +1028,11 @@ function createLoftedLineBody(lines, depth) { indices.push(frontA, frontB, backA); indices.push(frontB, backB, backA); } + const bottomStart = indices.length; + for (let col = 0; col < smoothLines[0].length - 1; col += 1) { + indices.push(front[0][col], front[0][col + 1], back[0][col]); + indices.push(front[0][col + 1], back[0][col + 1], back[0][col]); + } const topStart = indices.length; for (let col = 0; col < smoothLines[0].length - 1; col += 1) { const topRow = smoothLines.length - 1; @@ -981,7 +1045,8 @@ function createLoftedLineBody(lines, depth) { geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)); geometry.clearGroups(); geometry.addGroup(0, sideStart, 0); - geometry.addGroup(sideStart, topStart - sideStart, 1); + geometry.addGroup(sideStart, bottomStart - sideStart, 1); + geometry.addGroup(bottomStart, topStart - bottomStart, 2); geometry.addGroup(topStart, indices.length - topStart, 2); geometry.computeVertexNormals(); return geometry; diff --git a/public/webgl-book-shape-lab.html b/public/webgl-book-shape-lab.html index 5cc99ed..edd9b5b 100644 --- a/public/webgl-book-shape-lab.html +++ b/public/webgl-book-shape-lab.html @@ -74,6 +74,6 @@ 0 / 10 - +