/** * 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; }