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 progressValue = document.getElementById('progress_value');
|
||||||
const pageCountInput = document.getElementById('page_count');
|
const pageCountInput = document.getElementById('page_count');
|
||||||
const pageCountValue = document.getElementById('page_count_value');
|
const pageCountValue = document.getElementById('page_count_value');
|
||||||
|
const fastBackwardButton = document.getElementById('fast_backward');
|
||||||
const flipBackwardButton = document.getElementById('flip_backward');
|
const flipBackwardButton = document.getElementById('flip_backward');
|
||||||
const flipForwardButton = document.getElementById('flip_forward');
|
const flipForwardButton = document.getElementById('flip_forward');
|
||||||
|
const fastForwardButton = document.getElementById('fast_forward');
|
||||||
const flipCountValue = document.getElementById('flip_count');
|
const flipCountValue = document.getElementById('flip_count');
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
@@ -58,14 +60,17 @@ const BOOK_PROFILE = {
|
|||||||
singlePageCoverGap: 0.006,
|
singlePageCoverGap: 0.006,
|
||||||
bundleSpacing: 0.014
|
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 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 activeFlip = null;
|
let activeFlips = [];
|
||||||
let activeFlipMesh = null;
|
|
||||||
let pendingPageFlips = 0;
|
let pendingPageFlips = 0;
|
||||||
progressInput.value = readingProgress.toFixed(3);
|
progressInput.value = readingProgress.toFixed(3);
|
||||||
progressValue.value = readingProgress.toFixed(2);
|
progressValue.value = readingProgress.toFixed(2);
|
||||||
@@ -84,6 +89,10 @@ pageCountInput.addEventListener('input', () => {
|
|||||||
setPageCount(pageCountInput.value);
|
setPageCount(pageCountInput.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fastBackwardButton.addEventListener('click', () => {
|
||||||
|
startFastPageFlip(-1);
|
||||||
|
});
|
||||||
|
|
||||||
flipBackwardButton.addEventListener('click', () => {
|
flipBackwardButton.addEventListener('click', () => {
|
||||||
startPageFlip(-1);
|
startPageFlip(-1);
|
||||||
});
|
});
|
||||||
@@ -92,6 +101,10 @@ flipForwardButton.addEventListener('click', () => {
|
|||||||
startPageFlip(1);
|
startPageFlip(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fastForwardButton.addEventListener('click', () => {
|
||||||
|
startFastPageFlip(1);
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
|
|
||||||
window.BookShapeLab = {
|
window.BookShapeLab = {
|
||||||
@@ -120,6 +133,12 @@ window.BookShapeLab = {
|
|||||||
},
|
},
|
||||||
flipBackward() {
|
flipBackward() {
|
||||||
return startPageFlip(-1);
|
return startPageFlip(-1);
|
||||||
|
},
|
||||||
|
fastForward() {
|
||||||
|
return startFastPageFlip(1);
|
||||||
|
},
|
||||||
|
fastBackward() {
|
||||||
|
return startFastPageFlip(-1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -780,22 +799,55 @@ function createEndpointPolyline(lines, depth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startPageFlip(direction) {
|
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 sourceSide = direction > 0 ? 1 : -1;
|
||||||
const sourceLine = topVisibleLine(sourceSide);
|
const sourceLine = topVisibleLine(sourceSide);
|
||||||
const destinationLine = topVisibleLine(-sourceSide);
|
const destinationLine = topVisibleLine(-sourceSide);
|
||||||
if (!sourceLine || !destinationLine) return false;
|
if (!sourceLine || !destinationLine) return null;
|
||||||
|
|
||||||
activeFlip = {
|
return {
|
||||||
direction,
|
direction,
|
||||||
sourceLine,
|
sourceLine,
|
||||||
destinationLine,
|
destinationLine,
|
||||||
startTime: performance.now(),
|
startTime,
|
||||||
duration: 1800
|
duration,
|
||||||
|
pageOffset: 0,
|
||||||
|
commitBundleOnFinish: false,
|
||||||
|
countAsPending: true,
|
||||||
|
mesh: null
|
||||||
};
|
};
|
||||||
updateFlipControls();
|
|
||||||
updateActiveFlip(activeFlip.startTime);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function canPageFlip(direction) {
|
function canPageFlip(direction) {
|
||||||
@@ -811,18 +863,23 @@ function topVisibleLine(side) {
|
|||||||
return sideLines[sideLines.length - 1] ?? null;
|
return sideLines[sideLines.length - 1] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateActiveFlip(now) {
|
function updateActiveFlips(now) {
|
||||||
if (!activeFlip || !lastBookModel) return;
|
if (!activeFlips.length || !lastBookModel) return;
|
||||||
const elapsed = (now - activeFlip.startTime) / activeFlip.duration;
|
const completed = [];
|
||||||
const t = THREE.MathUtils.clamp(elapsed, 0, 1);
|
activeFlips.forEach((flip) => {
|
||||||
const surface = buildFlippingPageSurface(activeFlip.sourceLine, activeFlip.destinationLine, activeFlip.direction, easeInOutCubic(t));
|
const elapsed = (now - flip.startTime) / flip.duration;
|
||||||
setActivePageGeometry(surface);
|
if (elapsed < 0) return;
|
||||||
if (t < 1) return;
|
const t = THREE.MathUtils.clamp(elapsed, 0, 1);
|
||||||
|
const surface = buildFlippingPageSurface(flip.sourceLine, flip.destinationLine, flip.direction, easeInOutCubic(t), flip.pageOffset);
|
||||||
finishActiveFlip();
|
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 widthSegments = sourceLine.points.length - 1;
|
||||||
const depthSegments = 18;
|
const depthSegments = 18;
|
||||||
const zFront = lastBookModel.pageDepth * 0.5 + 0.018;
|
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 relaxedY = THREE.MathUtils.lerp(stackPoint.y, anchor.y + Math.sin(angle) * radius, lift);
|
||||||
const point = {
|
const point = {
|
||||||
x: anchor.x + Math.cos(angle) * radius,
|
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
|
z
|
||||||
};
|
};
|
||||||
keepFlippingSurfacePointAboveStacks(point, lift);
|
keepFlippingSurfacePointAboveStacks(point, lift);
|
||||||
@@ -930,15 +987,15 @@ function lineYAtX(points, x) {
|
|||||||
return y;
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActivePageGeometry(surface) {
|
function setActivePageGeometry(flip, surface) {
|
||||||
const geometry = createFlippingPageGeometry(surface);
|
const geometry = createFlippingPageGeometry(surface);
|
||||||
if (!activeFlipMesh) {
|
if (!flip.mesh) {
|
||||||
activeFlipMesh = new THREE.Mesh(geometry, materials.flippingPage);
|
flip.mesh = new THREE.Mesh(geometry, materials.flippingPage);
|
||||||
book.add(activeFlipMesh);
|
book.add(flip.mesh);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeFlipMesh.geometry.dispose();
|
flip.mesh.geometry.dispose();
|
||||||
activeFlipMesh.geometry = geometry;
|
flip.mesh.geometry = geometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFlippingPageGeometry(surface) {
|
function createFlippingPageGeometry(surface) {
|
||||||
@@ -1002,31 +1059,51 @@ function createFlippingPageGeometry(surface) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishActiveFlip() {
|
function finishActiveFlip(flip) {
|
||||||
const direction = activeFlip.direction;
|
removeFlipMesh(flip);
|
||||||
clearActiveFlip();
|
activeFlips = activeFlips.filter((active) => active !== flip);
|
||||||
pendingPageFlips += direction;
|
if (flip.commitBundleOnFinish) {
|
||||||
|
shiftReadingProgressByBundle(flip.direction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!flip.countAsPending) {
|
||||||
|
updateFlipControls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPageFlips += flip.direction;
|
||||||
if (Math.abs(pendingPageFlips) >= 10) {
|
if (Math.abs(pendingPageFlips) >= 10) {
|
||||||
const commitDirection = Math.sign(pendingPageFlips);
|
const commitDirection = Math.sign(pendingPageFlips);
|
||||||
pendingPageFlips -= commitDirection * 10;
|
pendingPageFlips -= commitDirection * 10;
|
||||||
const step = 1 / (lastBookModel.bundleCount - 1);
|
shiftReadingProgressByBundle(commitDirection);
|
||||||
setReadingProgress(readingProgress + commitDirection * step);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateFlipControls();
|
updateFlipControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shiftReadingProgressByBundle(direction) {
|
||||||
|
const step = 1 / (lastBookModel.bundleCount - 1);
|
||||||
|
setReadingProgress(readingProgress + direction * step);
|
||||||
|
}
|
||||||
|
|
||||||
function clearActiveFlip() {
|
function clearActiveFlip() {
|
||||||
activeFlip = null;
|
activeFlips.forEach(removeFlipMesh);
|
||||||
if (!activeFlipMesh) return;
|
activeFlips = [];
|
||||||
book.remove(activeFlipMesh);
|
}
|
||||||
activeFlipMesh.geometry.dispose();
|
|
||||||
activeFlipMesh = null;
|
function removeFlipMesh(flip) {
|
||||||
|
if (!flip.mesh) return;
|
||||||
|
book.remove(flip.mesh);
|
||||||
|
flip.mesh.geometry.dispose();
|
||||||
|
flip.mesh = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFlipControls() {
|
function updateFlipControls() {
|
||||||
flipBackwardButton.disabled = Boolean(activeFlip) || !canPageFlip(-1);
|
const busy = activeFlips.length > 0;
|
||||||
flipForwardButton.disabled = Boolean(activeFlip) || !canPageFlip(1);
|
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`;
|
flipCountValue.textContent = `${Math.abs(pendingPageFlips)} / 10`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1048,7 +1125,7 @@ function animate() {
|
|||||||
const t = performance.now() * 0.00035;
|
const t = performance.now() * 0.00035;
|
||||||
setReadingProgress(0.5 + Math.sin(t) * 0.48);
|
setReadingProgress(0.5 + Math.sin(t) * 0.48);
|
||||||
}
|
}
|
||||||
updateActiveFlip(performance.now());
|
updateActiveFlips(performance.now());
|
||||||
controls.update();
|
controls.update();
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,10 +68,12 @@
|
|||||||
<label for="page_count">Book pages</label>
|
<label for="page_count">Book pages</label>
|
||||||
<input id="page_count" type="range" min="40" max="600" step="10" value="240">
|
<input id="page_count" type="range" min="40" max="600" step="10" value="240">
|
||||||
<output id="page_count_value" for="page_count">240</output>
|
<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_backward" type="button">Backward</button>
|
||||||
<button id="flip_forward" type="button">Forward</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>
|
<output id="flip_count">0 / 10</output>
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user