Checkpoint packed spine spacing
This commit is contained in:
@@ -64,12 +64,23 @@ 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;
|
||||||
const FAST_FLIP_OVERLAP = 5;
|
const FAST_FLIP_OVERLAP = 5;
|
||||||
|
const OPEN_SEAM_GAP = 0.003;
|
||||||
|
const SUPPORT_ANGLE_STEPS = 720;
|
||||||
|
const SUPPORT_ANGLE_CANDIDATES = Array.from({ length: SUPPORT_ANGLE_STEPS }, (_, sample) => {
|
||||||
|
const angle = sample / SUPPORT_ANGLE_STEPS * Math.PI * 2;
|
||||||
|
return {
|
||||||
|
angle,
|
||||||
|
cos: Math.cos(angle),
|
||||||
|
sin: Math.sin(angle)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
let readingProgress = readInitialProgress();
|
let readingProgress = readInitialProgress();
|
||||||
let pageCount = readInitialPageCount();
|
let pageCount = readInitialPageCount();
|
||||||
let lastLengthError = 0;
|
let lastLengthError = 0;
|
||||||
let lastSpacingError = 0;
|
let lastSpacingError = 0;
|
||||||
let lastBookModel = null;
|
let lastBookModel = null;
|
||||||
|
let activeSpineHalf = 0.08;
|
||||||
let activeFlips = [];
|
let activeFlips = [];
|
||||||
let pendingPageFlips = 0;
|
let pendingPageFlips = 0;
|
||||||
progressInput.value = readingProgress.toFixed(3);
|
progressInput.value = readingProgress.toFixed(3);
|
||||||
@@ -188,11 +199,14 @@ function rebuildBook() {
|
|||||||
const pageWidth = 1.62;
|
const pageWidth = 1.62;
|
||||||
const pageDepth = 2.24;
|
const pageDepth = 2.24;
|
||||||
const bundleCount = Math.max(4, Math.round(pageCount / 10));
|
const bundleCount = Math.max(4, Math.round(pageCount / 10));
|
||||||
const spineWidth = Math.max(0.16, bundleCount * BOOK_PROFILE.bundleSpacing);
|
const spineWidth = calculateSpineWidth(bundleCount);
|
||||||
const lines = simulatePageLines(bundleCount, pageWidth, spineWidth);
|
const leftCount = calculateLeftBundleCount(bundleCount);
|
||||||
|
const bundleSpacing = calculateBundleSpacing(bundleCount, spineWidth, leftCount);
|
||||||
|
activeSpineHalf = spineWidth * 0.5;
|
||||||
|
const lines = simulatePageLines(bundleCount, pageWidth, spineWidth, bundleSpacing, leftCount);
|
||||||
lastLengthError = measureLineLengthError(lines, pageWidth);
|
lastLengthError = measureLineLengthError(lines, pageWidth);
|
||||||
lastSpacingError = measureStackSpacingError(lines);
|
lastSpacingError = measureStackSpacingError(lines, bundleSpacing);
|
||||||
lastBookModel = { coverDepth, pageWidth, pageDepth, bundleCount, spineWidth, lines };
|
lastBookModel = { coverDepth, pageWidth, pageDepth, bundleCount, spineWidth, bundleSpacing, lines };
|
||||||
|
|
||||||
addCoverAssembly(pageWidth, coverDepth, coverThickness, spineWidth);
|
addCoverAssembly(pageWidth, coverDepth, coverThickness, spineWidth);
|
||||||
addClothSpine(pageDepth, spineWidth);
|
addClothSpine(pageDepth, spineWidth);
|
||||||
@@ -323,17 +337,76 @@ function spineCurvePoint(t, spineWidth) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function simulatePageLines(bundleCount, pageWidth, spineWidth) {
|
function calculateSpineWidth(bundleCount) {
|
||||||
|
const minimumWidth = 0.16;
|
||||||
|
if (bundleCount <= 1) return minimumWidth;
|
||||||
|
const targetArcLength = (bundleCount - 1) * BOOK_PROFILE.bundleSpacing + OPEN_SEAM_GAP;
|
||||||
|
let low = minimumWidth;
|
||||||
|
let high = Math.max(minimumWidth, bundleCount * BOOK_PROFILE.bundleSpacing * 1.4);
|
||||||
|
while (measureSpineArcLength(high) < targetArcLength) {
|
||||||
|
high *= 1.25;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 24; i += 1) {
|
||||||
|
const mid = (low + high) * 0.5;
|
||||||
|
if (measureSpineArcLength(mid) < targetArcLength) {
|
||||||
|
low = mid;
|
||||||
|
} else {
|
||||||
|
high = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return high;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateBundleSpacing(bundleCount, spineWidth, leftCount) {
|
||||||
|
const rightCount = bundleCount - leftCount;
|
||||||
|
const stackIntervals = Math.max(0, leftCount - 1) + Math.max(0, rightCount - 1);
|
||||||
|
if (stackIntervals <= 0) return BOOK_PROFILE.bundleSpacing;
|
||||||
|
return Math.max(0.001, (measureSpineArcLength(spineWidth) - OPEN_SEAM_GAP) / stackIntervals);
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureSpineArcLength(spineWidth) {
|
||||||
|
const steps = 240;
|
||||||
|
let length = 0;
|
||||||
|
let previous = spineCurvePoint(0, spineWidth);
|
||||||
|
for (let i = 1; i <= steps; i += 1) {
|
||||||
|
const point = spineCurvePoint(i / steps, spineWidth);
|
||||||
|
length += Math.hypot(point.x - previous.x, point.y - previous.y);
|
||||||
|
previous = point;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateLeftBundleCount(bundleCount) {
|
||||||
|
return THREE.MathUtils.clamp(Math.round(bundleCount * readingProgress), 0, bundleCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function simulatePageLines(bundleCount, pageWidth, spineWidth, bundleSpacing, leftCount) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const segments = 24;
|
const segments = 24;
|
||||||
const stepLength = pageWidth / segments;
|
const stepLength = pageWidth / segments;
|
||||||
const entries = [];
|
const entries = [];
|
||||||
const spineSamples = sampleSpineByArc(bundleCount, spineWidth);
|
const spineArc = buildSpineArcSamples(spineWidth);
|
||||||
const leftLimit = Math.min(bundleCount - 2, Math.floor((bundleCount - 1) * readingProgress));
|
const rightCount = bundleCount - leftCount;
|
||||||
|
const leftSpan = Math.max(0, leftCount - 1) * bundleSpacing;
|
||||||
|
const rightSpan = Math.max(0, rightCount - 1) * bundleSpacing;
|
||||||
|
const seamLeftLength = leftSpan;
|
||||||
|
const seamRightLength = seamLeftLength + OPEN_SEAM_GAP;
|
||||||
for (let index = 0; index < bundleCount; index += 1) {
|
for (let index = 0; index < bundleCount; index += 1) {
|
||||||
const t = spineSamples[index].t;
|
const side = index < leftCount ? -1 : 1;
|
||||||
const side = index <= leftLimit ? -1 : 1;
|
const sideRank = side < 0 ? index : index - leftCount;
|
||||||
entries.push({ index, t, side });
|
const arcLength = side < 0
|
||||||
|
? seamLeftLength - (leftCount - 1 - sideRank) * bundleSpacing
|
||||||
|
: seamRightLength + sideRank * bundleSpacing;
|
||||||
|
const point = pointAtSpineArcLength(spineArc, arcLength);
|
||||||
|
entries.push({ index, t: point.t, side });
|
||||||
|
}
|
||||||
|
if (leftCount === 0) {
|
||||||
|
const point = pointAtSpineArcLength(spineArc, seamLeftLength);
|
||||||
|
entries.push({ index: -1, t: point.t, side: -1, isHairPage: true });
|
||||||
|
}
|
||||||
|
if (rightCount === 0) {
|
||||||
|
const point = pointAtSpineArcLength(spineArc, seamRightLength);
|
||||||
|
entries.push({ index: bundleCount, t: point.t, side: 1, isHairPage: true });
|
||||||
}
|
}
|
||||||
[-1, 1].forEach((side) => {
|
[-1, 1].forEach((side) => {
|
||||||
const sideEntries = entries.filter((entry) => entry.side === side);
|
const sideEntries = entries.filter((entry) => entry.side === side);
|
||||||
@@ -351,9 +424,9 @@ function simulatePageLines(bundleCount, pageWidth, spineWidth) {
|
|||||||
let lowerLine = null;
|
let lowerLine = null;
|
||||||
sideEntries.forEach((entry, rank) => {
|
sideEntries.forEach((entry, rank) => {
|
||||||
const anchor = spineCurvePoint(entry.t, spineWidth);
|
const anchor = spineCurvePoint(entry.t, spineWidth);
|
||||||
const target = restingTarget(side, pageWidth, rank, sideEntries.length);
|
const target = restingTarget(side, pageWidth, rank, sideEntries.length, bundleSpacing);
|
||||||
const points = buildSupportSolvedLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount);
|
const points = buildSupportSolvedLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount, bundleSpacing);
|
||||||
const line = { index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1] };
|
const line = { index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1], isHairPage: entry.isHairPage === true };
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
lowerLine = line;
|
lowerLine = line;
|
||||||
});
|
});
|
||||||
@@ -371,7 +444,7 @@ function measureLineLengthError(lines, pageWidth) {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function measureStackSpacingError(lines) {
|
function measureStackSpacingError(lines, bundleSpacing) {
|
||||||
let maxViolation = 0;
|
let maxViolation = 0;
|
||||||
[-1, 1].forEach((side) => {
|
[-1, 1].forEach((side) => {
|
||||||
const sideLines = lines
|
const sideLines = lines
|
||||||
@@ -383,14 +456,14 @@ function measureStackSpacingError(lines) {
|
|||||||
for (let col = 1; col < upper.points.length; col += 1) {
|
for (let col = 1; col < upper.points.length; col += 1) {
|
||||||
const closest = closestPointOnPolyline(upper.points[col], lower.points);
|
const closest = closestPointOnPolyline(upper.points[col], lower.points);
|
||||||
const distance = Math.hypot(upper.points[col].x - closest.x, upper.points[col].y - closest.y);
|
const distance = Math.hypot(upper.points[col].x - closest.x, upper.points[col].y - closest.y);
|
||||||
maxViolation = Math.max(maxViolation, Math.max(0, BOOK_PROFILE.bundleSpacing - distance));
|
maxViolation = Math.max(maxViolation, Math.max(0, bundleSpacing - distance));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return maxViolation;
|
return maxViolation;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sampleSpineByArc(count, spineWidth) {
|
function buildSpineArcSamples(spineWidth) {
|
||||||
const samples = [];
|
const samples = [];
|
||||||
const steps = 240;
|
const steps = 240;
|
||||||
let length = 0;
|
let length = 0;
|
||||||
@@ -403,21 +476,27 @@ function sampleSpineByArc(count, spineWidth) {
|
|||||||
samples.push({ point, length });
|
samples.push({ point, length });
|
||||||
previous = point;
|
previous = point;
|
||||||
}
|
}
|
||||||
const points = [];
|
return { samples, length, spineWidth };
|
||||||
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);
|
function pointAtSpineArcLength(spineArc, targetLength) {
|
||||||
if (found <= 0) {
|
const target = THREE.MathUtils.clamp(targetLength, 0, spineArc.length);
|
||||||
points.push(samples[0].point);
|
let low = 0;
|
||||||
continue;
|
let high = spineArc.samples.length - 1;
|
||||||
|
while (low < high) {
|
||||||
|
const mid = Math.floor((low + high) * 0.5);
|
||||||
|
if (spineArc.samples[mid].length < target) {
|
||||||
|
low = mid + 1;
|
||||||
|
} else {
|
||||||
|
high = mid;
|
||||||
}
|
}
|
||||||
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;
|
if (low <= 0) return spineArc.samples[0].point;
|
||||||
|
const before = spineArc.samples[low - 1];
|
||||||
|
const after = spineArc.samples[low];
|
||||||
|
const span = after.length - before.length || 1;
|
||||||
|
const t = THREE.MathUtils.lerp(before.point.t, after.point.t, (target - before.length) / span);
|
||||||
|
return spineCurvePoint(t, spineArc.spineWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialPageLine(anchor, target, segments) {
|
function initialPageLine(anchor, target, segments) {
|
||||||
@@ -433,47 +512,54 @@ function initialPageLine(anchor, target, segments) {
|
|||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restingTarget(side, pageWidth, rank, sideCount) {
|
function restingTarget(side, pageWidth, rank, sideCount, bundleSpacing) {
|
||||||
const local = sideCount <= 1 ? 0 : rank / (sideCount - 1);
|
const local = sideCount <= 1 ? 0 : rank / (sideCount - 1);
|
||||||
const foreCurve = 0.11 * Math.sin(Math.PI * local);
|
const foreCurve = 0.11 * Math.sin(Math.PI * local);
|
||||||
const x = side * (pageWidth - foreCurve);
|
const x = side * (pageWidth - foreCurve);
|
||||||
const y = BOOK_PROFILE.coverThickness + BOOK_PROFILE.paperContactOffset + rank * BOOK_PROFILE.bundleSpacing + 0.002 * Math.sin(Math.PI * local);
|
const y = BOOK_PROFILE.coverThickness + BOOK_PROFILE.paperContactOffset + rank * bundleSpacing + 0.002 * Math.sin(Math.PI * local);
|
||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSupportSolvedLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount) {
|
function buildSupportSolvedLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount, bundleSpacing) {
|
||||||
const points = [{ x: anchor.x, y: anchor.y }];
|
const points = [{ x: anchor.x, y: anchor.y }];
|
||||||
let tangent = coverTangentAtX(anchor.x, side);
|
let tangent = coverTangentAtX(anchor.x, side);
|
||||||
for (let index = 1; index <= segments; index += 1) {
|
for (let index = 1; index <= segments; index += 1) {
|
||||||
const u = index / segments;
|
const u = index / segments;
|
||||||
const supportTangent = lowerLine ? lineTangentAt(lowerLine.points, index) : coverTangentAtX(points[index - 1].x, side);
|
const supportTangent = lowerLine ? lineTangentAt(lowerLine.points, index) : coverTangentAtX(points[index - 1].x, side);
|
||||||
const point = chooseClosestSupportedPoint(points[index - 1], tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, u);
|
const point = chooseClosestSupportedPoint(points[index - 1], tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, bundleSpacing, u);
|
||||||
points.push(point);
|
points.push(point);
|
||||||
tangent = normalizedVector(point.x - points[index - 1].x, point.y - points[index - 1].y);
|
tangent = normalizedVector(point.x - points[index - 1].x, point.y - points[index - 1].y);
|
||||||
}
|
}
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseClosestSupportedPoint(previous, tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, u) {
|
function chooseClosestSupportedPoint(previous, tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, bundleSpacing, u) {
|
||||||
const blendTangent = normalizedVector(tangent.x + supportTangent.x * 2, tangent.y + supportTangent.y * 2);
|
const blendTangent = normalizedVector(tangent.x + supportTangent.x * 2, tangent.y + supportTangent.y * 2);
|
||||||
const angleHint = Math.atan2(blendTangent.y, blendTangent.x);
|
const angleHint = Math.atan2(blendTangent.y, blendTangent.x);
|
||||||
let best = null;
|
let best = null;
|
||||||
let fallback = null;
|
for (const sample of SUPPORT_ANGLE_CANDIDATES) {
|
||||||
for (let sample = 0; sample < 720; sample += 1) {
|
|
||||||
const angle = sample / 720 * Math.PI * 2;
|
|
||||||
const candidate = {
|
const candidate = {
|
||||||
x: previous.x + Math.cos(angle) * stepLength,
|
x: previous.x + sample.cos * stepLength,
|
||||||
y: previous.y + Math.sin(angle) * stepLength
|
y: previous.y + sample.sin * stepLength
|
||||||
};
|
};
|
||||||
const score = scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle, angleHint, target, lowerLine, index, side, bundleCount, u);
|
const score = scoreSupportedPoint(candidate, previous, tangent, supportTangent, sample.angle, angleHint, target, lowerLine, index, side, bundleCount, bundleSpacing, u);
|
||||||
if (best === null || score < best.score) best = { point: candidate, score };
|
if (best === null || score < best.score) best = { point: candidate, score };
|
||||||
const fallbackScore = scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle, angleHint, target, lowerLine, index, side, bundleCount, u, true);
|
}
|
||||||
|
if (Number.isFinite(best?.score)) return best.point;
|
||||||
|
|
||||||
|
let fallback = null;
|
||||||
|
for (const sample of SUPPORT_ANGLE_CANDIDATES) {
|
||||||
|
const candidate = {
|
||||||
|
x: previous.x + sample.cos * stepLength,
|
||||||
|
y: previous.y + sample.sin * stepLength
|
||||||
|
};
|
||||||
|
const fallbackScore = scoreSupportedPoint(candidate, previous, tangent, supportTangent, sample.angle, angleHint, target, lowerLine, index, side, bundleCount, bundleSpacing, u, true);
|
||||||
if (fallback === null || fallbackScore < fallback.score) fallback = { point: candidate, score: fallbackScore };
|
if (fallback === null || fallbackScore < fallback.score) fallback = { point: candidate, score: fallbackScore };
|
||||||
}
|
}
|
||||||
return Number.isFinite(best?.score) ? best.point : fallback.point;
|
return fallback.point;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle, angleHint, target, lowerLine, index, side, bundleCount, u, allowViolation = false) {
|
function scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle, angleHint, target, lowerLine, index, side, bundleCount, bundleSpacing, u, allowViolation = false) {
|
||||||
const backward = Math.max(0, side * (previous.x - candidate.x));
|
const backward = Math.max(0, side * (previous.x - candidate.x));
|
||||||
if (!allowViolation && backward > 0.00001) return Number.POSITIVE_INFINITY;
|
if (!allowViolation && backward > 0.00001) return Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
@@ -482,9 +568,9 @@ function scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle
|
|||||||
if (lowerLine) {
|
if (lowerLine) {
|
||||||
const closest = closestPointOnPolyline(candidate, lowerLine.points);
|
const closest = closestPointOnPolyline(candidate, lowerLine.points);
|
||||||
const closestDistance = Math.hypot(candidate.x - closest.x, candidate.y - closest.y);
|
const closestDistance = Math.hypot(candidate.x - closest.x, candidate.y - closest.y);
|
||||||
supportViolation = Math.max(0, BOOK_PROFILE.bundleSpacing - closestDistance) + Math.max(0, closest.y - candidate.y);
|
supportViolation = Math.max(0, bundleSpacing - closestDistance) + Math.max(0, closest.y - candidate.y);
|
||||||
if (!allowViolation && supportViolation > 0.00001) return Number.POSITIVE_INFINITY;
|
if (!allowViolation && supportViolation > 0.00001) return Number.POSITIVE_INFINITY;
|
||||||
supportError = closestDistance - BOOK_PROFILE.bundleSpacing;
|
supportError = closestDistance - bundleSpacing;
|
||||||
} else {
|
} else {
|
||||||
const floor = coverTopYAtX(candidate.x) + coverClearance(bundleCount);
|
const floor = coverTopYAtX(candidate.x) + coverClearance(bundleCount);
|
||||||
supportViolation = Math.max(0, floor - candidate.y);
|
supportViolation = Math.max(0, floor - candidate.y);
|
||||||
@@ -636,6 +722,7 @@ function coverClearance(bundleCount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function enforceStackConstraints(lines, stepLength, bundleCount) {
|
function enforceStackConstraints(lines, stepLength, bundleCount) {
|
||||||
|
const bundleSpacing = lastBookModel?.bundleSpacing ?? BOOK_PROFILE.bundleSpacing;
|
||||||
const iterations = 44;
|
const iterations = 44;
|
||||||
[-1, 1].forEach((side) => {
|
[-1, 1].forEach((side) => {
|
||||||
const sideLines = lines
|
const sideLines = lines
|
||||||
@@ -655,8 +742,8 @@ function enforceStackConstraints(lines, stepLength, bundleCount) {
|
|||||||
const upper = sideLines[row];
|
const upper = sideLines[row];
|
||||||
for (let col = 1; col < upper.points.length; col += 1) {
|
for (let col = 1; col < upper.points.length; col += 1) {
|
||||||
const normal = upwardNormalAt(lower.points, col);
|
const normal = upwardNormalAt(lower.points, col);
|
||||||
const targetX = lower.points[col].x + normal.x * BOOK_PROFILE.bundleSpacing;
|
const targetX = lower.points[col].x + normal.x * bundleSpacing;
|
||||||
const targetY = lower.points[col].y + normal.y * BOOK_PROFILE.bundleSpacing;
|
const targetY = lower.points[col].y + normal.y * bundleSpacing;
|
||||||
upper.points[col].x = THREE.MathUtils.lerp(upper.points[col].x, targetX, 0.28);
|
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[col].y = Math.max(upper.points[col].y, THREE.MathUtils.lerp(upper.points[col].y, targetY, 0.42));
|
||||||
}
|
}
|
||||||
@@ -720,7 +807,7 @@ function coverTopYAtX(x) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function currentSpineHalf() {
|
function currentSpineHalf() {
|
||||||
return Math.max(0.16, Math.round(pageCount / 10) * BOOK_PROFILE.bundleSpacing) * 0.5;
|
return activeSpineHalf;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSimulatedStackBodies(lines, depth) {
|
function addSimulatedStackBodies(lines, depth) {
|
||||||
@@ -735,9 +822,10 @@ function addSimulatedStackBodies(lines, depth) {
|
|||||||
|
|
||||||
function createSinglePageBodyLines(line) {
|
function createSinglePageBodyLines(line) {
|
||||||
const bundleCount = Math.max(4, Math.round(pageCount / 10));
|
const bundleCount = Math.max(4, Math.round(pageCount / 10));
|
||||||
|
const bundleSpacing = lastBookModel?.bundleSpacing ?? BOOK_PROFILE.bundleSpacing;
|
||||||
const supportPoints = line.points.map((point) => ({
|
const supportPoints = line.points.map((point) => ({
|
||||||
x: point.x,
|
x: point.x,
|
||||||
y: Math.max(coverTopYAtX(point.x) + coverClearance(bundleCount) + BOOK_PROFILE.singlePageCoverGap, point.y - BOOK_PROFILE.bundleSpacing)
|
y: Math.max(coverTopYAtX(point.x) + coverClearance(bundleCount) + BOOK_PROFILE.singlePageCoverGap, point.y - bundleSpacing)
|
||||||
}));
|
}));
|
||||||
return [
|
return [
|
||||||
{ ...line, points: supportPoints, endpoint: supportPoints[supportPoints.length - 1] },
|
{ ...line, points: supportPoints, endpoint: supportPoints[supportPoints.length - 1] },
|
||||||
@@ -951,10 +1039,11 @@ function buildFlippingPageSurface(sourceLine, destinationLine, direction, t, pag
|
|||||||
const baseAngle = startAngle + direction * Math.PI * t;
|
const baseAngle = startAngle + direction * Math.PI * t;
|
||||||
const lift = Math.sin(Math.PI * t);
|
const lift = Math.sin(Math.PI * t);
|
||||||
const curlStrength = direction * 0.48 * lift;
|
const curlStrength = direction * 0.48 * lift;
|
||||||
|
const sourceLengths = cumulativeLineLengths(sourceLine.points);
|
||||||
const surface = [];
|
const surface = [];
|
||||||
for (let widthIndex = 0; widthIndex <= widthSegments; widthIndex += 1) {
|
for (let widthIndex = 0; widthIndex <= widthSegments; widthIndex += 1) {
|
||||||
const u = widthIndex / widthSegments;
|
const u = widthIndex / widthSegments;
|
||||||
const radius = lastBookModel.pageWidth * u;
|
const radius = sourceLengths[widthIndex];
|
||||||
const row = [];
|
const row = [];
|
||||||
for (let depthIndex = 0; depthIndex <= depthSegments; depthIndex += 1) {
|
for (let depthIndex = 0; depthIndex <= depthSegments; depthIndex += 1) {
|
||||||
const v = depthIndex / depthSegments;
|
const v = depthIndex / depthSegments;
|
||||||
@@ -977,6 +1066,16 @@ function buildFlippingPageSurface(sourceLine, destinationLine, direction, t, pag
|
|||||||
return surface;
|
return surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cumulativeLineLengths(points) {
|
||||||
|
const lengths = [0];
|
||||||
|
for (let index = 1; index < points.length; index += 1) {
|
||||||
|
const previous = points[index - 1];
|
||||||
|
const current = points[index];
|
||||||
|
lengths.push(lengths[index - 1] + Math.hypot(current.x - previous.x, current.y - previous.y));
|
||||||
|
}
|
||||||
|
return lengths;
|
||||||
|
}
|
||||||
|
|
||||||
function createRestingPageSurface(points, depthSegments, zFront, zBack) {
|
function createRestingPageSurface(points, depthSegments, zFront, zBack) {
|
||||||
return points.map((point) => {
|
return points.map((point) => {
|
||||||
const row = [];
|
const row = [];
|
||||||
|
|||||||
@@ -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=stack-texture-lines-5"></script>
|
<script type="module" src="/js/webgl-book-shape-lab.js?v=packed-spine-seam-2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user