Checkpoint book shape task 2
This commit is contained in:
@@ -17,6 +17,8 @@ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
renderer.setClearColor(0x202124, 1);
|
renderer.setClearColor(0x202124, 1);
|
||||||
|
|
||||||
|
const GRID_Y = -0.12;
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 30);
|
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 30);
|
||||||
if (urlParams.get('view') === 'profile') {
|
if (urlParams.get('view') === 'profile') {
|
||||||
@@ -38,7 +40,7 @@ const book = new THREE.Group();
|
|||||||
scene.add(book);
|
scene.add(book);
|
||||||
|
|
||||||
const guide = new THREE.GridHelper(5.6, 16, 0x4c4c4c, 0x343434);
|
const guide = new THREE.GridHelper(5.6, 16, 0x4c4c4c, 0x343434);
|
||||||
guide.position.y = -0.12;
|
guide.position.y = GRID_Y;
|
||||||
scene.add(guide);
|
scene.add(guide);
|
||||||
|
|
||||||
const materials = {
|
const materials = {
|
||||||
@@ -60,6 +62,8 @@ const BOOK_PROFILE = {
|
|||||||
singlePageCoverGap: 0.006,
|
singlePageCoverGap: 0.006,
|
||||||
bundleSpacing: 0.014
|
bundleSpacing: 0.014
|
||||||
};
|
};
|
||||||
|
|
||||||
|
book.position.y = GRID_Y - BOOK_PROFILE.tableY;
|
||||||
const NORMAL_FLIP_DURATION = 1800;
|
const NORMAL_FLIP_DURATION = 1800;
|
||||||
const FAST_FLIP_DURATION = 900;
|
const FAST_FLIP_DURATION = 900;
|
||||||
const FAST_FLIP_COUNT = 10;
|
const FAST_FLIP_COUNT = 10;
|
||||||
@@ -74,7 +78,6 @@ const PAGE_DEPTH = 2.24;
|
|||||||
const COVER_DEPTH = 2.30;
|
const COVER_DEPTH = 2.30;
|
||||||
const COVER_OVERHANG = (COVER_DEPTH - PAGE_DEPTH) * 0.5;
|
const COVER_OVERHANG = (COVER_DEPTH - PAGE_DEPTH) * 0.5;
|
||||||
const COVER_SUPPORT_OVERHANG = COVER_OVERHANG;
|
const COVER_SUPPORT_OVERHANG = COVER_OVERHANG;
|
||||||
const HINGE_INSET = 0.07;
|
|
||||||
const SUPPORT_ANGLE_STEPS = 720;
|
const SUPPORT_ANGLE_STEPS = 720;
|
||||||
const SUPPORT_ANGLE_CANDIDATES = Array.from({ length: SUPPORT_ANGLE_STEPS }, (_, sample) => {
|
const SUPPORT_ANGLE_CANDIDATES = Array.from({ length: SUPPORT_ANGLE_STEPS }, (_, sample) => {
|
||||||
const angle = sample / SUPPORT_ANGLE_STEPS * Math.PI * 2;
|
const angle = sample / SUPPORT_ANGLE_STEPS * Math.PI * 2;
|
||||||
@@ -93,6 +96,7 @@ let lastLengthError = 0;
|
|||||||
let lastSpacingError = 0;
|
let lastSpacingError = 0;
|
||||||
let lastBookModel = null;
|
let lastBookModel = null;
|
||||||
let activeSpineHalf = 0.08;
|
let activeSpineHalf = 0.08;
|
||||||
|
let activeCoverOuterX = activeSpineHalf + PAGE_WIDTH + COVER_OVERHANG;
|
||||||
let activeFlips = [];
|
let activeFlips = [];
|
||||||
let pendingPageFlips = 0;
|
let pendingPageFlips = 0;
|
||||||
pageCountInput.max = String(maximumPageCount);
|
pageCountInput.max = String(maximumPageCount);
|
||||||
@@ -144,6 +148,25 @@ window.BookShapeLab = {
|
|||||||
get lastSpacingError() {
|
get lastSpacingError() {
|
||||||
return 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) {
|
||||||
setReadingProgress(value);
|
setReadingProgress(value);
|
||||||
return readingProgress;
|
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() {
|
function readInitialProgress() {
|
||||||
const parsed = Number.parseFloat(urlParams.get('progress') ?? '0.25');
|
const parsed = Number.parseFloat(urlParams.get('progress') ?? '0.25');
|
||||||
return Number.isFinite(parsed) ? THREE.MathUtils.clamp(parsed, 0, 1) : 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 spineWidth = calculateSpineWidth(bundleCount);
|
||||||
const leftCount = calculateLeftBundleCount(bundleCount);
|
const leftCount = calculateLeftBundleCount(bundleCount);
|
||||||
const spineHalf = spineArcHalf(spineWidth);
|
const spineHalf = spineArcHalf(spineWidth);
|
||||||
const hingeX = spineHalf + HINGE_INSET;
|
const hingeX = spineHalf + hingeInset();
|
||||||
const foreEdgeX = spineHalf + pageWidth;
|
const foreEdgeX = spineHalf + pageWidth;
|
||||||
const bundleSpacing = calculateBundleSpacing(bundleCount, spineWidth, leftCount);
|
const bundleSpacing = calculateBundleSpacing(bundleCount, spineWidth, leftCount);
|
||||||
activeSpineHalf = spineHalf;
|
activeSpineHalf = spineHalf;
|
||||||
|
activeCoverOuterX = spineHalf + pageWidth + COVER_OVERHANG;
|
||||||
const lines = simulatePageLines(bundleCount, pageWidth, pageSplineLength, spineWidth, foreEdgeX, bundleSpacing, leftCount);
|
const lines = simulatePageLines(bundleCount, pageWidth, pageSplineLength, spineWidth, foreEdgeX, bundleSpacing, leftCount);
|
||||||
|
const coverOuterX = solvedStackOuterX(lines) + COVER_OVERHANG;
|
||||||
|
activeCoverOuterX = coverOuterX;
|
||||||
lastLengthError = measureLineLengthError(lines, pageSplineLength);
|
lastLengthError = measureLineLengthError(lines, pageSplineLength);
|
||||||
lastSpacingError = measureStackSpacingError(lines, bundleSpacing);
|
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);
|
addClothSpine(pageDepth, spineWidth);
|
||||||
addSimulatedStackBodies(lines, pageDepth);
|
addSimulatedStackBodies(lines, pageDepth);
|
||||||
updateFlipControls();
|
updateFlipControls();
|
||||||
@@ -241,15 +290,15 @@ function clearGroup(group) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCoverAssembly(pageWidth, depth, thickness, spineWidth) {
|
function addCoverAssembly(pageWidth, depth, thickness, spineWidth, coverOuterX) {
|
||||||
const cover = new THREE.Mesh(createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth), materials.cover);
|
const cover = new THREE.Mesh(createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth, coverOuterX), materials.cover);
|
||||||
book.add(cover);
|
book.add(cover);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth) {
|
function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth, coverOuterX) {
|
||||||
const spineHalf = spineArcHalf(spineWidth);
|
const spineHalf = spineArcHalf(spineWidth);
|
||||||
const hingeX = spineHalf + HINGE_INSET;
|
const hingeX = spineHalf + hingeInset();
|
||||||
const outerX = spineHalf + pageWidth + COVER_OVERHANG;
|
const outerX = coverOuterX ?? spineHalf + pageWidth + COVER_OVERHANG;
|
||||||
const outerTopY = BOOK_PROFILE.tableY + thickness;
|
const outerTopY = BOOK_PROFILE.tableY + thickness;
|
||||||
const connectionTopY = BOOK_PROFILE.raisedHingeY;
|
const connectionTopY = BOOK_PROFILE.raisedHingeY;
|
||||||
const spineTopY = BOOK_PROFILE.tableY + thickness;
|
const spineTopY = BOOK_PROFILE.tableY + thickness;
|
||||||
@@ -356,6 +405,10 @@ function spineArcHalf(spineWidth) {
|
|||||||
return spineWidth * 0.42;
|
return spineWidth * 0.42;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hingeInset() {
|
||||||
|
return Math.max(0.001, BOOK_PROFILE.raisedHingeY - BOOK_PROFILE.coverThickness);
|
||||||
|
}
|
||||||
|
|
||||||
function calculateSpineWidth(bundleCount) {
|
function calculateSpineWidth(bundleCount) {
|
||||||
const minimumWidth = 0.16;
|
const minimumWidth = 0.16;
|
||||||
if (bundleCount <= 1) return minimumWidth;
|
if (bundleCount <= 1) return minimumWidth;
|
||||||
@@ -421,6 +474,12 @@ function calculateLeftBundleCount(bundleCount) {
|
|||||||
return THREE.MathUtils.clamp(Math.round(bundleCount * readingProgress), 0, 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) {
|
function simulatePageLines(bundleCount, pageWidth, pageSplineLength, spineWidth, foreEdgeX, bundleSpacing, leftCount) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const segments = PAGE_LINE_SEGMENTS;
|
const segments = PAGE_LINE_SEGMENTS;
|
||||||
@@ -836,8 +895,8 @@ function upwardNormalAt(points, index) {
|
|||||||
function coverTopYAtX(x) {
|
function coverTopYAtX(x) {
|
||||||
const ax = Math.abs(x);
|
const ax = Math.abs(x);
|
||||||
const spineHalf = currentSpineHalf();
|
const spineHalf = currentSpineHalf();
|
||||||
const hingeX = spineHalf + HINGE_INSET;
|
const hingeX = spineHalf + hingeInset();
|
||||||
const outerX = spineHalf + PAGE_WIDTH + COVER_SUPPORT_OVERHANG;
|
const outerX = activeCoverOuterX;
|
||||||
if (ax <= spineHalf) return BOOK_PROFILE.coverThickness;
|
if (ax <= spineHalf) return BOOK_PROFILE.coverThickness;
|
||||||
if (ax <= hingeX) {
|
if (ax <= hingeX) {
|
||||||
const t = (ax - spineHalf) / (hingeX - spineHalf);
|
const t = (ax - spineHalf) / (hingeX - spineHalf);
|
||||||
@@ -969,6 +1028,11 @@ function createLoftedLineBody(lines, depth) {
|
|||||||
indices.push(frontA, frontB, backA);
|
indices.push(frontA, frontB, backA);
|
||||||
indices.push(frontB, backB, 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;
|
const topStart = indices.length;
|
||||||
for (let col = 0; col < smoothLines[0].length - 1; col += 1) {
|
for (let col = 0; col < smoothLines[0].length - 1; col += 1) {
|
||||||
const topRow = smoothLines.length - 1;
|
const topRow = smoothLines.length - 1;
|
||||||
@@ -981,7 +1045,8 @@ function createLoftedLineBody(lines, depth) {
|
|||||||
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
||||||
geometry.clearGroups();
|
geometry.clearGroups();
|
||||||
geometry.addGroup(0, sideStart, 0);
|
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.addGroup(topStart, indices.length - topStart, 2);
|
||||||
geometry.computeVertexNormals();
|
geometry.computeVertexNormals();
|
||||||
return geometry;
|
return geometry;
|
||||||
|
|||||||
@@ -74,6 +74,6 @@
|
|||||||
<button id="fast_forward" type="button">Fast Forward</button>
|
<button id="fast_forward" type="button">Fast Forward</button>
|
||||||
<output id="flip_count">0 / 10</output>
|
<output id="flip_count">0 / 10</output>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="/js/webgl-book-shape-lab.js?v=cover-overhang-1"></script>
|
<script type="module" src="/js/webgl-book-shape-lab.js?v=task2-grid-aligned-1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user