From 4adf85b4d28cd5d3ee6ec82a4fef09f71d3b5de8 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Thu, 4 Jun 2026 23:39:44 +0200 Subject: [PATCH] Checkpoint curve-origin book shape lab --- public/js/webgl-book-shape-lab.js | 93 ++++++++++++++++++------------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/public/js/webgl-book-shape-lab.js b/public/js/webgl-book-shape-lab.js index 16efce3..42a7f29 100644 --- a/public/js/webgl-book-shape-lab.js +++ b/public/js/webgl-book-shape-lab.js @@ -100,14 +100,14 @@ function rebuildBook() { const fullBlock = 0.41; const leftThickness = THREE.MathUtils.lerp(sheetTick, fullBlock, readingProgress); const rightThickness = THREE.MathUtils.lerp(fullBlock, sheetTick, readingProgress); - const foldX = clothFoldX(gutter); const spineWidth = fullBlock; + const fold = spineCurvePoint(readingProgress, spineWidth); addCoverAssembly(pageWidth, coverDepth, coverThickness, spineWidth); - addSplinePageBlock(-1, pageWidth, pageDepth, leftThickness, foldX); - addSplinePageBlock(1, pageWidth, pageDepth, rightThickness, foldX); - addPageLayerLines(-1, pageWidth, pageDepth, leftThickness, foldX); - addPageLayerLines(1, pageWidth, pageDepth, rightThickness, foldX); + addSplinePageBlock(-1, pageWidth, pageDepth, leftThickness, fold, spineWidth); + addSplinePageBlock(1, pageWidth, pageDepth, rightThickness, fold, spineWidth); + addPageLayerLines(-1, pageWidth, pageDepth, leftThickness, spineWidth, fold); + addPageLayerLines(1, pageWidth, pageDepth, rightThickness, spineWidth, fold); addClothSpine(pageDepth, spineWidth); } @@ -185,45 +185,38 @@ function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth) { return geometry; } -function addSplinePageBlock(side, width, depth, thickness, foldX) { - const block = new THREE.Mesh(createSplinePageBlockGeometry(side, width, depth, thickness, foldX), side < 0 ? materials.pagesLeft : materials.pagesRight); +function addSplinePageBlock(side, width, depth, thickness, fold, spineWidth) { + const block = new THREE.Mesh(createSplinePageBlockGeometry(side, width, depth, thickness, fold, spineWidth), side < 0 ? materials.pagesLeft : materials.pagesRight); book.add(block); } -function addPageLayerLines(side, width, depth, thickness, foldX) { +function addPageLayerLines(side, width, depth, thickness, spineWidth, fold) { const material = new THREE.LineBasicMaterial({ color: 0x8f7750, transparent: true, opacity: 0.55 }); const z = depth * 0.5 + 0.006; const lineCount = Math.max(1, Math.round(thickness / 0.018)); for (let layer = 1; layer < lineCount; layer += 1) { const t = layer / lineCount; + const curveT = side < 0 + ? THREE.MathUtils.lerp(0, readingProgress, t) + : THREE.MathUtils.lerp(1, readingProgress, t); + const lineFold = spineCurvePoint(curveT, spineWidth); const points = []; for (let i = 0; i <= 40; i += 1) { - const u = 0.04 + 0.96 * (i / 40); - const top = pageBlockTopY(thickness, u, 0.5); - const bottom = pageBlockBottomY(thickness, u, 0.5); - points.push(new THREE.Vector3(pageX(side, foldX, width, u, 0.5), bottom + (top - bottom) * t, z)); + const u = i / 40; + const top = pageBlockTopY(side, thickness, u, 0.5, lineFold, spineWidth); + const bottom = pageBlockBottomY(side, thickness, u, 0.5, lineFold, spineWidth); + points.push(new THREE.Vector3(pageX(side, lineFold, spineWidth, width, u, 0.5), bottom + (top - bottom) * t, z)); } book.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), material)); } } -function clothFoldX(gutter) { - const spineTravel = gutter * 0.52; - return THREE.MathUtils.lerp(-spineTravel, spineTravel, readingProgress); -} - function addClothSpine(depth, spineWidth) { const material = new THREE.LineBasicMaterial({ color: 0xb51f1f }); - const radiusX = spineWidth * 0.42; - const radiusY = 0.018; - const baseY = BOOK_PROFILE.tableY + BOOK_PROFILE.coverThickness + 0.002; const profile = []; for (let i = 0; i <= 32; i += 1) { const u = i / 32; - const theta = Math.PI * (1 - u); - const x = Math.cos(theta) * radiusX; - const y = baseY + Math.sin(theta) * radiusY; - profile.push({ x, y }); + profile.push(spineCurvePoint(u, spineWidth)); } [depth * 0.5 + 0.008, -depth * 0.5 - 0.008].forEach((z) => { @@ -232,26 +225,42 @@ function addClothSpine(depth, spineWidth) { }); } -function pageBlockTopY(thickness, u, v) { +function spineCurvePoint(t, spineWidth) { + const radiusX = spineWidth * 0.42; + const radiusY = 0.018; + const baseY = BOOK_PROFILE.tableY + BOOK_PROFILE.coverThickness + 0.002; + const theta = Math.PI * (1 - THREE.MathUtils.clamp(t, 0, 1)); + return { + t: THREE.MathUtils.clamp(t, 0, 1), + x: Math.cos(theta) * radiusX, + y: baseY + Math.sin(theta) * radiusY + }; +} + +function pageBlockTopY(side, thickness, u, v, fold, spineWidth) { const hingeWidth = 0.105; const hinge = THREE.MathUtils.clamp(u / hingeWidth, 0, 1); const t = easeOutCubic(hinge); - const sewnY = 0.066; - const stackY = pageBlockBottomY(thickness, u, v) + thickness; + const sewnY = fold.y + 0.002; + const stackY = pageBlockBottomY(side, thickness, u, v, fold, spineWidth) + thickness; const flatCrown = 0.006 * Math.sin(Math.PI * v); const foreCurl = 0.006 * smoothstep(THREE.MathUtils.clamp((u - 0.88) / 0.12, 0, 1)); return sewnY * (1 - t) + (stackY + flatCrown + foreCurl) * t; } -function pageBlockBottomY(thickness, u, v) { - const supportY = coverSupportTopY(u); - return supportY + BOOK_PROFILE.paperContactOffset; +function pageBlockBottomY(side, thickness, u, v, fold, spineWidth) { + const curveEnd = 0.34; + if (u <= curveEnd) { + return pageCurvePoint(side, fold, spineWidth, u, curveEnd).y; + } + const flatY = BOOK_PROFILE.coverThickness + BOOK_PROFILE.paperContactOffset; + return flatY; } -function coverSupportTopY(u) { - const hingeSpan = 0.14; - const t = THREE.MathUtils.clamp((u - hingeSpan) / (1 - hingeSpan), 0, 1); - return THREE.MathUtils.lerp(BOOK_PROFILE.raisedHingeY, BOOK_PROFILE.coverThickness, t); +function pageCurvePoint(side, fold, spineWidth, u, curveEnd) { + const along = THREE.MathUtils.clamp(u / curveEnd, 0, 1); + const targetT = side < 0 ? 0 : 1; + return spineCurvePoint(THREE.MathUtils.lerp(fold.t, targetT, along), spineWidth); } function smoothstep(value) { @@ -266,12 +275,18 @@ function pageWidthAtDepth(width, v) { return width; } -function pageX(side, foldX, width, u, v = 0.5) { +function pageX(side, fold, spineWidth, width, u, v = 0.5) { + const curveEnd = 0.34; + if (u <= curveEnd) { + return pageCurvePoint(side, fold, spineWidth, u, curveEnd).x; + } + const outerT = THREE.MathUtils.clamp((u - curveEnd) / (1 - curveEnd), 0, 1); + const curveEndX = spineCurvePoint(side < 0 ? 0 : 1, spineWidth).x; const foreX = side * pageWidthAtDepth(width, v); - return foldX * (1 - u) + foreX * u; + return curveEndX * (1 - outerT) + foreX * outerT; } -function createSplinePageBlockGeometry(side, width, depth, thickness, foldX) { +function createSplinePageBlockGeometry(side, width, depth, thickness, fold, spineWidth) { const columns = 36; const rows = 36; const positions = []; @@ -294,8 +309,8 @@ function createSplinePageBlockGeometry(side, width, depth, thickness, foldX) { for (let column = 0; column <= columns; column += 1) { const u = column / columns; const z = (v - 0.5) * depth; - top[row][column] = push(pageX(side, foldX, width, u, v), pageBlockTopY(thickness, u, v), z, u, v); - bottom[row][column] = push(pageX(side, foldX, width, u, v), pageBlockBottomY(thickness, u, v), z, u, v); + top[row][column] = push(pageX(side, fold, spineWidth, width, u, v), pageBlockTopY(side, thickness, u, v, fold, spineWidth), z, u, v); + bottom[row][column] = push(pageX(side, fold, spineWidth, width, u, v), pageBlockBottomY(side, thickness, u, v, fold, spineWidth), z, u, v); } }