From a95ac9db50047672b4348e0398cf7d2590415e9d Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Fri, 5 Jun 2026 00:31:48 +0200 Subject: [PATCH] Checkpoint reconstructed book shape solver --- public/js/webgl-book-shape-lab.js | 550 +++++++++++++++++++++++------- public/webgl-book-shape-lab.html | 6 +- 2 files changed, 436 insertions(+), 120 deletions(-) diff --git a/public/js/webgl-book-shape-lab.js b/public/js/webgl-book-shape-lab.js index 42a7f29..f32780f 100644 --- a/public/js/webgl-book-shape-lab.js +++ b/public/js/webgl-book-shape-lab.js @@ -4,6 +4,8 @@ import { OrbitControls } from 'https://esm.sh/three@0.165.0/examples/jsm/control const canvas = document.getElementById('scene'); const progressInput = document.getElementById('progress'); const progressValue = document.getElementById('progress_value'); +const pageCountInput = document.getElementById('page_count'); +const pageCountValue = document.getElementById('page_count_value'); const urlParams = new URLSearchParams(window.location.search); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); @@ -48,12 +50,18 @@ const BOOK_PROFILE = { tableY: 0, coverThickness: 0.03, raisedHingeY: 0.056, - paperContactOffset: 0.0012 + paperContactOffset: 0.0012, + bundleSpacing: 0.014 }; let readingProgress = readInitialProgress(); +let pageCount = readInitialPageCount(); +let lastLengthError = 0; +let lastSpacingError = 0; progressInput.value = readingProgress.toFixed(3); progressValue.value = readingProgress.toFixed(2); +pageCountInput.value = String(pageCount); +pageCountValue.value = String(pageCount); rebuildBook(); resize(); animate(); @@ -62,15 +70,32 @@ progressInput.addEventListener('input', () => { setReadingProgress(progressInput.value); }); +pageCountInput.addEventListener('input', () => { + setPageCount(pageCountInput.value); +}); + window.addEventListener('resize', resize); window.BookShapeLab = { get progress() { return readingProgress; }, + get pageCount() { + return pageCount; + }, + get lastLengthError() { + return lastLengthError; + }, + get lastSpacingError() { + return lastSpacingError; + }, setReadingProgress(value) { setReadingProgress(value); return readingProgress; + }, + setPageCount(value) { + setPageCount(value); + return pageCount; } }; @@ -79,6 +104,16 @@ function readInitialProgress() { return Number.isFinite(parsed) ? THREE.MathUtils.clamp(parsed, 0, 1) : 0.25; } +function readInitialPageCount() { + const parsed = Number.parseInt(urlParams.get('pages') ?? '240', 10); + if (!Number.isFinite(parsed)) return 240; + return snapPageCount(parsed); +} + +function snapPageCount(value) { + return THREE.MathUtils.clamp(Math.round(value / 10) * 10, 40, 600); +} + function setReadingProgress(value) { const next = THREE.MathUtils.clamp(Number.parseFloat(value), 0, 1); if (!Number.isFinite(next)) return; @@ -88,6 +123,15 @@ function setReadingProgress(value) { rebuildBook(); } +function setPageCount(value) { + const next = snapPageCount(Number.parseFloat(value)); + if (!Number.isFinite(next)) return; + pageCount = next; + pageCountInput.value = String(pageCount); + pageCountValue.value = String(pageCount); + rebuildBook(); +} + function rebuildBook() { clearGroup(book); @@ -95,20 +139,16 @@ function rebuildBook() { const coverThickness = BOOK_PROFILE.coverThickness; const pageWidth = 1.62; const pageDepth = 2.24; - const gutter = 0.12; - const sheetTick = 0.0045; - const fullBlock = 0.41; - const leftThickness = THREE.MathUtils.lerp(sheetTick, fullBlock, readingProgress); - const rightThickness = THREE.MathUtils.lerp(fullBlock, sheetTick, readingProgress); - const spineWidth = fullBlock; - const fold = spineCurvePoint(readingProgress, spineWidth); + const bundleCount = Math.max(4, Math.round(pageCount / 10)); + const spineWidth = Math.max(0.16, bundleCount * BOOK_PROFILE.bundleSpacing); + const lines = simulatePageLines(bundleCount, pageWidth, spineWidth); + lastLengthError = measureLineLengthError(lines, pageWidth); + lastSpacingError = measureStackSpacingError(lines); addCoverAssembly(pageWidth, coverDepth, coverThickness, spineWidth); - 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); + addSimulatedStackBodies(lines, pageDepth); + addSimulatedPageLines(lines, pageDepth); } function clearGroup(group) { @@ -185,44 +225,41 @@ function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth) { return geometry; } -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, 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 = 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 addClothSpine(depth, spineWidth) { - const material = new THREE.LineBasicMaterial({ color: 0xb51f1f }); + const spine = new THREE.Mesh(createClothSpineGeometry(depth, spineWidth), materials.spine); + book.add(spine); +} + +function createClothSpineGeometry(depth, spineWidth) { const profile = []; for (let i = 0; i <= 32; i += 1) { const u = i / 32; profile.push(spineCurvePoint(u, spineWidth)); } + const positions = []; + const indices = []; + const front = []; + const back = []; + const push = (point, z) => { + const index = positions.length / 3; + positions.push(point.x, point.y, z); + return index; + }; - [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)); + profile.forEach((point) => { + front.push(push(point, depth * 0.5 + 0.012)); + back.push(push(point, -depth * 0.5 - 0.012)); }); + for (let i = 0; i < profile.length - 1; i += 1) { + indices.push(front[i], back[i], front[i + 1]); + indices.push(front[i + 1], back[i], back[i + 1]); + } + + const geometry = new THREE.BufferGeometry(); + geometry.setIndex(indices); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geometry.computeVertexNormals(); + return geometry; } function spineCurvePoint(t, spineWidth) { @@ -237,110 +274,385 @@ function spineCurvePoint(t, spineWidth) { }; } -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 = 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(side, thickness, u, v, fold, spineWidth) { - const curveEnd = 0.34; - if (u <= curveEnd) { - return pageCurvePoint(side, fold, spineWidth, u, curveEnd).y; +function simulatePageLines(bundleCount, pageWidth, spineWidth) { + const lines = []; + const segments = 16; + const stepLength = pageWidth / segments; + const entries = []; + const spineSamples = sampleSpineByArc(bundleCount, spineWidth); + const leftLimit = Math.floor((bundleCount - 1) * readingProgress); + for (let index = 0; index < bundleCount; index += 1) { + const t = spineSamples[index].t; + const side = index <= leftLimit ? -1 : 1; + entries.push({ index, t, side }); } - const flatY = BOOK_PROFILE.coverThickness + BOOK_PROFILE.paperContactOffset; - return flatY; + [-1, 1].forEach((side) => { + const sideEntries = entries.filter((entry) => entry.side === side); + sideEntries.forEach((entry, rank) => { + entry.rank = rank; + entry.sideCount = sideEntries.length; + }); + }); + [-1, 1].forEach((side) => { + const sideEntries = entries + .filter((entry) => entry.side === side) + .sort((a, b) => side < 0 ? a.t - b.t : b.t - a.t); + if (!sideEntries.length) return; + + const bottomEntry = sideEntries[0]; + const bottomAnchor = spineCurvePoint(bottomEntry.t, spineWidth); + const bottomTarget = restingTarget(side, pageWidth, 0, sideEntries.length); + const bottomPoints = initialPageLine(bottomAnchor, bottomTarget, segments); + relaxPageLine(bottomPoints, bottomAnchor, stepLength, side, 0, bundleCount); + keepPageAboveCover(bottomPoints, side, bundleCount); + + sideEntries.forEach((entry, rank) => { + const anchor = spineCurvePoint(entry.t, spineWidth); + const points = rank === 0 + ? bottomPoints.map((point) => ({ ...point })) + : offsetPageLine(bottomPoints, anchor, rank * BOOK_PROFILE.bundleSpacing); + keepPageAboveCover(points, side, bundleCount); + lines.push({ index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1] }); + }); + }); + enforceStackConstraints(lines, stepLength, bundleCount); + return lines; } -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 measureLineLengthError(lines, pageWidth) { + return lines.reduce((maxError, line) => { + let length = 0; + for (let i = 0; i < line.points.length - 1; i += 1) { + length += Math.hypot(line.points[i + 1].x - line.points[i].x, line.points[i + 1].y - line.points[i].y); + } + return Math.max(maxError, Math.abs(length - pageWidth)); + }, 0); } -function smoothstep(value) { - return value * value * (3 - 2 * value); +function measureStackSpacingError(lines) { + let maxError = 0; + [-1, 1].forEach((side) => { + const sideLines = lines + .filter((line) => line.side === side) + .sort((a, b) => side < 0 ? a.t - b.t : b.t - a.t); + for (let row = 1; row < sideLines.length; row += 1) { + const lower = sideLines[row - 1]; + const upper = sideLines[row]; + for (let col = 0; col < upper.points.length; col += 1) { + const distance = Math.hypot(upper.points[col].x - lower.points[col].x, upper.points[col].y - lower.points[col].y); + maxError = Math.max(maxError, Math.abs(distance - BOOK_PROFILE.bundleSpacing)); + } + } + }); + return maxError; } -function easeOutCubic(value) { - return 1 - Math.pow(1 - value, 3); -} - -function pageWidthAtDepth(width, v) { - return width; -} - -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; +function sampleSpineByArc(count, spineWidth) { + const samples = []; + const steps = 240; + let length = 0; + let previous = spineCurvePoint(0, spineWidth); + samples.push({ point: previous, length }); + for (let i = 1; i <= steps; i += 1) { + const t = i / steps; + const point = spineCurvePoint(t, spineWidth); + length += Math.hypot(point.x - previous.x, point.y - previous.y); + samples.push({ point, length }); + previous = point; } - 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 curveEndX * (1 - outerT) + foreX * outerT; + const points = []; + for (let i = 0; i < count; i += 1) { + const target = count === 1 ? length * 0.5 : length * (i / (count - 1)); + const found = samples.findIndex((sample) => sample.length >= target); + if (found <= 0) { + points.push(samples[0].point); + continue; + } + const before = samples[found - 1]; + const after = samples[found]; + const span = after.length - before.length || 1; + const t = THREE.MathUtils.lerp(before.point.t, after.point.t, (target - before.length) / span); + points.push(spineCurvePoint(t, spineWidth)); + } + return points; } -function createSplinePageBlockGeometry(side, width, depth, thickness, fold, spineWidth) { - const columns = 36; - const rows = 36; +function initialPageLine(anchor, target, segments) { + const points = []; + for (let i = 0; i <= segments; i += 1) { + const u = i / segments; + const sag = 0.04 * Math.sin(Math.PI * u); + points.push({ + x: THREE.MathUtils.lerp(anchor.x, target.x, u), + y: THREE.MathUtils.lerp(anchor.y, target.y, u) - sag * u + }); + } + return points; +} + +function restingTarget(side, pageWidth, rank, sideCount) { + const local = sideCount <= 1 ? 0 : rank / (sideCount - 1); + const foreCurve = 0.11 * Math.sin(Math.PI * local); + const x = side * (pageWidth - foreCurve); + const y = BOOK_PROFILE.coverThickness + BOOK_PROFILE.paperContactOffset + rank * BOOK_PROFILE.bundleSpacing + 0.002 * Math.sin(Math.PI * local); + return { x, y }; +} + +function relaxPageLine(points, anchor, stepLength, side, local, bundleCount) { + const gravity = 0.00072; + const stackPressure = 0.0011 * (1 - local); + const bendStrength = 0.52; + const iterations = 72; + for (let iteration = 0; iteration < iterations; iteration += 1) { + points[0].x = anchor.x; + points[0].y = anchor.y; + for (let i = 1; i < points.length; i += 1) { + const u = i / (points.length - 1); + points[i].y -= gravity * u + stackPressure * u * u; + } + applyBendingResistance(points, bendStrength); + for (let pass = 0; pass < 3; pass += 1) { + points[0].x = anchor.x; + points[0].y = anchor.y; + enforceLineLength(points, anchor, stepLength, 3); + keepPageAboveCover(points, side, bundleCount); + } + } +} + +function applyBendingResistance(points, strength) { + const updates = points.map((point) => ({ x: point.x, y: point.y })); + for (let i = 1; i < points.length - 1; i += 1) { + const previous = points[i - 1]; + const current = points[i]; + const next = points[i + 1]; + updates[i].x += (previous.x + next.x - current.x * 2) * strength; + updates[i].y += (previous.y + next.y - current.y * 2) * strength; + } + for (let i = 1; i < points.length - 1; i += 1) { + points[i].x = updates[i].x; + points[i].y = updates[i].y; + } +} + +function enforceLineLength(points, anchor, stepLength, passes) { + for (let pass = 0; pass < passes; pass += 1) { + points[0].x = anchor.x; + points[0].y = anchor.y; + for (let i = 0; i < points.length - 1; i += 1) { + constrainSegment(points[i], points[i + 1], stepLength, i === 0); + } + for (let i = points.length - 2; i >= 0; i -= 1) { + constrainSegment(points[i], points[i + 1], stepLength, i === 0); + } + } +} + +function constrainSegment(a, b, length, anchorA) { + const dx = b.x - a.x; + const dy = b.y - a.y; + const distance = Math.hypot(dx, dy) || 0.0001; + const correction = (distance - length) / distance; + if (anchorA) { + b.x -= dx * correction; + b.y -= dy * correction; + return; + } + a.x += dx * correction * 0.5; + a.y += dy * correction * 0.5; + b.x -= dx * correction * 0.5; + b.y -= dy * correction * 0.5; +} + +function keepPageAboveCover(points, side, bundleCount) { + for (let i = 1; i < points.length; i += 1) { + const clearance = BOOK_PROFILE.paperContactOffset + 0.0002 * bundleCount; + points[i].y = Math.max(points[i].y, coverTopYAtX(points[i].x) + clearance); + points[i].x = side < 0 ? Math.min(points[i].x, -0.01) : Math.max(points[i].x, 0.01); + } +} + +function enforceStackConstraints(lines, stepLength, bundleCount) { + const iterations = 44; + [-1, 1].forEach((side) => { + const sideLines = lines + .filter((line) => line.side === side) + .sort((a, b) => side < 0 ? a.t - b.t : b.t - a.t); + for (let iteration = 0; iteration < iterations; iteration += 1) { + sideLines.forEach((line) => { + line.points[0].x = line.anchor.x; + line.points[0].y = line.anchor.y; + applyBendingResistance(line.points, 0.22); + enforceLineLength(line.points, line.anchor, stepLength, 3); + keepPageAboveCover(line.points, side, bundleCount); + }); + + for (let row = 1; row < sideLines.length; row += 1) { + const lower = sideLines[row - 1]; + const upper = sideLines[row]; + for (let col = 1; col < upper.points.length; col += 1) { + const normal = upwardNormalAt(lower.points, col); + const targetX = lower.points[col].x + normal.x * BOOK_PROFILE.bundleSpacing; + const targetY = lower.points[col].y + normal.y * BOOK_PROFILE.bundleSpacing; + upper.points[col].x = THREE.MathUtils.lerp(upper.points[col].x, targetX, 0.28); + upper.points[col].y = Math.max(upper.points[col].y, THREE.MathUtils.lerp(upper.points[col].y, targetY, 0.42)); + } + upper.points[0].x = upper.anchor.x; + upper.points[0].y = upper.anchor.y; + applyBendingResistance(upper.points, 0.2); + enforceLineLength(upper.points, upper.anchor, stepLength, 3); + keepPageAboveCover(upper.points, side, bundleCount); + } + } + sideLines.forEach((line) => { + applyBendingResistance(line.points, 0.32); + enforceLineLength(line.points, line.anchor, stepLength, 10); + keepPageAboveCover(line.points, side, bundleCount); + enforceLineLength(line.points, line.anchor, stepLength, 6); + }); + sideLines.forEach((line) => { + line.endpoint = line.points[line.points.length - 1]; + }); + }); +} + +function offsetPageLine(basePoints, anchor, distance) { + return basePoints.map((point, index) => { + if (index === 0) return { x: anchor.x, y: anchor.y }; + const normal = upwardNormalAt(basePoints, index); + return { + x: point.x + normal.x * distance, + y: point.y + normal.y * distance + }; + }); +} + +function upwardNormalAt(points, index) { + const previous = points[Math.max(0, index - 1)]; + const next = points[Math.min(points.length - 1, index + 1)]; + const dx = next.x - previous.x; + const dy = next.y - previous.y; + const length = Math.hypot(dx, dy) || 0.0001; + let nx = -dy / length; + let ny = dx / length; + if (ny < 0) { + nx = -nx; + ny = -ny; + } + return { x: nx, y: ny }; +} + +function coverTopYAtX(x) { + const ax = Math.abs(x); + const spineHalf = currentSpineHalf(); + const hingeX = spineHalf + 0.07; + const outerX = 1.62 + 0.055; + if (ax <= spineHalf) return BOOK_PROFILE.coverThickness; + if (ax <= hingeX) { + const t = (ax - spineHalf) / (hingeX - spineHalf); + return THREE.MathUtils.lerp(BOOK_PROFILE.coverThickness, BOOK_PROFILE.raisedHingeY, t); + } + const t = THREE.MathUtils.clamp((ax - hingeX) / (outerX - hingeX), 0, 1); + return THREE.MathUtils.lerp(BOOK_PROFILE.raisedHingeY, BOOK_PROFILE.coverThickness, t); +} + +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.11; + lines.forEach((line) => { + const points = smoothLinePoints(line.points, 4).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 < 2) return; + const material = side < 0 ? materials.pagesLeft : materials.pagesRight; + book.add(new THREE.Mesh(createLoftedLineBody(sideLines, depth), material)); + book.add(new THREE.Line(createEndpointPolyline(sideLines, depth), new THREE.LineBasicMaterial({ color: 0xb99a68, transparent: true, opacity: 0.62 }))); + }); +} + +function createLoftedLineBody(lines, depth) { const positions = []; - const uvs = []; const indices = []; - const top = []; - const bottom = []; - - const push = (x, y, z, u, v) => { + const smoothLines = lines.map((line) => smoothLinePoints(line.points, 4)); + const push = (point, z) => { const index = positions.length / 3; - positions.push(x, y, z); - uvs.push(u, v); + positions.push(point.x, point.y, z); return index; }; - - for (let row = 0; row <= rows; row += 1) { - const v = row / rows; - top[row] = []; - bottom[row] = []; - for (let column = 0; column <= columns; column += 1) { - const u = column / columns; - const z = (v - 0.5) * depth; - 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); + const front = smoothLines.map((points) => points.map((point) => push(point, depth * 0.5 + 0.09))); + const back = smoothLines.map((points) => points.map((point) => push(point, -depth * 0.5 + 0.09))); + 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]); + indices.push(front[row][col + 1], front[row + 1][col], front[row + 1][col + 1]); + indices.push(back[row][col], back[row][col + 1], back[row + 1][col]); + indices.push(back[row][col + 1], back[row + 1][col + 1], back[row + 1][col]); } } - - for (let row = 0; row < rows; row += 1) { - for (let column = 0; column < columns; column += 1) { - indices.push(top[row][column], top[row + 1][column], top[row][column + 1]); - indices.push(top[row][column + 1], top[row + 1][column], top[row + 1][column + 1]); - indices.push(bottom[row][column], bottom[row][column + 1], bottom[row + 1][column]); - indices.push(bottom[row][column + 1], bottom[row + 1][column + 1], bottom[row + 1][column]); - } + 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]); } - - for (let row = 0; row < rows; row += 1) { - indices.push(top[row][0], bottom[row][0], top[row + 1][0], top[row + 1][0], bottom[row][0], bottom[row + 1][0]); - indices.push(top[row][columns], top[row + 1][columns], bottom[row][columns], top[row + 1][columns], bottom[row + 1][columns], bottom[row][columns]); + for (let col = 0; col < smoothLines[0].length - 1; col += 1) { + const bottomRow = 0; + const topRow = smoothLines.length - 1; + indices.push(front[bottomRow][col], front[bottomRow][col + 1], back[bottomRow][col]); + indices.push(front[bottomRow][col + 1], back[bottomRow][col + 1], back[bottomRow][col]); + indices.push(front[topRow][col], back[topRow][col], front[topRow][col + 1]); + indices.push(front[topRow][col + 1], back[topRow][col], back[topRow][col + 1]); } - - for (let column = 0; column < columns; column += 1) { - indices.push(top[0][column], top[0][column + 1], bottom[0][column], top[0][column + 1], bottom[0][column + 1], bottom[0][column]); - indices.push(top[rows][column], bottom[rows][column], top[rows][column + 1], top[rows][column + 1], bottom[rows][column], bottom[rows][column + 1]); + for (let row = 0; row < smoothLines.length - 1; row += 1) { + indices.push(front[row][0], back[row][0], front[row + 1][0]); + indices.push(front[row + 1][0], back[row][0], back[row + 1][0]); } - 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; } +function createEndpointPolyline(lines, depth) { + const points = lines.map((line) => new THREE.Vector3(line.endpoint.x, line.endpoint.y, depth * 0.5 + 0.112)); + return new THREE.BufferGeometry().setFromPoints(points); +} + +function smoothLinePoints(points, subdivisions) { + const result = []; + for (let i = 0; i < points.length - 1; i += 1) { + const p0 = points[Math.max(0, i - 1)]; + const p1 = points[i]; + const p2 = points[i + 1]; + const p3 = points[Math.min(points.length - 1, i + 2)]; + for (let step = 0; step < subdivisions; step += 1) { + const t = step / subdivisions; + result.push(catmullRomPoint(p0, p1, p2, p3, t)); + } + } + result.push(points[points.length - 1]); + return result; +} + +function catmullRomPoint(p0, p1, p2, p3, t) { + const tt = t * t; + const ttt = tt * t; + return { + x: 0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * tt + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * ttt), + y: 0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * tt + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * ttt) + }; +} + function resize() { const width = window.innerWidth; const height = window.innerHeight; diff --git a/public/webgl-book-shape-lab.html b/public/webgl-book-shape-lab.html index 093ce40..5a1ce00 100644 --- a/public/webgl-book-shape-lab.html +++ b/public/webgl-book-shape-lab.html @@ -38,7 +38,8 @@ border-radius: 6px; } - #progress { + #progress, + #page_count { width: 100%; } @@ -49,6 +50,9 @@ 0.25 + + + 240