diff --git a/public/js/webgl-book-shape-lab.js b/public/js/webgl-book-shape-lab.js index 7e4c3aa..c2b97e3 100644 --- a/public/js/webgl-book-shape-lab.js +++ b/public/js/webgl-book-shape-lab.js @@ -197,7 +197,6 @@ function rebuildBook() { addCoverAssembly(pageWidth, coverDepth, coverThickness, spineWidth); addClothSpine(pageDepth, spineWidth); addSimulatedStackBodies(lines, pageDepth); - addSimulatedPageLines(lines, pageDepth); updateFlipControls(); } @@ -724,24 +723,13 @@ function currentSpineHalf() { return Math.max(0.16, Math.round(pageCount / 10) * BOOK_PROFILE.bundleSpacing) * 0.5; } -function addSimulatedPageLines(lines, depth) { - const leftMaterial = new THREE.LineBasicMaterial({ color: 0x8f7750, transparent: true, opacity: 0.72 }); - const rightMaterial = new THREE.LineBasicMaterial({ color: 0x9a8058, transparent: true, opacity: 0.72 }); - const z = depth * 0.5 + 0.006; - lines.forEach((line) => { - const points = line.points.map((point) => new THREE.Vector3(point.x, point.y, z)); - book.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), line.side < 0 ? leftMaterial : rightMaterial)); - }); -} - function addSimulatedStackBodies(lines, depth) { [-1, 1].forEach((side) => { const sideLines = lines.filter((line) => line.side === side); if (!sideLines.length) return; - const material = side < 0 ? materials.pagesLeft : materials.pagesRight; const bodyLines = sideLines.length === 1 ? createSinglePageBodyLines(sideLines[0]) : sideLines; - book.add(new THREE.Mesh(createLoftedLineBody(bodyLines, depth), material)); - book.add(new THREE.Line(createEndpointPolyline(bodyLines, depth), new THREE.LineBasicMaterial({ color: 0xb99a68, transparent: true, opacity: 0.62 }))); + const stackMaterials = createStackBodyMaterials(bodyLines, side); + book.add(new THREE.Mesh(createLoftedLineBody(bodyLines, depth), stackMaterials)); }); } @@ -757,17 +745,78 @@ function createSinglePageBodyLines(line) { ]; } +function createStackBodyMaterials(lines, side) { + const baseColor = side < 0 ? '#d8c7a4' : '#e7d6b4'; + const lineColor = '#9a8058'; + const layerTexture = createStackLayerTexture(lines.length, baseColor, lineColor); + return [ + new THREE.MeshBasicMaterial({ map: layerTexture, side: THREE.DoubleSide }), + new THREE.MeshBasicMaterial({ map: layerTexture, side: THREE.DoubleSide }), + new THREE.MeshBasicMaterial({ color: baseColor, side: THREE.DoubleSide }) + ]; +} + +function createStackLayerTexture(lineCount, baseColor, lineColor) { + const canvas = document.createElement('canvas'); + canvas.width = 2048; + canvas.height = 1024; + const context = canvas.getContext('2d'); + context.fillStyle = baseColor; + context.fillRect(0, 0, canvas.width, canvas.height); + context.strokeStyle = lineColor; + context.globalAlpha = 0.95; + context.lineWidth = 4.2; + context.lineCap = 'square'; + for (let row = 0; row < lineCount; row += 1) { + const v = lineCount <= 1 ? 0.5 : row / (lineCount - 1); + const y = (1 - v) * canvas.height; + context.beginPath(); + context.moveTo(-8, y); + context.lineTo(canvas.width + 8, y); + context.stroke(); + } + return createCanvasTexture(canvas); +} + +function createCanvasTexture(canvas) { + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + texture.anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy()); + texture.needsUpdate = true; + return texture; +} + function createLoftedLineBody(lines, depth) { const positions = []; + const uvs = []; const indices = []; const smoothLines = lines.map((line) => line.points); - const push = (point, z) => { + const push = (point, z, uv) => { const index = positions.length / 3; positions.push(point.x, point.y, z); + uvs.push(uv.u, uv.v); return index; }; - const front = smoothLines.map((points) => points.map((point) => push(point, depth * 0.5))); - const back = smoothLines.map((points) => points.map((point) => push(point, -depth * 0.5))); + const rowUv = (row) => ( + smoothLines.length <= 1 ? 0.5 : row / (smoothLines.length - 1) + ); + const colUv = (points, col) => ( + points.length <= 1 ? 0.5 : col / (points.length - 1) + ); + const lineUv = (row, col) => ({ + u: colUv(smoothLines[row], col), + v: rowUv(row) + }); + const backLineUv = (row, col) => ({ + u: colUv(smoothLines[row], col), + v: rowUv(row) + }); + const sideUv = (row, z) => ({ + u: (z + depth * 0.5) / depth, + v: rowUv(row) + }); + const front = smoothLines.map((points, row) => points.map((point, col) => push(point, depth * 0.5, lineUv(row, col)))); + const back = smoothLines.map((points, row) => points.map((point, col) => push(point, -depth * 0.5, backLineUv(row, col)))); for (let row = 0; row < smoothLines.length - 1; row += 1) { for (let col = 0; col < smoothLines[row].length - 1; col += 1) { indices.push(front[row][col], front[row + 1][col], front[row][col + 1]); @@ -776,11 +825,19 @@ function createLoftedLineBody(lines, depth) { indices.push(back[row][col + 1], back[row + 1][col + 1], back[row + 1][col]); } } + const sideStart = indices.length; for (let row = 0; row < smoothLines.length - 1; row += 1) { const last = smoothLines[row].length - 1; - indices.push(front[row][last], front[row + 1][last], back[row][last]); - indices.push(front[row + 1][last], back[row + 1][last], back[row][last]); + const a = smoothLines[row][last]; + const b = smoothLines[row + 1][last]; + const frontA = push(a, depth * 0.5, sideUv(row, depth * 0.5)); + const frontB = push(b, depth * 0.5, sideUv(row + 1, depth * 0.5)); + const backA = push(a, -depth * 0.5, sideUv(row, -depth * 0.5)); + const backB = push(b, -depth * 0.5, sideUv(row + 1, -depth * 0.5)); + indices.push(frontA, frontB, backA); + indices.push(frontB, backB, backA); } + const topStart = indices.length; for (let col = 0; col < smoothLines[0].length - 1; col += 1) { const topRow = smoothLines.length - 1; indices.push(front[topRow][col], back[topRow][col], front[topRow][col + 1]); @@ -789,15 +846,15 @@ function createLoftedLineBody(lines, depth) { 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.clearGroups(); + geometry.addGroup(0, sideStart, 0); + geometry.addGroup(sideStart, topStart - sideStart, 1); + geometry.addGroup(topStart, indices.length - topStart, 2); geometry.computeVertexNormals(); return geometry; } -function createEndpointPolyline(lines, depth) { - const points = lines.map((line) => new THREE.Vector3(line.endpoint.x, line.endpoint.y, depth * 0.5 + 0.008)); - return new THREE.BufferGeometry().setFromPoints(points); -} - function startPageFlip(direction) { if (activeFlips.length || !lastBookModel || !canPageFlip(direction)) return false; const flip = createPageFlip(direction, performance.now(), NORMAL_FLIP_DURATION); diff --git a/public/webgl-book-shape-lab.html b/public/webgl-book-shape-lab.html index 1587b21..1361a26 100644 --- a/public/webgl-book-shape-lab.html +++ b/public/webgl-book-shape-lab.html @@ -74,6 +74,6 @@ 0 / 10 - +