src/paging.ts (3227 bytes)
1 // Page turns from keys, swipes and the arrows of the top bar. Pure: no DOM. 2 // 3 // In right-to-left mode (vertical Japanese) the next page is on the left: 4 // ArrowLeft, a swipe to the right and the left arrow go forward. 5 6 /** A page turn: 1 goes to the next page, -1 to the page before. */ 7 export type Step = -1 | 1; 8 9 /** The shortest swipe that turns a page, in CSS pixels. */ 10 export const SWIPE_MIN = 50; 11 12 /** A swipe turns a page only when its horizontal move is more than this times its vertical move. */ 13 export const SWIPE_RATIO = 1.5; 14 15 /** A press this long, in milliseconds, without a move is a long press: an Anki card. */ 16 export const LONG_PRESS_MS = 500; 17 18 /** A finger that moves more than this, in CSS pixels, makes no long press. */ 19 export const LONG_PRESS_MOVE = 10; 20 21 /** True when the finger moved too far for a long press. Such a move can be a swipe. */ 22 export function movedTooFar(dx: number, dy: number): boolean { 23 return Math.hypot(dx, dy) > LONG_PRESS_MOVE; 24 } 25 26 /** The step of a key, or 0 for a key that turns no page. */ 27 export function keyStep(key: string, rtl: boolean): Step | 0 { 28 switch (key) { 29 case 'PageDown': 30 return 1; 31 case 'PageUp': 32 return -1; 33 case 'ArrowRight': 34 return rtl ? -1 : 1; 35 case 'ArrowLeft': 36 return rtl ? 1 : -1; 37 default: 38 return 0; 39 } 40 } 41 42 export interface Swipe { 43 /** The move of the finger in CSS pixels. A positive dx goes right, a positive dy goes down. */ 44 dx: number; 45 dy: number; 46 /** True when the user zoomed in on the page. */ 47 zoomed: boolean; 48 /** True when the viewer can scroll sideways. */ 49 wide: boolean; 50 } 51 52 /** 53 * The step of a swipe, or 0 when the swipe turns no page. A finger that 54 * moves to the left goes to the next page, as on paper. A page that is 55 * zoomed in or wider than the viewer needs the swipe to pan, so it does 56 * not turn. 57 */ 58 export function swipeStep(s: Swipe, rtl: boolean): Step | 0 { 59 if (s.zoomed || s.wide) return 0; 60 const x = Math.abs(s.dx); 61 if (x < SWIPE_MIN || x <= SWIPE_RATIO * Math.abs(s.dy)) return 0; 62 return s.dx < 0 !== rtl ? 1 : -1; 63 } 64 65 /** The step of the left or the right arrow of the top bar. */ 66 export function arrowStep(side: 'left' | 'right', rtl: boolean): Step { 67 return (side === 'right') !== rtl ? 1 : -1; 68 } 69 70 /** 71 * Which arrows are off: an arrow is off when its step leads to no page. 72 * `pages` is 0 when no PDF is open. 73 */ 74 export function arrowsOff( 75 page: number, 76 pages: number, 77 rtl: boolean, 78 ): { left: boolean; right: boolean } { 79 const off = (step: Step): boolean => page + step < 0 || page + step >= pages; 80 return { left: off(arrowStep('left', rtl)), right: off(arrowStep('right', rtl)) }; 81 } 82 83 /** 84 * The index of the page that the user typed: a number from 1 to `pages`. 85 * NFKC reads full-width digits. Null for other text. 86 */ 87 export function parsePage(text: string, pages: number): number | null { 88 const t = text.normalize('NFKC').trim(); 89 if (!/^[0-9]+$/.test(t)) return null; 90 const n = Number(t); 91 return n >= 1 && n <= pages ? n - 1 : null; 92 } 93 94 /** The reading direction before the user sets one: right to left for vertical Japanese. */ 95 export function defaultRtl(ocrLang: string): boolean { 96 return ocrLang.includes('vert'); 97 }