Add burst page flip controls
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user