diff --git a/public/js/webgl-book-shape-lab.js b/public/js/webgl-book-shape-lab.js
index 29c968d..7e4c3aa 100644
--- a/public/js/webgl-book-shape-lab.js
+++ b/public/js/webgl-book-shape-lab.js
@@ -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;
- 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();
+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(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);
}
diff --git a/public/webgl-book-shape-lab.html b/public/webgl-book-shape-lab.html
index fd30fa5..1587b21 100644
--- a/public/webgl-book-shape-lab.html
+++ b/public/webgl-book-shape-lab.html
@@ -68,10 +68,12 @@
+
+
-
+