Add burst page flip controls

This commit is contained in:
2026-06-05 03:41:26 +02:00
parent ae8068ad8a
commit ae84eb8976
2 changed files with 121 additions and 42 deletions
+117 -40
View File
@@ -6,8 +6,10 @@ 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 fastBackwardButton = document.getElementById('fast_backward');
const flipBackwardButton = document.getElementById('flip_backward');
const flipForwardButton = document.getElementById('flip_forward');
const fastForwardButton = document.getElementById('fast_forward');
const flipCountValue = document.getElementById('flip_count');
const urlParams = new URLSearchParams(window.location.search);
@@ -58,14 +60,17 @@ const BOOK_PROFILE = {
singlePageCoverGap: 0.006,
bundleSpacing: 0.014
};
const NORMAL_FLIP_DURATION = 1800;
const FAST_FLIP_DURATION = 900;
const FAST_FLIP_COUNT = 10;
const FAST_FLIP_OVERLAP = 5;
let readingProgress = readInitialProgress();
let pageCount = readInitialPageCount();
let lastLengthError = 0;
let lastSpacingError = 0;
let lastBookModel = null;
let activeFlip = null;
let activeFlipMesh = null;
let activeFlips = [];
let pendingPageFlips = 0;
progressInput.value = readingProgress.toFixed(3);
progressValue.value = readingProgress.toFixed(2);
@@ -84,6 +89,10 @@ pageCountInput.addEventListener('input', () => {
setPageCount(pageCountInput.value);
});
fastBackwardButton.addEventListener('click', () => {
startFastPageFlip(-1);
});
flipBackwardButton.addEventListener('click', () => {
startPageFlip(-1);
});
@@ -92,6 +101,10 @@ flipForwardButton.addEventListener('click', () => {
startPageFlip(1);
});
fastForwardButton.addEventListener('click', () => {
startFastPageFlip(1);
});
window.addEventListener('resize', resize);
window.BookShapeLab = {
@@ -120,6 +133,12 @@ window.BookShapeLab = {
},
flipBackward() {
return startPageFlip(-1);
},
fastForward() {
return startFastPageFlip(1);
},
fastBackward() {
return startFastPageFlip(-1);
}
};
@@ -780,22 +799,55 @@ function createEndpointPolyline(lines, depth) {
}
function startPageFlip(direction) {
if (activeFlip || !lastBookModel || !canPageFlip(direction)) return false;
if (activeFlips.length || !lastBookModel || !canPageFlip(direction)) return false;
const flip = createPageFlip(direction, performance.now(), NORMAL_FLIP_DURATION);
if (!flip) return false;
activeFlips.push(flip);
updateFlipControls();
updateActiveFlips(flip.startTime);
return true;
}
function startFastPageFlip(direction) {
if (activeFlips.length || !lastBookModel || !canPageFlip(direction)) return false;
const firstFlip = createPageFlip(direction, performance.now(), FAST_FLIP_DURATION);
if (!firstFlip) return false;
const startTime = firstFlip.startTime;
const interval = FAST_FLIP_DURATION / FAST_FLIP_OVERLAP;
for (let index = 0; index < FAST_FLIP_COUNT; index += 1) {
activeFlips.push({
...firstFlip,
mesh: null,
startTime: startTime + index * interval,
pageOffset: index * 0.002,
commitBundleOnFinish: index === FAST_FLIP_COUNT - 1,
countAsPending: false
});
}
updateFlipControls();
updateActiveFlips(startTime);
return true;
}
function createPageFlip(direction, startTime, duration) {
const sourceSide = direction > 0 ? 1 : -1;
const sourceLine = topVisibleLine(sourceSide);
const destinationLine = topVisibleLine(-sourceSide);
if (!sourceLine || !destinationLine) return false;
if (!sourceLine || !destinationLine) return null;
activeFlip = {
return {
direction,
sourceLine,
destinationLine,
startTime: performance.now(),
duration: 1800
startTime,
duration,
pageOffset: 0,
commitBundleOnFinish: false,
countAsPending: true,
mesh: null
};
updateFlipControls();
updateActiveFlip(activeFlip.startTime);
return true;
}
function canPageFlip(direction) {
@@ -811,18 +863,23 @@ function topVisibleLine(side) {
return sideLines[sideLines.length - 1] ?? null;
}
function updateActiveFlip(now) {
if (!activeFlip || !lastBookModel) return;
const elapsed = (now - activeFlip.startTime) / activeFlip.duration;
function updateActiveFlips(now) {
if (!activeFlips.length || !lastBookModel) return;
const completed = [];
activeFlips.forEach((flip) => {
const elapsed = (now - flip.startTime) / flip.duration;
if (elapsed < 0) return;
const t = THREE.MathUtils.clamp(elapsed, 0, 1);
const surface = buildFlippingPageSurface(activeFlip.sourceLine, activeFlip.destinationLine, activeFlip.direction, easeInOutCubic(t));
setActivePageGeometry(surface);
if (t < 1) return;
finishActiveFlip();
const surface = buildFlippingPageSurface(flip.sourceLine, flip.destinationLine, flip.direction, easeInOutCubic(t), flip.pageOffset);
setActivePageGeometry(flip, surface);
if (t >= 1) completed.push(flip);
});
completed.forEach((flip) => {
finishActiveFlip(flip);
});
}
function buildFlippingPageSurface(sourceLine, destinationLine, direction, t) {
function buildFlippingPageSurface(sourceLine, destinationLine, direction, t, pageOffset = 0) {
const widthSegments = sourceLine.points.length - 1;
const depthSegments = 18;
const zFront = lastBookModel.pageDepth * 0.5 + 0.018;
@@ -852,7 +909,7 @@ function buildFlippingPageSurface(sourceLine, destinationLine, direction, t) {
const relaxedY = THREE.MathUtils.lerp(stackPoint.y, anchor.y + Math.sin(angle) * radius, lift);
const point = {
x: anchor.x + Math.cos(angle) * radius,
y: relaxedY + 0.055 * lift * Math.sin(Math.PI * u),
y: relaxedY + pageOffset + 0.055 * lift * Math.sin(Math.PI * u),
z
};
keepFlippingSurfacePointAboveStacks(point, lift);
@@ -930,15 +987,15 @@ function lineYAtX(points, x) {
return y;
}
function setActivePageGeometry(surface) {
function setActivePageGeometry(flip, surface) {
const geometry = createFlippingPageGeometry(surface);
if (!activeFlipMesh) {
activeFlipMesh = new THREE.Mesh(geometry, materials.flippingPage);
book.add(activeFlipMesh);
if (!flip.mesh) {
flip.mesh = new THREE.Mesh(geometry, materials.flippingPage);
book.add(flip.mesh);
return;
}
activeFlipMesh.geometry.dispose();
activeFlipMesh.geometry = geometry;
flip.mesh.geometry.dispose();
flip.mesh.geometry = geometry;
}
function createFlippingPageGeometry(surface) {
@@ -1002,31 +1059,51 @@ function createFlippingPageGeometry(surface) {
}
}
function finishActiveFlip() {
const direction = activeFlip.direction;
clearActiveFlip();
pendingPageFlips += direction;
function finishActiveFlip(flip) {
removeFlipMesh(flip);
activeFlips = activeFlips.filter((active) => active !== flip);
if (flip.commitBundleOnFinish) {
shiftReadingProgressByBundle(flip.direction);
return;
}
if (!flip.countAsPending) {
updateFlipControls();
return;
}
pendingPageFlips += flip.direction;
if (Math.abs(pendingPageFlips) >= 10) {
const commitDirection = Math.sign(pendingPageFlips);
pendingPageFlips -= commitDirection * 10;
const step = 1 / (lastBookModel.bundleCount - 1);
setReadingProgress(readingProgress + commitDirection * step);
shiftReadingProgressByBundle(commitDirection);
return;
}
updateFlipControls();
}
function shiftReadingProgressByBundle(direction) {
const step = 1 / (lastBookModel.bundleCount - 1);
setReadingProgress(readingProgress + direction * step);
}
function clearActiveFlip() {
activeFlip = null;
if (!activeFlipMesh) return;
book.remove(activeFlipMesh);
activeFlipMesh.geometry.dispose();
activeFlipMesh = null;
activeFlips.forEach(removeFlipMesh);
activeFlips = [];
}
function removeFlipMesh(flip) {
if (!flip.mesh) return;
book.remove(flip.mesh);
flip.mesh.geometry.dispose();
flip.mesh = null;
}
function updateFlipControls() {
flipBackwardButton.disabled = Boolean(activeFlip) || !canPageFlip(-1);
flipForwardButton.disabled = Boolean(activeFlip) || !canPageFlip(1);
const busy = activeFlips.length > 0;
fastBackwardButton.disabled = busy || !canPageFlip(-1);
flipBackwardButton.disabled = busy || !canPageFlip(-1);
flipForwardButton.disabled = busy || !canPageFlip(1);
fastForwardButton.disabled = busy || !canPageFlip(1);
flipCountValue.textContent = `${Math.abs(pendingPageFlips)} / 10`;
}
@@ -1048,7 +1125,7 @@ function animate() {
const t = performance.now() * 0.00035;
setReadingProgress(0.5 + Math.sin(t) * 0.48);
}
updateActiveFlip(performance.now());
updateActiveFlips(performance.now());
controls.update();
renderer.render(scene, camera);
}
+3 -1
View File
@@ -68,10 +68,12 @@
<label for="page_count">Book pages</label>
<input id="page_count" type="range" min="40" max="600" step="10" value="240">
<output id="page_count_value" for="page_count">240</output>
<button id="fast_backward" type="button">Fast Backward</button>
<button id="flip_backward" type="button">Backward</button>
<button id="flip_forward" type="button">Forward</button>
<button id="fast_forward" type="button">Fast Forward</button>
<output id="flip_count">0 / 10</output>
</div>
<script type="module" src="/js/webgl-book-shape-lab.js?v=flip-3d-surface-3"></script>
<script type="module" src="/js/webgl-book-shape-lab.js?v=flip-burst-1"></script>
</body>
</html>