Touch drags flip pages: the page follows the finger live, the axis locks on first movement so vertical pans never flip, and edge drags get rubber-band resistance. Swipes starting on interactive controls are ignored. Swipe intent lives in bookSwipeDirection with unit tests.
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
/**
|
|
* Layout math for the BookView two-page reader. Pure, so the flip logic is
|
|
* unit-testable and the component only does DOM measurement.
|
|
*
|
|
* The book renders one column per page. A spread shows `columnCount` pages
|
|
* side by side; flipping advances one page (column) at a time by translating
|
|
* the multicol content horizontally.
|
|
*/
|
|
|
|
/** Horizontal gap (px) between book pages: the book's spine. Keep it equal to
|
|
* or larger than the reader's horizontal padding so adjacent pages never peek
|
|
* into the page margins mid-flip. */
|
|
export const BOOK_COLUMN_GAP = 32;
|
|
|
|
/** Maximum width (px) of the whole two-page book window. On larger viewports
|
|
* the book stops growing and centers itself, so the capped text blocks stay
|
|
* close to the spine instead of drifting apart with huge gutters. Choose a
|
|
* value whose page width (content/2) stays close to the text cap. */
|
|
export const BOOK_MAX_WIDTH = 1536;
|
|
|
|
/** Viewport width (px) below which the book falls back to a single page. */
|
|
export const BOOK_SINGLE_COLUMN_BREAKPOINT = 768;
|
|
|
|
/** Pages shown side by side at the given viewport width. */
|
|
export function bookColumnCount(viewportWidth: number): number {
|
|
return viewportWidth > 0 && viewportWidth < BOOK_SINGLE_COLUMN_BREAKPOINT ? 1 : 2;
|
|
}
|
|
|
|
/** Distance between the left edges of consecutive pages. */
|
|
export function bookPagePitch(
|
|
viewportWidth: number,
|
|
columnCount: number,
|
|
gap = BOOK_COLUMN_GAP,
|
|
): number {
|
|
return (viewportWidth + gap) / columnCount;
|
|
}
|
|
|
|
/** Total number of pages the content spans, given the measured scrollWidth. */
|
|
export function bookPageCount(
|
|
scrollWidth: number,
|
|
viewportWidth: number,
|
|
columnCount: number,
|
|
gap = BOOK_COLUMN_GAP,
|
|
): number {
|
|
if (viewportWidth <= 0 || scrollWidth <= 0) return 1;
|
|
const pitch = bookPagePitch(viewportWidth, columnCount, gap);
|
|
return Math.max(1, Math.ceil((scrollWidth + gap) / pitch));
|
|
}
|
|
|
|
/** Horizontal translation (positive px) that brings pageIndex's page into view. */
|
|
export function bookColumnOffset(
|
|
pageIndex: number,
|
|
viewportWidth: number,
|
|
columnCount: number,
|
|
gap = BOOK_COLUMN_GAP,
|
|
): number {
|
|
return pageIndex * bookPagePitch(viewportWidth, columnCount, gap);
|
|
}
|
|
|
|
/**
|
|
* Swipe intent for a touch drag on the book: -1 = previous page, 1 = next
|
|
* page, 0 = no flip. Requires horizontal dominance (a vertical scroll gesture
|
|
* never flips) and travel past a threshold proportional to the page width.
|
|
* Swiping left reveals the next page; swiping right goes back.
|
|
*/
|
|
export function bookSwipeDirection(
|
|
deltaX: number,
|
|
deltaY: number,
|
|
pageAreaWidth: number,
|
|
): -1 | 0 | 1 {
|
|
if (Math.abs(deltaX) <= Math.abs(deltaY)) return 0;
|
|
const threshold = Math.max(48, pageAreaWidth * 0.18);
|
|
if (deltaX <= -threshold) return 1;
|
|
if (deltaX >= threshold) return -1;
|
|
return 0;
|
|
}
|