From 073be20dcae322d3ce2e864f6d8d56b37f065d27 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Thu, 4 Jun 2026 23:03:33 +0200 Subject: [PATCH] Checkpoint clean procedural book profile --- public/js/webgl-book-shape-lab.js | 122 +++++++++++++----------------- 1 file changed, 53 insertions(+), 69 deletions(-) diff --git a/public/js/webgl-book-shape-lab.js b/public/js/webgl-book-shape-lab.js index eb368b4..16efce3 100644 --- a/public/js/webgl-book-shape-lab.js +++ b/public/js/webgl-book-shape-lab.js @@ -35,7 +35,7 @@ guide.position.y = -0.12; scene.add(guide); const materials = { - cover: new THREE.MeshBasicMaterial({ color: 0x2c1810 }), + cover: new THREE.MeshBasicMaterial({ color: 0x2c1810, side: THREE.DoubleSide }), spine: new THREE.MeshBasicMaterial({ color: 0x9c1f1f, side: THREE.DoubleSide }), pagesLeft: new THREE.MeshBasicMaterial({ color: 0xd8c7a4, side: THREE.DoubleSide }), pagesRight: new THREE.MeshBasicMaterial({ color: 0xe7d6b4, side: THREE.DoubleSide }), @@ -44,6 +44,13 @@ const materials = { hinge: new THREE.MeshBasicMaterial({ color: 0x2b0808 }) }; +const BOOK_PROFILE = { + tableY: 0, + coverThickness: 0.03, + raisedHingeY: 0.056, + paperContactOffset: 0.0012 +}; + let readingProgress = readInitialProgress(); progressInput.value = readingProgress.toFixed(3); progressValue.value = readingProgress.toFixed(2); @@ -85,7 +92,7 @@ function rebuildBook() { clearGroup(book); const coverDepth = 2.34; - const coverThickness = 0.022; + const coverThickness = BOOK_PROFILE.coverThickness; const pageWidth = 1.62; const pageDepth = 2.24; const gutter = 0.12; @@ -94,13 +101,14 @@ function rebuildBook() { const leftThickness = THREE.MathUtils.lerp(sheetTick, fullBlock, readingProgress); const rightThickness = THREE.MathUtils.lerp(fullBlock, sheetTick, readingProgress); const foldX = clothFoldX(gutter); + const spineWidth = fullBlock; - addCoverAssembly(pageWidth, coverDepth, coverThickness); + 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); - addClothSpine(pageDepth, gutter); + addClothSpine(pageDepth, spineWidth); } function clearGroup(group) { @@ -113,24 +121,27 @@ function clearGroup(group) { } } -function addCoverAssembly(pageWidth, depth, thickness) { - const cover = new THREE.Mesh(createCoverAssemblyGeometry(pageWidth, depth, thickness), materials.cover); +function addCoverAssembly(pageWidth, depth, thickness, spineWidth) { + const cover = new THREE.Mesh(createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth), materials.cover); book.add(cover); } -function createCoverAssemblyGeometry(pageWidth, depth, thickness) { +function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth) { const overhang = 0.055; - const hingeX = 0.18; - const centerX = 0.095; - const boardTopY = 0.023; - const centerTopY = -0.002; + const spineHalf = spineWidth * 0.5; + const hingeInset = 0.07; + const outerX = pageWidth + overhang; + const hingeX = spineHalf + hingeInset; + const outerTopY = BOOK_PROFILE.tableY + thickness; + const connectionTopY = BOOK_PROFILE.raisedHingeY; + const spineTopY = BOOK_PROFILE.tableY + thickness; const section = [ - { x: -pageWidth - overhang, y: boardTopY }, - { x: -hingeX, y: boardTopY }, - { x: -centerX, y: centerTopY }, - { x: centerX, y: centerTopY }, - { x: hingeX, y: boardTopY }, - { x: pageWidth + overhang, y: boardTopY } + { x: -outerX, y: outerTopY }, + { x: -hingeX, y: connectionTopY }, + { x: -spineHalf, y: spineTopY }, + { x: spineHalf, y: spineTopY }, + { x: hingeX, y: connectionTopY }, + { x: outerX, y: outerTopY } ]; const positions = []; const uvs = []; @@ -201,53 +212,24 @@ function clothFoldX(gutter) { return THREE.MathUtils.lerp(-spineTravel, spineTravel, readingProgress); } -function addClothSpine(depth, gutter) { - const geometry = createClothSpineGeometry(depth, gutter); - const spine = new THREE.Mesh(geometry, materials.spine); - book.add(spine); -} - -function createClothSpineGeometry(depth, gutter) { - const columns = 24; - const rows = 18; - const positions = []; - const uvs = []; - const indices = []; - const radiusX = gutter * 0.52; - const radiusY = 0.04; - const baseY = -0.004; - - for (let row = 0; row <= rows; row += 1) { - const v = row / rows; - const z = (v - 0.5) * depth * 0.94; - for (let column = 0; column <= columns; column += 1) { - const u = column / columns; - const theta = Math.PI * (1 - u); - const x = Math.cos(theta) * radiusX; - const y = baseY + Math.sin(theta) * radiusY + x * 0.12; - const exposedY = Math.min(y, 0.038); - const lengthSag = -0.006 * Math.sin(Math.PI * v); - positions.push(x, exposedY + lengthSag, z); - uvs.push(u, v); - } +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 }); } - for (let row = 0; row < rows; row += 1) { - for (let column = 0; column < columns; column += 1) { - const a = row * (columns + 1) + column; - const b = a + 1; - const c = a + columns + 1; - const d = c + 1; - indices.push(a, c, b, b, c, d); - } - } - - const geometry = new THREE.BufferGeometry(); - geometry.setIndex(indices); - geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); - geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)); - geometry.computeVertexNormals(); - return geometry; + [depth * 0.5 + 0.008, -depth * 0.5 - 0.008].forEach((z) => { + const points = profile.map((point) => new THREE.Vector3(point.x, point.y, z)); + book.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), material)); + }); } function pageBlockTopY(thickness, u, v) { @@ -255,19 +237,21 @@ function pageBlockTopY(thickness, u, v) { const hinge = THREE.MathUtils.clamp(u / hingeWidth, 0, 1); const t = easeOutCubic(hinge); const sewnY = 0.066; - const stackY = 0.032 + thickness; + const stackY = pageBlockBottomY(thickness, u, v) + 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 hingeWidth = 0.105; - const hinge = THREE.MathUtils.clamp(u / hingeWidth, 0, 1); - const t = easeOutCubic(hinge); - const sewnY = 0.026; - const stackY = 0.026; - return sewnY * (1 - t) + stackY * t; + const supportY = coverSupportTopY(u); + return supportY + BOOK_PROFILE.paperContactOffset; +} + +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 smoothstep(value) {