Recently Written · git

scan-subread

PDF reader that follows an audiobook with the SubRead suite

git clone https://github.com/equwal/scan-subread

Log | Files | Refs


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 }