export const PAGE_GAP = 24; export const MAX_RENDERED_PAGES = 9; export const MAX_CANVAS_PIXELS = 1_500_000; export function buildPageLayout( count, width, ratios = {}, defaultRatio = Math.SQRT2, ) { const offsets = [], heights = []; let totalHeight = 0; for (let index = 0; index < count; index++) { offsets.push(totalHeight); const height = width * (ratios[index + 1] || defaultRatio); heights.push(height); totalHeight += height + PAGE_GAP; } return { offsets, heights, totalHeight: Math.max(0, totalHeight - PAGE_GAP), count, }; } export function pageAtOffset(layout, offset) { let low = 0, high = layout.count - 1; while (low < high) { const middle = Math.ceil((low + high) / 2); // Scroll offsets can round down by a fraction of a CSS pixel at browser zoom. if (layout.offsets[middle] <= offset + 0.5) low = middle; else high = middle - 1; } return low + 1; } export function readingPosition(layout, offset) { const page = pageAtOffset(layout, Math.max(0, offset)); const height = layout.heights[page - 1] || 1; return { page, pageOffset: Math.max( 0, Math.min(1, (offset - layout.offsets[page - 1]) / height), ), }; } export function positionOffset(layout, position) { const page = Math.max(1, Math.min(layout.count, position.page)); return ( (layout.offsets[page - 1] || 0) + (position.pageOffset || 0) * (layout.heights[page - 1] || 0) ); } export function navigationScrollTop(layout, position, padding, maximumTop) { return Math.max( 0, Math.min(maximumTop, padding + positionOffset(layout, position)), ); } export function renderedWindow(layout, top, height) { if (!layout.count) return { start: 1, end: 0 }; const start = Math.max(1, pageAtOffset(layout, Math.max(0, top)) - 1); const end = Math.min( layout.count, pageAtOffset(layout, top + height) + 1, start + MAX_RENDERED_PAGES - 1, ); return { start, end }; } export function canvasPixelRatio(width, height, deviceRatio = 1) { return Math.min( deviceRatio, 1.5, Math.sqrt(MAX_CANVAS_PIXELS / Math.max(1, width * height)), ); }