@quran.ws/engine 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/MIT.txt +21 -0
- package/README.md +78 -3
- package/docs/API.md +586 -57
- package/docs/LITE-PASSAGES.md +54 -0
- package/package.json +10 -2
- package/web/index.mjs +1 -1
- package/web/lite-passage-geometry.mjs +174 -0
- package/web/lite-passage.mjs +199 -0
- package/web/lite-path.mjs +16 -0
- package/web/lite.mjs +465 -0
- package/web/qvp.js +457 -130
- package/web/qvp_ffi.wasm +0 -0
package/web/qvp.js
CHANGED
|
@@ -6,14 +6,26 @@
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
8
|
const NONE = 0xffffffff;
|
|
9
|
+
// the reader's zoom control: what a pinch does to the page
|
|
10
|
+
const ZOOM_MODE = { stepped: 0, continuous: 1, magnify: 2 };
|
|
11
|
+
const ZOOM_MODE_NAME = ['stepped', 'continuous', 'magnify'];
|
|
12
|
+
/** The defaults every wrapper shares (QVP_DEFAULT_* in qvp.h; the parity check compares them). */
|
|
13
|
+
const DEFAULTS = { INK: 0x231f20ff, HIGHLIGHT_INK: 0x1a73e8ff, HIGHLIGHT_BAND: 0xd6a3264d, HIGHLIGHT_PAD_X: 1.2, HIGHLIGHT_PAD_Y: 0, HIGHLIGHT_SEAM: 0.25,
|
|
14
|
+
SELECTION_BAND: 0x2d6fd640, GAP_BIAS: 0.6, TAP_DISTANCE: 6, GRID_LINES: 15, ASPECT_SLACK: 1.15, MASK_BLOCK: 0xd9d4c8ff, MASK_PAD: 0.6, MASK_RADIUS: 0.8,
|
|
15
|
+
REVEAL_LIT: 1, REVEAL_GREY: 0xc9c4b8ff, CROP_PAD: 2,
|
|
16
|
+
MIN_ZOOM: 0.5, MAX_ZOOM: 12, ZOOMED_THRESHOLD: 1.02, SWIPE_AXIS_RATIO: 1.5, SWIPE_DISTANCE: 40, SWIPE_VELOCITY: 500,
|
|
17
|
+
ZOOM_SNAP_HYSTERESIS: 0.03, ZOOM_QUANTUM: 0.01 };
|
|
9
18
|
const KIND = { BODY: 0, MARK: 1, AYAH_NUMBER: 2, AYAH_MARK_ORNAMENT: 3, HEADER_INK: 4, ORNAMENT: 5, PAGE_NUMBER: 6, RUNNING_HEAD: 7, OTHER: 255 };
|
|
10
19
|
const FAMILY = { NONE: 0, DIACRITIC: 1, TANWIN: 2, DOTS: 3, WAQF: 4, SIFR: 5, SAJDAH: 6, READING_SIGN: 7 };
|
|
11
20
|
const CATEGORY = { NONE: 0, HARAKAH: 1, TANWIN: 2, LETTER_DOT: 3, ORTHOGRAPHIC: 4, DABT: 5, WAQF: 6, READING_SIGN: 7, STANDALONE: 8 };
|
|
12
|
-
const
|
|
21
|
+
const DECORATION = { AYAH_MARK: 0, SURAH_NAME: 1, BASMALAH: 2, DIVISION_MARK: 3, SAJDAH_MARK: 4, PAGE_NUMBER: 5, RUNNING_HEAD: 6, OTHER: 255 };
|
|
13
22
|
const FORM = { rasm_uthmani: 0, rasm_imlai: 1, qpc: 2, rasm: 3, search: 4 };
|
|
14
23
|
const LAYER = { BASE: 0, THEME: 10, HIGHLIGHT: 50, SELECTION: 60, TOP: 100 };
|
|
15
|
-
|
|
16
|
-
const
|
|
24
|
+
// The engine's name tables (QVP_NAMES_*), read from the engine when it is initialised.
|
|
25
|
+
const NAMES_TABLE = { mark: 0, kind: 1, family: 2, category: 3, decoration: 4, division: 5, place: 6 };
|
|
26
|
+
let NAMES = null;
|
|
27
|
+
const names = table => { if (!NAMES) throw new Error('QvpEngine.init() first: names come from the engine'); return NAMES[table]; };
|
|
28
|
+
const idOf = (table, v) => { if (typeof v !== 'string') return v; const i = names(table).indexOf(v); return i < 0 ? 255 : i; };
|
|
17
29
|
|
|
18
30
|
// colours: numbers are 0xRRGGBBAA; strings '#rgb', '#rrggbb', '#rrggbbaa'
|
|
19
31
|
function rgba(c, alpha) {
|
|
@@ -28,35 +40,35 @@
|
|
|
28
40
|
|
|
29
41
|
// Selectors (what a style rule applies to)
|
|
30
42
|
const Sel = {
|
|
31
|
-
page: () => ({
|
|
32
|
-
path: i => ({
|
|
33
|
-
wordPath: (w, n) => ({
|
|
34
|
-
wordMark: (w, n) => ({
|
|
35
|
-
wordMarkNamed: (w, mark, n = 0) => ({
|
|
36
|
-
wordBody: w => ({
|
|
37
|
-
wordMarks: w => ({
|
|
38
|
-
word: w => ({
|
|
39
|
-
ayah: (s, a) => ({
|
|
40
|
-
line: n => ({
|
|
41
|
-
mark: m => ({
|
|
42
|
-
category: c => ({
|
|
43
|
-
family: f => ({
|
|
44
|
-
kind: k => ({
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
page: () => ({ selector: 0 }),
|
|
44
|
+
path: i => ({ selector: 1, a: i }),
|
|
45
|
+
wordPath: (w, n) => ({ selector: 2, a: w, b: n }),
|
|
46
|
+
wordMark: (w, n) => ({ selector: 3, a: w, b: n }), // nth mark of the word (0-based)
|
|
47
|
+
wordMarkNamed: (w, mark, n = 0) => ({ selector: 4, a: w, b: markId(mark), c: n }),
|
|
48
|
+
wordBody: w => ({ selector: 5, a: w }),
|
|
49
|
+
wordMarks: w => ({ selector: 6, a: w }),
|
|
50
|
+
word: w => ({ selector: 7, a: w }),
|
|
51
|
+
ayah: (s, a) => ({ selector: 8, a: s, b: a }),
|
|
52
|
+
line: n => ({ selector: 9, a: n }),
|
|
53
|
+
mark: m => ({ selector: 10, a: markId(m) }),
|
|
54
|
+
category: c => ({ selector: 11, a: idOf('category', c) }),
|
|
55
|
+
family: f => ({ selector: 12, a: idOf('family', f) }),
|
|
56
|
+
kind: k => ({ selector: 13, a: idOf('kind', k) }),
|
|
57
|
+
decoration: k => ({ selector: 14, a: idOf('decoration', k) }),
|
|
58
|
+
decorationIndex: i => ({ selector: 15, a: i }),
|
|
47
59
|
};
|
|
48
|
-
const markId = m => (
|
|
60
|
+
const markId = m => idOf('mark', m);
|
|
49
61
|
|
|
50
62
|
// Targets (what resolves to a word list). Strings: 'page', '2:255', '2:255:3', '2:255-257', 'line:7', 'surah:2'
|
|
51
63
|
const T = {
|
|
52
|
-
page: () => ({
|
|
53
|
-
word: i => ({
|
|
54
|
-
words: ws => ({
|
|
55
|
-
ayah: (s, a) => ({
|
|
56
|
-
ayahRange: (s, a, b) => ({
|
|
57
|
-
line: n => ({
|
|
58
|
-
surah: s => ({
|
|
59
|
-
range: (a, b) => ({
|
|
64
|
+
page: () => ({ target: 0 }),
|
|
65
|
+
word: i => ({ target: 1, a: i }),
|
|
66
|
+
words: ws => ({ target: 2, words: ws }),
|
|
67
|
+
ayah: (s, a) => ({ target: 3, a: s, b: a }),
|
|
68
|
+
ayahRange: (s, a, b) => ({ target: 4, a: s, b: a, c: b }),
|
|
69
|
+
line: n => ({ target: 5, a: n }),
|
|
70
|
+
surah: s => ({ target: 6, a: s }),
|
|
71
|
+
range: (a, b) => ({ target: 7, a, b }),
|
|
60
72
|
};
|
|
61
73
|
|
|
62
74
|
class QvpEngine {
|
|
@@ -69,27 +81,63 @@
|
|
|
69
81
|
this.mem = this.ex.memory;
|
|
70
82
|
this.scratch = this.ex.qvp_alloc(1 << 16); // 64 KB for outputs
|
|
71
83
|
this.scratch2 = this.ex.qvp_alloc(1 << 16); // 64 KB for inputs
|
|
84
|
+
this._buf = 0; this._bufLen = 0; // grown on demand for the bigger reads
|
|
72
85
|
this.td = new TextDecoder();
|
|
73
86
|
this.te = new TextEncoder();
|
|
87
|
+
// Load every name table from the engine once; Sel and markId read them.
|
|
88
|
+
NAMES = Object.fromEntries(Object.entries(NAMES_TABLE).map(([k, t]) => [k, this.names(t)]));
|
|
74
89
|
}
|
|
90
|
+
/** All names of a table (QVP_NAMES_* id or key), index = id. */
|
|
91
|
+
names(table) { const t = typeof table === 'string' ? NAMES_TABLE[table] : table; const n = this.ex.qvp_name_count(t); return Array.from({ length: n }, (_, i) => this.name(t, i)); }
|
|
92
|
+
nameCount(table) { return this.ex.qvp_name_count(typeof table === 'string' ? NAMES_TABLE[table] : table); }
|
|
93
|
+
name(table, id) { this.ex.qvp_name(typeof table === 'string' ? NAMES_TABLE[table] : table, id, this.scratch); return this.qstr(this.scratch); }
|
|
94
|
+
nameId(table, s) { const [p, n] = this.putStr(s); return this.ex.qvp_name_id(typeof table === 'string' ? NAMES_TABLE[table] : table, p, n); }
|
|
75
95
|
dv() { return new DataView(this.mem.buffer); }
|
|
76
96
|
str(ptr, len) { return this.td.decode(new Uint8Array(this.mem.buffer, ptr, len)); }
|
|
77
97
|
qstr(at) { const d = this.dv(); return this.str(d.getUint32(at, true), d.getUint32(at + 4, true)); }
|
|
98
|
+
/** the reader's pan and zoom: {scale, offsetX, offsetY} in, the same out. The engine owns
|
|
99
|
+
* the arithmetic so every platform pinches alike; the host owns only the gesture. */
|
|
100
|
+
_putView(v, at) { const d = this.dv(); d.setFloat32(at, v.scale, true); d.setFloat32(at + 4, v.offsetX, true); d.setFloat32(at + 8, v.offsetY, true); return at; }
|
|
101
|
+
_getView(at) { const d = this.dv(); return { scale: d.getFloat32(at, true), offsetX: d.getFloat32(at + 4, true), offsetY: d.getFloat32(at + 8, true) }; }
|
|
102
|
+
/** zoom about a point of the viewport (the point between two fingers), clamped */
|
|
103
|
+
viewZoomAbout(view, focalX, focalY, factor, { min = 0, max = 0 } = {}) {
|
|
104
|
+
const i = this._putView(view, this.scratch2 + 32768);
|
|
105
|
+
this.ex.qvp_view_zoom_about(i, focalX, focalY, factor, min, max, this.scratch);
|
|
106
|
+
return this._getView(this.scratch);
|
|
107
|
+
}
|
|
108
|
+
/** move by a drag, in viewport px */
|
|
109
|
+
viewPan(view, dx, dy) { const i = this._putView(view, this.scratch2 + 32768); this.ex.qvp_view_pan(i, dx, dy, this.scratch); return this._getView(this.scratch); }
|
|
110
|
+
/** hold the content against the viewport: centre what does not fill it, cover what does */
|
|
111
|
+
viewClamp(view, contentW, contentH, viewportW, viewportH) { const i = this._putView(view, this.scratch2 + 32768); this.ex.qvp_view_clamp(i, contentW, contentH, viewportW, viewportH, this.scratch); return this._getView(this.scratch); }
|
|
112
|
+
/** +1 or -1 when a released drag is a page swipe, 0 when it is not */
|
|
113
|
+
viewSwipe(dx, dy, vx = 0, vy = 0) { return this.ex.qvp_view_swipe(dx, dy, vx, vy); }
|
|
114
|
+
/** how many pages a released drag turns, in reading order: a mushaf runs right to left, so a
|
|
115
|
+
* flick to the right is the next page. Add it to the page you are on. */
|
|
116
|
+
swipePages(dx, dy, vx = 0, vy = 0) { return this.ex.qvp_swipe_pages(dx, dy, vx, vy); }
|
|
117
|
+
/** a scratch buffer of at least `len` bytes, kept between calls */
|
|
118
|
+
buf(len) { if (len > this._bufLen) { this._buf = this.ex.qvp_alloc(len); this._bufLen = len; } return this._buf; }
|
|
78
119
|
/** write a JS string into scratch2 at offset; returns [ptr, len] */
|
|
79
120
|
putStr(s, off = 0) { const b = this.te.encode(s); new Uint8Array(this.mem.buffer, this.scratch2 + off, b.length).set(b); return [this.scratch2 + off, b.length]; }
|
|
80
121
|
putU32(arr, off = 0) { const p = this.scratch2 + off; new Uint32Array(this.mem.buffer, p, arr.length).set(arr); return p; }
|
|
81
122
|
nameOf(fn, v) { this.ex[fn](v, this.scratch); return this.qstr(this.scratch); }
|
|
82
|
-
markName(m) { return
|
|
123
|
+
markName(m) { return this.nameOf('qvp_mark_name', m); }
|
|
83
124
|
familyName(f) { return this.nameOf('qvp_family_name', f); }
|
|
84
125
|
kindName(k) { return this.nameOf('qvp_kind_name', k); }
|
|
85
126
|
categoryName(c) { return this.nameOf('qvp_category_name', c); }
|
|
86
127
|
markCategory(m) { return this.ex.qvp_mark_category(markId(m)); }
|
|
128
|
+
markFromName(s) { const [p, n] = this.putStr(s); return this.ex.qvp_mark_from_name(p, n); }
|
|
129
|
+
/** The engine version, e.g. '0.2.0'. */
|
|
130
|
+
version() { this.ex.qvp_version(this.scratch); return this.qstr(this.scratch); }
|
|
131
|
+
/** The page format version the engine reads. */
|
|
132
|
+
formatVersion() { return this.ex.qvp_format_version(); }
|
|
133
|
+
/** The engine's name, `qvp` (a NUL-terminated C string in wasm memory). */
|
|
134
|
+
engineName() { const p = this.ex.qvp_engine_name(), u = new Uint8Array(this.mem.buffer); let n = 0; while (u[p + n]) n++; return this.str(p, n); }
|
|
87
135
|
/** Arabic text tools */
|
|
88
136
|
strip(s) { return this._arabic(0, s); }
|
|
89
137
|
fold(s) { return this._arabic(1, s); }
|
|
90
138
|
normalize(s) { return this._arabic(2, s); }
|
|
91
139
|
looseKey(s) { return this._arabic(3, s); }
|
|
92
|
-
_arabic(
|
|
140
|
+
_arabic(op, s) { const [p, n] = this.putStr(s); this.ex.qvp_arabic(op, p, n, this.scratch); return this.qstr(this.scratch); }
|
|
93
141
|
loadPage(bytes) {
|
|
94
142
|
const p = this.ex.qvp_alloc(bytes.length);
|
|
95
143
|
new Uint8Array(this.mem.buffer, p, bytes.length).set(bytes);
|
|
@@ -106,15 +154,13 @@
|
|
|
106
154
|
if (!h) throw new Error('qvp_atlas_load failed');
|
|
107
155
|
return new QvpAtlas(this, h);
|
|
108
156
|
}
|
|
109
|
-
gapToFill(pageW, pageH, lines, viewW, viewH, max = 0) { return this.ex.qvp_gap_to_fill(pageW, pageH, lines, viewW, viewH, max); }
|
|
110
|
-
wastedFraction(pageW, pageH, viewW, viewH) { return this.ex.qvp_wasted_fraction(pageW, pageH, viewW, viewH); }
|
|
111
157
|
}
|
|
112
158
|
|
|
113
159
|
// struct writers (wasm32 layouts, see qvp.h)
|
|
114
160
|
function writeTarget(e, at, t) {
|
|
115
161
|
if (typeof t === 'string') t = parseTarget(t);
|
|
116
162
|
const d = e.dv();
|
|
117
|
-
d.setUint8(at, t.
|
|
163
|
+
d.setUint8(at, t.target); d.setUint32(at + 4, t.a >>> 0 || 0, true); d.setUint32(at + 8, t.b >>> 0 || 0, true); d.setUint32(at + 12, t.c >>> 0 || 0, true);
|
|
118
164
|
let wp = 0, wn = 0;
|
|
119
165
|
if (t.words) { wp = e.putU32(Uint32Array.from(t.words), 1024); wn = t.words.length; }
|
|
120
166
|
d.setUint32(at + 16, wp, true); d.setUint32(at + 20, wn, true);
|
|
@@ -126,16 +172,16 @@
|
|
|
126
172
|
if ((m = /^line:(\d+)$/.exec(s))) return T.line(+m[1]);
|
|
127
173
|
if ((m = /^surah:(\d+)$/.exec(s))) return T.surah(+m[1]);
|
|
128
174
|
if ((m = /^(\d+):(\d+)-(\d+)$/.exec(s))) return T.ayahRange(+m[1], +m[2], +m[3]);
|
|
129
|
-
if ((m = /^(\d+):(\d+):(\d+)$/.exec(s))) return {
|
|
175
|
+
if ((m = /^(\d+):(\d+):(\d+)$/.exec(s))) return { target: 1, wordKey: [+m[1], +m[2], +m[3]] };
|
|
130
176
|
if ((m = /^(\d+):(\d+)$/.exec(s))) return T.ayah(+m[1], +m[2]);
|
|
131
177
|
throw new Error('bad target ' + s);
|
|
132
178
|
}
|
|
133
179
|
function writeSel(e, at, s) {
|
|
134
180
|
const d = e.dv();
|
|
135
|
-
d.setUint8(at, s.
|
|
181
|
+
d.setUint8(at, s.selector); d.setUint32(at + 4, s.a >>> 0 || 0, true); d.setUint32(at + 8, s.b >>> 0 || 0, true); d.setUint32(at + 12, s.c >>> 0 || 0, true);
|
|
136
182
|
return at;
|
|
137
183
|
}
|
|
138
|
-
const HL_DEFAULT = { mode: 'band', height: '
|
|
184
|
+
const HL_DEFAULT = { mode: 'band', height: 'lineSpacing', ink: DEFAULTS.HIGHLIGHT_INK, band: DEFAULTS.HIGHLIGHT_BAND, padX: DEFAULTS.HIGHLIGHT_PAD_X, padY: DEFAULTS.HIGHLIGHT_PAD_Y, radius: 0, seam: DEFAULTS.HIGHLIGHT_SEAM, ms: 0, layer: LAYER.HIGHLIGHT };
|
|
139
185
|
function writeHl(e, at, st) {
|
|
140
186
|
st = { ...HL_DEFAULT, ...st };
|
|
141
187
|
const d = e.dv();
|
|
@@ -158,7 +204,7 @@
|
|
|
158
204
|
ex.qvp_page_info(handle, s);
|
|
159
205
|
let d = engine.dv();
|
|
160
206
|
this.width = d.getFloat32(s, true); this.height = d.getFloat32(s + 4, true); this.page = d.getUint32(s + 8, true);
|
|
161
|
-
this.nLines = d.getUint32(s + 12, true); this.nAyahs = d.getUint32(s + 16, true); this.nWords = d.getUint32(s + 20, true); this.nPaths = d.getUint32(s + 24, true); this.
|
|
207
|
+
this.nLines = d.getUint32(s + 12, true); this.nAyahs = d.getUint32(s + 16, true); this.nWords = d.getUint32(s + 20, true); this.nPaths = d.getUint32(s + 24, true); this.nDecorations = d.getUint32(s + 28, true);
|
|
162
208
|
ex.qvp_geometry(handle, s);
|
|
163
209
|
d = engine.dv();
|
|
164
210
|
const opsPtr = d.getUint32(s, true), opsLen = d.getUint32(s + 4, true), ptsPtr = d.getUint32(s + 8, true), ptsLen = d.getUint32(s + 12, true), tabPtr = d.getUint32(s + 16, true), nPaths = d.getUint32(s + 20, true);
|
|
@@ -169,10 +215,10 @@
|
|
|
169
215
|
this.words = Array.from({ length: this.nWords }, (_, i) => this._word(i));
|
|
170
216
|
this.ayahs = Array.from({ length: this.nAyahs }, (_, i) => this._ayah(i));
|
|
171
217
|
this.lines = Array.from({ length: this.nLines }, (_, i) => this._line(i));
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
218
|
+
this.decorations = Array.from({ length: this.nDecorations }, (_, i) => this._deco(i));
|
|
219
|
+
this.lineSpacing = ex.qvp_page_line_spacing(handle);
|
|
174
220
|
this.currentLayout = null;
|
|
175
|
-
this._defaultInk =
|
|
221
|
+
this._defaultInk = DEFAULTS.INK;
|
|
176
222
|
}
|
|
177
223
|
free() { this.e.ex.qvp_page_free(this.h); this.h = 0; }
|
|
178
224
|
|
|
@@ -211,25 +257,25 @@
|
|
|
211
257
|
_word(i) {
|
|
212
258
|
const ex = this.e.ex, s = this.e.scratch; if (!ex.qvp_word_info(this.h, i, s)) return null;
|
|
213
259
|
const d = this.e.dv();
|
|
214
|
-
return {
|
|
260
|
+
return { index: i, surah: d.getUint16(s, true), ayah: d.getUint16(s + 2, true), word: d.getUint16(s + 4, true), line: d.getUint16(s + 6, true), ayahIndex: d.getUint32(s + 8, true), lineIndex: d.getUint32(s + 12, true),
|
|
215
261
|
x0: d.getFloat32(s + 16, true), y0: d.getFloat32(s + 20, true), x1: d.getFloat32(s + 24, true), y1: d.getFloat32(s + 28, true), text: this.e.qstr(s + 32), firstPath: d.getUint32(s + 40, true), nPaths: d.getUint32(s + 44, true) };
|
|
216
262
|
}
|
|
217
263
|
_ayah(i) {
|
|
218
264
|
const ex = this.e.ex, s = this.e.scratch; if (!ex.qvp_ayah_info(this.h, i, s)) return null;
|
|
219
265
|
const d = this.e.dv();
|
|
220
|
-
return {
|
|
221
|
-
firstWord: d.getUint32(s + 12, true), nWords: d.getUint32(s + 16, true),
|
|
266
|
+
return { index: i, surah: d.getUint16(s, true), ayah: d.getUint16(s + 2, true), fragment: d.getUint8(s + 4), fragments: d.getUint8(s + 5), flags: d.getUint8(s + 6), rubuAlHizb: d.getUint16(s + 8, true),
|
|
267
|
+
firstWord: d.getUint32(s + 12, true), nWords: d.getUint32(s + 16, true), ayahMarkDecoration: d.getUint32(s + 20, true), x0: d.getFloat32(s + 24, true), y0: d.getFloat32(s + 28, true), x1: d.getFloat32(s + 32, true), y1: d.getFloat32(s + 36, true) };
|
|
222
268
|
}
|
|
223
269
|
_line(i) {
|
|
224
270
|
const ex = this.e.ex, s = this.e.scratch; if (!ex.qvp_line_info(this.h, i, s)) return null;
|
|
225
271
|
const d = this.e.dv();
|
|
226
|
-
return {
|
|
272
|
+
return { index: i, lineNumber: d.getUint8(s), isHeader: !!d.getUint8(s + 1), firstWord: d.getUint32(s + 4, true), nWords: d.getUint32(s + 8, true), x0: d.getFloat32(s + 12, true), y0: d.getFloat32(s + 16, true), x1: d.getFloat32(s + 20, true), y1: d.getFloat32(s + 24, true),
|
|
227
273
|
bandY0: d.getFloat32(s + 28, true), bandY1: d.getFloat32(s + 32, true), centre: d.getFloat32(s + 36, true) };
|
|
228
274
|
}
|
|
229
275
|
_deco(i) {
|
|
230
|
-
const ex = this.e.ex, s = this.e.scratch; if (!ex.
|
|
276
|
+
const ex = this.e.ex, s = this.e.scratch; if (!ex.qvp_decoration_info(this.h, i, s)) return null;
|
|
231
277
|
const d = this.e.dv();
|
|
232
|
-
return {
|
|
278
|
+
return { index: i, decoration: d.getUint8(s), surah: d.getUint16(s + 2, true), ayah: d.getUint16(s + 4, true), line: d.getUint32(s + 8, true), x0: d.getFloat32(s + 12, true), y0: d.getFloat32(s + 16, true), x1: d.getFloat32(s + 20, true), y1: d.getFloat32(s + 24, true),
|
|
233
279
|
text: this.e.qstr(s + 28), firstPath: d.getUint32(s + 36, true), nPaths: d.getUint32(s + 40, true) };
|
|
234
280
|
}
|
|
235
281
|
wordForm(i, form = 'rasm_uthmani') { const s = this.e.scratch; if (!this.e.ex.qvp_word_form(this.h, i, FORM[form] ?? 0, s)) return ''; return this.e.qstr(s); }
|
|
@@ -240,37 +286,55 @@
|
|
|
240
286
|
if (t instanceof Array) t = T.words(t);
|
|
241
287
|
return writeTarget(this.e, this.e.scratch2 + 4096, t);
|
|
242
288
|
}
|
|
243
|
-
|
|
289
|
+
targetWords(t) { const n = this.e.ex.qvp_target_words(this.h, this._target(t), this.e.scratch, 16384); return Array.from(new Uint32Array(this.e.mem.buffer, this.e.scratch, Math.min(n, 16384))); }
|
|
244
290
|
|
|
245
291
|
// ── metadata ──
|
|
246
292
|
surahs() {
|
|
247
|
-
const ex = this.e.ex, s = this.e.scratch, n = ex.
|
|
293
|
+
const ex = this.e.ex, s = this.e.scratch, n = ex.qvp_surah_count(this.h), out = [];
|
|
248
294
|
for (let i = 0; i < n; i++) { ex.qvp_surah_at(this.h, i, s); const d = this.e.dv();
|
|
249
|
-
out.push({ number: d.getUint16(s, true), ayahCount: d.getUint16(s + 2, true), hasBanner: !!d.getUint8(s + 4), hasBasmalah: !!d.getUint8(s + 5), place:
|
|
295
|
+
out.push({ number: d.getUint16(s, true), ayahCount: d.getUint16(s + 2, true), hasBanner: !!d.getUint8(s + 4), hasBasmalah: !!d.getUint8(s + 5), place: names('place')[d.getUint8(s + 6)] || '', bannerDecoration: d.getUint32(s + 8, true), arabic: this.e.qstr(s + 12), latin: this.e.qstr(s + 20), english: this.e.qstr(s + 28) }); }
|
|
250
296
|
return out;
|
|
251
297
|
}
|
|
252
298
|
divisions() {
|
|
253
299
|
const ex = this.e.ex, s = this.e.scratch, n = ex.qvp_divisions(this.h, s, 64), d = this.e.dv(), out = [];
|
|
254
|
-
for (let i = 0; i < Math.min(n, 64); i++) { const o = s + i * 12; out.push({
|
|
300
|
+
for (let i = 0; i < Math.min(n, 64); i++) { const o = s + i * 12; out.push({ division: names('division')[d.getUint8(o)], line: d.getUint8(o + 1), number: d.getUint16(o + 2, true), surah: d.getUint16(o + 4, true), ayah: d.getUint16(o + 6, true), ayahIndex: d.getUint32(o + 8, true) }); }
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
/** the surah headings in page units: the box a frame fills, and the title ink in it */
|
|
304
|
+
surahHeaders() {
|
|
305
|
+
const ex = this.e.ex, s = this.e.scratch, n = ex.qvp_surah_headers(this.h, s, 32), d = this.e.dv(), out = [];
|
|
306
|
+
for (let i = 0; i < Math.min(n, 32); i++) { const o = s + i * 44; out.push({ decoration: d.getUint32(o, true), surah: d.getUint16(o + 4, true), line: d.getUint32(o + 8, true), x0: d.getFloat32(o + 12, true), y0: d.getFloat32(o + 16, true), x1: d.getFloat32(o + 20, true), y1: d.getFloat32(o + 24, true), titleX0: d.getFloat32(o + 28, true), titleY0: d.getFloat32(o + 32, true), titleX1: d.getFloat32(o + 36, true), titleY1: d.getFloat32(o + 40, true) }); }
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
/** the same, in viewport px through the current layout: what a host draws its own frame around */
|
|
310
|
+
surahHeadersView() {
|
|
311
|
+
const ex = this.e.ex, s = this.e.scratch, n = ex.qvp_surah_headers_view(this.h, s, 32), d = this.e.dv(), out = [];
|
|
312
|
+
for (let i = 0; i < Math.min(n, 32); i++) { const o = s + i * 44; out.push({ decoration: d.getUint32(o, true), surah: d.getUint16(o + 4, true), line: d.getUint32(o + 8, true), x0: d.getFloat32(o + 12, true), y0: d.getFloat32(o + 16, true), x1: d.getFloat32(o + 20, true), y1: d.getFloat32(o + 24, true), titleX0: d.getFloat32(o + 28, true), titleY0: d.getFloat32(o + 32, true), titleX1: d.getFloat32(o + 36, true), titleY1: d.getFloat32(o + 40, true) }); }
|
|
255
313
|
return out;
|
|
256
314
|
}
|
|
257
315
|
ayahMarks() {
|
|
258
316
|
const ex = this.e.ex, s = this.e.scratch, n = ex.qvp_ayah_marks(this.h, s, 128), d = this.e.dv(), out = [];
|
|
259
|
-
for (let i = 0; i < Math.min(n, 128); i++) { const o = s + i * 32; out.push({
|
|
317
|
+
for (let i = 0; i < Math.min(n, 128); i++) { const o = s + i * 32; out.push({ decoration: d.getUint32(o, true), surah: d.getUint16(o + 4, true), ayah: d.getUint16(o + 6, true), line: d.getUint32(o + 8, true), cx: d.getFloat32(o + 12, true), cy: d.getFloat32(o + 16, true), r: d.getFloat32(o + 20, true), ornamentPath: d.getUint32(o + 24, true), numeralPath: d.getUint32(o + 28, true) }); }
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
/** the ayah medallions in viewport px through the current layout: where each is drawn */
|
|
321
|
+
ayahMarksView() {
|
|
322
|
+
const ex = this.e.ex, s = this.e.scratch, n = ex.qvp_ayah_marks_view(this.h, s, 128), d = this.e.dv(), out = [];
|
|
323
|
+
for (let i = 0; i < Math.min(n, 128); i++) { const o = s + i * 32; out.push({ decoration: d.getUint32(o, true), surah: d.getUint16(o + 4, true), ayah: d.getUint16(o + 6, true), line: d.getUint32(o + 8, true), cx: d.getFloat32(o + 12, true), cy: d.getFloat32(o + 16, true), r: d.getFloat32(o + 20, true), ornamentPath: d.getUint32(o + 24, true), numeralPath: d.getUint32(o + 28, true) }); }
|
|
260
324
|
return out;
|
|
261
325
|
}
|
|
262
326
|
rosettes() {
|
|
263
327
|
const ex = this.e.ex, s = this.e.scratch, n = ex.qvp_rosettes(this.h, s, 32), d = this.e.dv(), out = [];
|
|
264
|
-
for (let i = 0; i < Math.min(n, 32); i++) { const o = s + i * 20; out.push({
|
|
328
|
+
for (let i = 0; i < Math.min(n, 32); i++) { const o = s + i * 20; out.push({ decoration: d.getUint32(o, true), surah: d.getUint16(o + 4, true), ayah: d.getUint16(o + 6, true), juz: d.getUint16(o + 8, true), hizb: d.getUint16(o + 10, true), nisf: d.getUint16(o + 12, true), rubuAlHizb: d.getUint16(o + 14, true), rubuAlHizbInHizb: d.getUint16(o + 16, true) }); }
|
|
265
329
|
return out;
|
|
266
330
|
}
|
|
267
331
|
sajdahs() {
|
|
268
332
|
const ex = this.e.ex, s = this.e.scratch, n = ex.qvp_sajdahs(this.h, s, 16), d = this.e.dv(), out = [];
|
|
269
|
-
for (let i = 0; i < Math.min(n, 16); i++) { const o = s + i * 12; out.push({
|
|
333
|
+
for (let i = 0; i < Math.min(n, 16); i++) { const o = s + i * 12; out.push({ decoration: d.getUint32(o, true), surah: d.getUint16(o + 4, true), ayah: d.getUint16(o + 6, true), signPath: d.getUint32(o + 8, true) }); }
|
|
270
334
|
return out;
|
|
271
335
|
}
|
|
272
336
|
ayahKeys() { const n = this.e.ex.qvp_ayah_keys(this.h, this.e.scratch, 256); const v = new Uint32Array(this.e.mem.buffer, this.e.scratch, Math.min(n, 256)); return Array.from(v, k => [k >>> 16, k & 0xffff]); }
|
|
273
|
-
ayahWordCount(surah, ayah) { const n = this.e.ex.qvp_ayah_word_count(this.h, surah, ayah, this.e.scratch); return { count: n,
|
|
337
|
+
ayahWordCount(surah, ayah) { const n = this.e.ex.qvp_ayah_word_count(this.h, surah, ayah, this.e.scratch); return { count: n, isComplete: !!this.e.dv().getUint8(this.e.scratch) }; }
|
|
274
338
|
/** words for n recitation segments, or null when the counts disagree (follow the ayah whole) */
|
|
275
339
|
reciteMap(surah, ayah, nSegments) { const n = this.e.ex.qvp_recite_map(this.h, surah, ayah, nSegments, this.e.scratch, 4096); return n < 0 ? null : Array.from(new Uint32Array(this.e.mem.buffer, this.e.scratch, n)); }
|
|
276
340
|
wordLabel(i) { this.e.ex.qvp_word_label(this.h, i, this.e.scratch); return this.e.qstr(this.e.scratch); }
|
|
@@ -279,14 +343,14 @@
|
|
|
279
343
|
// ── text & search ──
|
|
280
344
|
text(target = 'page', { form = 'rasm_uthmani', wordSep = ' ', lineSep = '\n' } = {}) {
|
|
281
345
|
const [wp, wn] = this.e.putStr(wordSep, 0), [lp, ln] = this.e.putStr(lineSep, 256);
|
|
282
|
-
this.e.ex.
|
|
346
|
+
this.e.ex.qvp_text(this.h, this._target(target), FORM[form] ?? 0, wp, wn, lp, ln, this.e.scratch);
|
|
283
347
|
return this.e.qstr(this.e.scratch);
|
|
284
348
|
}
|
|
285
|
-
search(query, { form = 'search', mode = 'includes', normalize = true,
|
|
349
|
+
search(query, { form = 'search', mode = 'includes', normalize = true, looseMatch = true, limit = 0 } = {}) {
|
|
286
350
|
const [qp, qn] = this.e.putStr(query);
|
|
287
|
-
const n = this.e.ex.qvp_search(this.h, qp, qn, FORM[form] ?? 4, { includes: 0, exact: 1, prefix: 2 }[mode] ?? 0, normalize ? 1 : 0,
|
|
351
|
+
const n = this.e.ex.qvp_search(this.h, qp, qn, FORM[form] ?? 4, { includes: 0, exact: 1, prefix: 2 }[mode] ?? 0, normalize ? 1 : 0, looseMatch ? 1 : 0, limit, this.e.scratch, 1024);
|
|
288
352
|
const d = this.e.dv(), out = [];
|
|
289
|
-
for (let i = 0; i < Math.min(n, 1024); i++) { const o = this.e.scratch + i * 12; const w = d.getUint32(o, true); out.push({ word: w, index: d.getUint32(o + 4, true),
|
|
353
|
+
for (let i = 0; i < Math.min(n, 1024); i++) { const o = this.e.scratch + i * 12; const w = d.getUint32(o, true); out.push({ word: w, index: d.getUint32(o + 4, true), isLooseMatch: !!d.getUint8(o + 8), wordKey: this.wordKey(w), text: this.words[w].text }); }
|
|
290
354
|
return out;
|
|
291
355
|
}
|
|
292
356
|
wordKey(i) { const w = this.words[i]; return `${w.surah}:${w.ayah}:${w.word}`; }
|
|
@@ -296,42 +360,245 @@
|
|
|
296
360
|
citation(words) { const p = this.e.putU32(Uint32Array.from(words)); this.e.ex.qvp_citation(this.h, p, words.length, this.e.scratch); return this.e.qstr(this.e.scratch); }
|
|
297
361
|
|
|
298
362
|
// ── hit testing ──
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
363
|
+
hitTestExact(x, y) { return this.e.ex.qvp_hit_test_exact(this.h, x, y, this.e.scratch) ? this._hit(this.e.scratch) : null; }
|
|
364
|
+
hitTestExactView(viewX, viewY) { return this.e.ex.qvp_hit_test_exact_view(this.h, viewX, viewY, this.e.scratch) ? this._hit(this.e.scratch) : null; }
|
|
365
|
+
_hitOpt(o) { const d = this.e.dv(), at = this.e.scratch2 + 8192; d.setFloat32(at, o.maxDistance ?? 0, true); d.setFloat32(at + 4, o.gapBias ?? 0.6, true); d.setUint8(at + 8, o.preferExact === false ? 0 : 1); return at; }
|
|
366
|
+
/** the one hit shape for every hit test; the exact variants report distance 0 and isExact */
|
|
367
|
+
_hit(s) { const d = this.e.dv(); const f = v => (v === NONE ? -1 : v); const h = { word: f(d.getUint32(s, true)), path: f(d.getUint32(s + 4, true)), decoration: f(d.getUint32(s + 8, true)), line: f(d.getUint32(s + 12, true)), distance: d.getFloat32(s + 16, true), isExact: !!d.getUint8(s + 20) }; if (h.word >= 0) { h.wordKey = this.wordKey(h.word); const w = this.words[h.word]; h.ayahKey = `${w.surah}:${w.ayah}`; } return h; }
|
|
304
368
|
/** gap-aware: every point on a printed line resolves to the word the user meant */
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
369
|
+
hitTest(x, y, opt = {}) { return this.e.ex.qvp_hit_test(this.h, x, y, this._hitOpt(opt), this.e.scratch) ? this._hit(this.e.scratch) : null; }
|
|
370
|
+
hitTestView(viewX, viewY, opt = {}) { return this.e.ex.qvp_hit_test_view(this.h, viewX, viewY, this._hitOpt(opt), this.e.scratch) ? this._hit(this.e.scratch) : null; }
|
|
371
|
+
/** the hit boxes in viewport px through the current layout: what a tap is resolved with */
|
|
372
|
+
hitAreasView(gapBias = DEFAULTS.GAP_BIAS) { const n = this.e.ex.qvp_hit_areas_view(this.h, gapBias, this.e.scratch, 1024), d = this.e.dv(), out = []; for (let i = 0; i < Math.min(n, 1024); i++) { const o = this.e.scratch + i * 40; out.push({ word: d.getUint32(o, true), line: d.getUint32(o + 4, true), x0: d.getFloat32(o + 8, true), y0: d.getFloat32(o + 12, true), x1: d.getFloat32(o + 16, true), y1: d.getFloat32(o + 20, true), inkX0: d.getFloat32(o + 24, true), inkY0: d.getFloat32(o + 28, true), inkX1: d.getFloat32(o + 32, true), inkY1: d.getFloat32(o + 36, true) }); } return out; }
|
|
373
|
+
lineBands() { const n = this.e.ex.qvp_line_bands(this.h, this.e.scratch, 64), d = this.e.dv(), out = []; for (let i = 0; i < Math.min(n, 64); i++) { const o = this.e.scratch + i * 28; out.push({ line: d.getUint32(o, true), lineNumber: d.getUint32(o + 4, true), y0: d.getFloat32(o + 8, true), y1: d.getFloat32(o + 12, true), mid: d.getFloat32(o + 16, true), inkY0: d.getFloat32(o + 20, true), inkY1: d.getFloat32(o + 24, true) }); } return out; }
|
|
374
|
+
hitAreas(gapBias = DEFAULTS.GAP_BIAS) { const n = this.e.ex.qvp_hit_areas(this.h, gapBias, this.e.scratch, 1024), d = this.e.dv(), out = []; for (let i = 0; i < Math.min(n, 1024); i++) { const o = this.e.scratch + i * 40; out.push({ word: d.getUint32(o, true), line: d.getUint32(o + 4, true), x0: d.getFloat32(o + 8, true), y0: d.getFloat32(o + 12, true), x1: d.getFloat32(o + 16, true), y1: d.getFloat32(o + 20, true), inkX0: d.getFloat32(o + 24, true), inkY0: d.getFloat32(o + 28, true), inkX1: d.getFloat32(o + 32, true), inkY1: d.getFloat32(o + 36, true) }); } return out; }
|
|
309
375
|
|
|
310
376
|
// ── layout ──
|
|
311
|
-
layout(
|
|
312
|
-
|
|
377
|
+
/** Write a layout spec (QvpLayoutSpec, 76 bytes) at scratch offset `s`. */
|
|
378
|
+
_writeLayoutSpec(spec, s) {
|
|
379
|
+
const d = this.e.dv();
|
|
313
380
|
d.setFloat32(s, spec.viewportW, true); d.setFloat32(s + 4, spec.viewportH, true); d.setFloat32(s + 8, spec.padTop || 0, true); d.setFloat32(s + 12, spec.padBottom || 0, true);
|
|
314
|
-
d.setFloat32(s + 16, spec.padLeft || 0, true); d.setFloat32(s + 20, spec.padRight || 0, true); d.setFloat32(s + 24, spec.lineSpacing ?? 1, true);
|
|
315
|
-
d.
|
|
316
|
-
|
|
317
|
-
|
|
381
|
+
d.setFloat32(s + 16, spec.padLeft || 0, true); d.setFloat32(s + 20, spec.padRight || 0, true); d.setFloat32(s + 24, spec.lineSpacing ?? 1, true);
|
|
382
|
+
d.setUint8(s + 28, spec.fillHeight ? 1 : 0); d.setUint32(s + 32, spec.gridLines || 0, true);
|
|
383
|
+
d.setFloat32(s + 36, spec.cropLeft || 0, true); d.setFloat32(s + 40, spec.cropRight || 0, true); d.setFloat32(s + 44, spec.maxAspectSlack || 0, true);
|
|
384
|
+
const r = spec.reflow;
|
|
385
|
+
d.setFloat32(s + 48, r ? (r.zoom ?? 1) : 0, true);
|
|
386
|
+
// 255 asks the engine for its own default
|
|
387
|
+
const fill = { ragged: 0, justified: 1, centred: 2, centered: 2 }[r && r.fill] ?? 255;
|
|
388
|
+
const breaks = { greedy: 0, even: 1, fitted: 2 }[r && r.breaks] ?? 255;
|
|
389
|
+
d.setUint8(s + 52, fill); d.setUint8(s + 53, breaks); d.setUint8(s + 54, r && r.gaps === 'printed' ? 0 : 1);
|
|
390
|
+
d.setFloat32(s + 56, r ? (r.wordGap ?? 1) : 1, true);
|
|
391
|
+
d.setFloat32(s + 60, r ? (r.maxStretch ?? 0) : 0, true);
|
|
392
|
+
d.setFloat32(s + 64, r && r.relax !== undefined ? r.relax : -1, true);
|
|
393
|
+
// bannerZoom: how big a surah name or basmalah may get against its printed size as the
|
|
394
|
+
// reader zooms; 0 lets it grow with the page until it fills the row.
|
|
395
|
+
d.setFloat32(s + 68, spec.bannerZoom ?? 0, true);
|
|
396
|
+
// Native frames preserve the printed page by default; a host may supply its own.
|
|
397
|
+
d.setUint8(s + 72, spec.surahFrames === false ? 0 : 1);
|
|
398
|
+
}
|
|
399
|
+
/** Leading (page units) that makes the page fill the padded viewport of `spec`; max 0 = unlimited. */
|
|
400
|
+
layoutLineSpacingToFill(spec, max = 0) { const s = this.e.scratch; this._writeLayoutSpec(spec, s); return this.e.ex.qvp_layout_line_spacing_to_fill(this.h, s, max); }
|
|
401
|
+
/** the share of the padded viewport left empty when the page is fitted to width */
|
|
402
|
+
layoutWastedFraction(spec) { const s = this.e.scratch; this._writeLayoutSpec(spec, s); return this.e.ex.qvp_layout_wasted_fraction(this.h, s); }
|
|
403
|
+
/** the page's height laid out for `spec` at the printed pitch, in viewport px: contentH before fill-height adds leading, padding included */
|
|
404
|
+
layoutPrintedHeight(spec) { const s = this.e.scratch; this._writeLayoutSpec(spec, s); return this.e.ex.qvp_layout_printed_height(this.h, s); }
|
|
405
|
+
/** the grid this page is laid out inside: the mushaf's line count and the printed line spacing */
|
|
406
|
+
get grid() { this.e.ex.qvp_page_grid(this.h, this.e.scratch); const d = this.e.dv(); return { lines: d.getUint32(this.e.scratch, true), lineSpacing: d.getFloat32(this.e.scratch + 4, true) }; }
|
|
407
|
+
layout(spec) {
|
|
408
|
+
const ex = this.e.ex, s = this.e.scratch;
|
|
409
|
+
this._writeLayoutSpec(spec, s);
|
|
410
|
+
ex.qvp_layout(this.h, s, s + 128);
|
|
411
|
+
return this._readLayout(s + 128);
|
|
412
|
+
}
|
|
413
|
+
/** read a QvpLayout the engine has written at `o`, and everything that goes with it */
|
|
414
|
+
_readLayout(o) {
|
|
415
|
+
const ex = this.e.ex; let d = this.e.dv();
|
|
318
416
|
const n = d.getUint32(o + 24, true), lp = d.getUint32(o + 28, true);
|
|
319
417
|
const f = new Float32Array(this.e.mem.buffer.slice(lp, lp + n * 12));
|
|
320
418
|
const lineDy = new Float32Array(n), slots = new Array(n);
|
|
321
419
|
for (let i = 0; i < n; i++) { lineDy[i] = f[i * 3]; slots[i] = [f[i * 3 + 1], f[i * 3 + 2]]; }
|
|
322
|
-
|
|
420
|
+
const reflowed = d.getUint32(o + 44, true) === 1, rows = d.getUint32(o + 48, true);
|
|
421
|
+
const L = { scale: d.getFloat32(o, true), offsetX: d.getFloat32(o + 4, true), offsetY: d.getFloat32(o + 8, true), contentW: d.getFloat32(o + 12, true), contentH: d.getFloat32(o + 16, true), lineSpacing: d.getFloat32(o + 20, true), lineDy, slots,
|
|
422
|
+
fitScale: d.getFloat32(o + 32, true), fitX: d.getFloat32(o + 36, true), fitY: d.getFloat32(o + 40, true), reflowed, rows };
|
|
423
|
+
// where each group of paths sits: {dx, dy, k} triples, and the group of every path
|
|
424
|
+
const ng = ex.qvp_layout_groups(this.h, 0, 0);
|
|
425
|
+
const gp = this.e.buf(ng * 16);
|
|
426
|
+
ex.qvp_layout_groups(this.h, gp, ng);
|
|
427
|
+
L.groups = new Float32Array(this.e.mem.buffer.slice(gp, gp + ng * 16));
|
|
428
|
+
const np = ex.qvp_layout_path_groups(this.h, 0, 0);
|
|
429
|
+
if (np) { const pp = this.e.buf(np * 4); ex.qvp_layout_path_groups(this.h, pp, np); L.pathGroup = new Uint32Array(this.e.mem.buffer.slice(pp, pp + np * 4)); }
|
|
430
|
+
else L.pathGroup = null;
|
|
431
|
+
// paths this layout leaves undrawn: sheet furniture and a native surah frame hidden by the spec or reflow
|
|
432
|
+
const no = ex.qvp_layout_omitted_paths(this.h, 0, 0);
|
|
433
|
+
if (no) { const op = this.e.buf(no * 4); ex.qvp_layout_omitted_paths(this.h, op, no); L.omitted = new Set(new Uint32Array(this.e.mem.buffer, op, no)); }
|
|
434
|
+
else L.omitted = null;
|
|
435
|
+
// paths drawn a second time elsewhere: a sajdah line whose words ended up on two rows
|
|
436
|
+
const nr = ex.qvp_layout_repeats(this.h, 0, 0);
|
|
437
|
+
L.repeats = [];
|
|
438
|
+
if (nr) {
|
|
439
|
+
const rp = this.e.buf(nr * 24); ex.qvp_layout_repeats(this.h, rp, nr);
|
|
440
|
+
const f = new Float32Array(this.e.mem.buffer.slice(rp, rp + nr * 24));
|
|
441
|
+
for (let i = 0; i < nr; i++) L.repeats.push({ firstPath: f[i * 6], nPaths: f[i * 6 + 1], dx: f[i * 6 + 2], dy: f[i * 6 + 3], kx: f[i * 6 + 4], ky: f[i * 6 + 5] });
|
|
442
|
+
}
|
|
443
|
+
return (this.currentLayout = L);
|
|
444
|
+
}
|
|
445
|
+
// ── the reader's zoom control ──
|
|
446
|
+
/** where the zoom control stands: {mode, step, zoom}. All zero is what a page opens on:
|
|
447
|
+
* stepped, on the printed page. */
|
|
448
|
+
_putZoom(z, at) { const d = this.e.dv(); d.setUint32(at, ZOOM_MODE[z && z.mode] ?? 0, true); d.setUint32(at + 4, (z && z.step) || 0, true); d.setFloat32(at + 8, (z && z.zoom) || 1, true); return at; }
|
|
449
|
+
_getZoom(at) { const d = this.e.dv(); return { mode: ZOOM_MODE_NAME[d.getUint32(at, true)] || 'stepped', step: d.getUint32(at + 4, true), zoom: d.getFloat32(at + 8, true) }; }
|
|
450
|
+
_getZoomChange(at) { return { zoom: this._getZoom(at), view: this.e._getView(at + 12), relaid: !!this.e.dv().getUint32(at + 24, true) }; }
|
|
451
|
+
/** the same control under another policy, keeping the size the reader is at */
|
|
452
|
+
zoomMode(spec, zoom, mode) {
|
|
453
|
+
const s = this.e.scratch2 + 40960; this._writeLayoutSpec(spec, s);
|
|
454
|
+
const z = this._putZoom(zoom, this.e.scratch2 + 41984);
|
|
455
|
+
this.e.ex.qvp_zoom_mode(this.h, s, z, ZOOM_MODE[mode] ?? 0, this.e.scratch);
|
|
456
|
+
return this._getZoom(this.e.scratch);
|
|
457
|
+
}
|
|
458
|
+
/** one frame of a pinch: `factor` is the fingers' distance against their distance when
|
|
459
|
+
* they went down, and (x, y) the point between them. Returns {zoom, view, relaid}. */
|
|
460
|
+
zoomPinch(spec, zoom, view, factor, x, y) {
|
|
461
|
+
const s = this.e.scratch2 + 40960; this._writeLayoutSpec(spec, s);
|
|
462
|
+
const z = this._putZoom(zoom, this.e.scratch2 + 41984);
|
|
463
|
+
const v = this.e._putView(view, this.e.scratch2 + 42000);
|
|
464
|
+
this.e.ex.qvp_zoom_pinch(this.h, s, z, v, factor, x, y, this.e.scratch);
|
|
465
|
+
const c = this._getZoomChange(this.e.scratch);
|
|
466
|
+
// the engine laid the page out inside the call: keep the cached layout with it
|
|
467
|
+
if (c.relaid) this.layoutCurrent();
|
|
468
|
+
return c;
|
|
469
|
+
}
|
|
470
|
+
/** the control moved straight to a step: a size button, a double tap, a reset. Step 0 is
|
|
471
|
+
* the printed page. */
|
|
472
|
+
zoomToStep(spec, zoom, step, view) {
|
|
473
|
+
const s = this.e.scratch2 + 40960; this._writeLayoutSpec(spec, s);
|
|
474
|
+
const z = this._putZoom(zoom, this.e.scratch2 + 41984);
|
|
475
|
+
const v = this.e._putView(view, this.e.scratch2 + 42000);
|
|
476
|
+
this.e.ex.qvp_zoom_to_step(this.h, s, z, step, v, this.e.scratch);
|
|
477
|
+
const c = this._getZoomChange(this.e.scratch);
|
|
478
|
+
if (c.relaid) this.layoutCurrent();
|
|
479
|
+
return c;
|
|
323
480
|
}
|
|
324
|
-
|
|
481
|
+
/** `spec` with this control's zoom in it: what the host lays out and draws with */
|
|
482
|
+
zoomSpec(spec, zoom) {
|
|
483
|
+
const s = this.e.scratch2 + 40960; this._writeLayoutSpec(spec, s);
|
|
484
|
+
const z = this._putZoom(zoom, this.e.scratch2 + 41984);
|
|
485
|
+
this.e.ex.qvp_zoom_spec(this.h, s, z, this.e.scratch);
|
|
486
|
+
const at = this.e.scratch + 48;
|
|
487
|
+
const v = this.e.dv().getFloat32(at, true);
|
|
488
|
+
return { ...spec, reflow: v > 0 ? { ...(spec.reflow || {}), zoom: v } : null };
|
|
489
|
+
}
|
|
490
|
+
/** The same control on this page: what the reader was reading at, carried onto the page they
|
|
491
|
+
* turned to. A step carries as a step, because every page's steps are its own; a free zoom
|
|
492
|
+
* carries as a size, held inside what this page can reach. */
|
|
493
|
+
zoomCarried(spec, zoom) {
|
|
494
|
+
const s = this.e.scratch2 + 40960; this._writeLayoutSpec(spec, s);
|
|
495
|
+
const z = this._putZoom(zoom, this.e.scratch2 + 41984);
|
|
496
|
+
this.e.ex.qvp_zoom_carried(this.h, s, z, this.e.scratch);
|
|
497
|
+
return this._getZoom(this.e.scratch);
|
|
498
|
+
}
|
|
499
|
+
/** the reflow zoom one step of this page's control means; step 0 is the printed page */
|
|
500
|
+
zoomAtStep(spec, step) {
|
|
501
|
+
const s = this.e.scratch2 + 40960; this._writeLayoutSpec(spec, s);
|
|
502
|
+
return this.e.ex.qvp_zoom_at_step(this.h, s, step);
|
|
503
|
+
}
|
|
504
|
+
/** true once the reader has zoomed in, by either road: a magnified view, or a page reflowed
|
|
505
|
+
* above the printed size. `fitScale` is the scale the page is fitted at (0 = it is at it). */
|
|
506
|
+
zoomIsZoomed(zoom, view, fitScale = 0) {
|
|
507
|
+
const z = this._putZoom(zoom, this.e.scratch2 + 41984);
|
|
508
|
+
const v = this.e._putView(view, this.e.scratch2 + 42000);
|
|
509
|
+
return !!this.e.ex.qvp_zoom_is_zoomed(z, v, fitScale);
|
|
510
|
+
}
|
|
511
|
+
/** what a sideways drag on this page means: 'pan' it, or 'turnPage' */
|
|
512
|
+
sidewaysDrag(zoom, view, fitScale = 0) {
|
|
513
|
+
const z = this._putZoom(zoom, this.e.scratch2 + 41984);
|
|
514
|
+
const v = this.e._putView(view, this.e.scratch2 + 42000);
|
|
515
|
+
return this.e.ex.qvp_sideways_drag(this.h, z, v, fitScale) === 1 ? 'turnPage' : 'pan';
|
|
516
|
+
}
|
|
517
|
+
/** The layout the page already has, without computing one: what to read after a call that
|
|
518
|
+
* laid the page out itself, as the zoom control does. */
|
|
519
|
+
layoutCurrent() {
|
|
520
|
+
const s = this.e.scratch;
|
|
521
|
+
if (!this.e.ex.qvp_layout_current(this.h, s + 128)) return this.currentLayout;
|
|
522
|
+
return this._readLayout(s + 128);
|
|
523
|
+
}
|
|
524
|
+
/** Everything the current layout draws, in drawing order: `{path, placement}` pairs into
|
|
525
|
+
* `layoutPlacements()`. Omitted paths are already gone and a repeated one is already there
|
|
526
|
+
* twice, so one loop draws any page — printed or reflowed. */
|
|
527
|
+
layoutDrawList(bandTop = 0, bandBottom = 0) {
|
|
528
|
+
const n = this.e.ex.qvp_layout_draw_list(this.h, bandTop, bandBottom, 0, 0);
|
|
529
|
+
if (!n) return new Uint32Array(0);
|
|
530
|
+
const p = this.e.buf(n * 8); this.e.ex.qvp_layout_draw_list(this.h, bandTop, bandBottom, p, n);
|
|
531
|
+
return new Uint32Array(this.e.mem.buffer.slice(p, p + n * 8));
|
|
532
|
+
}
|
|
533
|
+
/** what a draw list's `placement` indexes: every group, then every repeat */
|
|
534
|
+
layoutPlacements() {
|
|
535
|
+
const n = this.e.ex.qvp_layout_placements(this.h, 0, 0);
|
|
536
|
+
if (!n) return new Float32Array(0);
|
|
537
|
+
const p = this.e.buf(n * 16); this.e.ex.qvp_layout_placements(this.h, p, n);
|
|
538
|
+
return new Float32Array(this.e.mem.buffer.slice(p, p + n * 16));
|
|
539
|
+
}
|
|
540
|
+
/** the largest reflow zoom at which every word of this page still fits a row */
|
|
541
|
+
reflowMaxZoom(spec) { const s = this.e.scratch; this._writeLayoutSpec(spec, s); return this.e.ex.qvp_reflow_max_zoom(this.h, s); }
|
|
542
|
+
/** the zoom each step of a reader's zoom control lands on for this page, rising */
|
|
543
|
+
zoomLevels(spec, nominals, band) {
|
|
544
|
+
const s = this.e.scratch; this._writeLayoutSpec(spec, s);
|
|
545
|
+
const k = nominals ? nominals.length : 0;
|
|
546
|
+
const np = k ? this.e.buf(k * 4) : 0;
|
|
547
|
+
if (k) new Float32Array(this.e.mem.buffer, np, k).set(nominals);
|
|
548
|
+
const out = this.e.buf(8 * 4);
|
|
549
|
+
const n = this.e.ex.qvp_zoom_levels(this.h, s, np, k, band || 0, out, 8);
|
|
550
|
+
return Array.from(new Float32Array(this.e.mem.buffer, out, n));
|
|
551
|
+
}
|
|
552
|
+
/** the zoom steps this page ships with, lowest first; zoom 1 is the step before them */
|
|
553
|
+
zoomSteps(spec) {
|
|
554
|
+
const s = this.e.scratch; this._writeLayoutSpec(spec, s);
|
|
555
|
+
const out = this.e.buf(8 * 4);
|
|
556
|
+
const n = this.e.ex.qvp_zoom_steps(this.h, s, out, 8);
|
|
557
|
+
return Array.from(new Float32Array(this.e.mem.buffer, out, n));
|
|
558
|
+
}
|
|
559
|
+
/** every zoom the search weighs for one step of a zoom control, with its cost */
|
|
560
|
+
zoomLevelCandidates(spec, nominal, band, floor) {
|
|
561
|
+
const s = this.e.scratch; this._writeLayoutSpec(spec, s);
|
|
562
|
+
// one block, two halves: `buf` hands back the same scratch every time
|
|
563
|
+
const N = 64, z = this.e.buf(N * 8), c = z + N * 4;
|
|
564
|
+
const n = this.e.ex.qvp_zoom_level_candidates(this.h, s, nominal, band || 0, floor || 0, z, c, N);
|
|
565
|
+
const zs = new Float32Array(this.e.mem.buffer, z, n), cs = new Float32Array(this.e.mem.buffer, c, n);
|
|
566
|
+
return Array.from(zs, (v, i) => ({ zoom: v, cost: cs[i] }));
|
|
567
|
+
}
|
|
568
|
+
/** the words of a reflowed row, in reading order */
|
|
569
|
+
rowWords(row) {
|
|
570
|
+
const n = this.e.ex.qvp_layout_row_words(this.h, row, 0, 0);
|
|
571
|
+
if (!n) return [];
|
|
572
|
+
const p = this.e.buf(n * 4); this.e.ex.qvp_layout_row_words(this.h, row, p, n);
|
|
573
|
+
return Array.from(new Uint32Array(this.e.mem.buffer, p, n));
|
|
574
|
+
}
|
|
575
|
+
/** the row a word landed on in a reflowed layout, or null */
|
|
576
|
+
wordRow(word) { const r = this.e.ex.qvp_layout_word_row(this.h, word); return r === 0xffffffff ? null : r; }
|
|
577
|
+
/** the view that puts a point inside a word (nx, ny from 0 to 1) at a place on the screen,
|
|
578
|
+
* then clamps: how a pinch holds its place when the layout reflows under it */
|
|
579
|
+
viewAnchor(view, word, nx, ny, toX, toY, viewportW, viewportH) {
|
|
580
|
+
const i = this.e._putView(view, this.e.scratch2 + 32768);
|
|
581
|
+
this.e.ex.qvp_view_anchor(this.h, i, word, nx, ny, toX, toY, viewportW, viewportH, this.e.scratch);
|
|
582
|
+
return this.e._getView(this.e.scratch);
|
|
583
|
+
}
|
|
584
|
+
/** where a viewport point lands in the laid-out page, for turning a touch into a hit test */
|
|
585
|
+
viewToLayout(view, vx, vy) {
|
|
586
|
+
const i = this.e._putView(view, this.e.scratch2 + 32768);
|
|
587
|
+
this.e.ex.qvp_view_to_layout(this.h, i, vx, vy, this.e.scratch);
|
|
588
|
+
const f = new Float32Array(this.e.mem.buffer, this.e.scratch, 2);
|
|
589
|
+
return { x: f[0], y: f[1] };
|
|
590
|
+
}
|
|
591
|
+
wordBoundsView(i) { this.e.ex.qvp_word_bounds_view(this.h, i, this.e.scratch); const f = new Float32Array(this.e.mem.buffer, this.e.scratch, 4); return { x0: f[0], y0: f[1], x1: f[2], y1: f[3] }; }
|
|
325
592
|
|
|
326
593
|
// ── styles (handles undo exactly) ──
|
|
327
594
|
style(sel, color, { ms = 0, layer = LAYER.BASE } = {}) { return this.e.ex.qvp_style_add(this.h, layer, writeSel(this.e, this.e.scratch2 + 12288, sel), rgba(color), ms); }
|
|
328
595
|
styleTarget(target, color, { ms = 0, layer = LAYER.BASE } = {}) { return this.e.ex.qvp_style_add_target(this.h, layer, this._target(target), rgba(color), ms); }
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
hide(sel) { return this.e.ex.
|
|
596
|
+
removeStyle(handle) { return this.e.ex.qvp_style_remove(this.h, handle); }
|
|
597
|
+
recolorStyle(handle, color, ms = 0) { return this.e.ex.qvp_style_recolor(this.h, handle, rgba(color), ms); }
|
|
598
|
+
hide(sel) { return this.e.ex.qvp_style_hide(this.h, writeSel(this.e, this.e.scratch2 + 12288, sel)); }
|
|
332
599
|
clearStyles() { this.e.ex.qvp_style_clear(this.h); }
|
|
333
600
|
clearLayer(layer) { this.e.ex.qvp_style_clear_layer(this.h, layer); }
|
|
334
|
-
|
|
601
|
+
setDefaultColor(color) { this._defaultInk = rgba(color); this.e.ex.qvp_style_default_color(this.h, this._defaultInk); }
|
|
335
602
|
get defaultInk() { return this._defaultInk; }
|
|
336
603
|
/** {ink, diacritics, dots, waqf, sifr, ayahMark, numeral, headers, marks: {name: colour}, ms} → handle */
|
|
337
604
|
theme(t) {
|
|
@@ -348,59 +615,61 @@
|
|
|
348
615
|
// ── clock & display list ──
|
|
349
616
|
/** advance animations; returns true while something is still moving */
|
|
350
617
|
tick(nowMs) { return !!this.e.ex.qvp_tick(this.h, nowMs); }
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const ex = this.e.ex; const n = ex.
|
|
354
|
-
const buf = ex.qvp_alloc(n * 8); ex.
|
|
618
|
+
colors() { const p = this.e.ex.qvp_colors(this.h); return new Uint32Array(this.e.mem.buffer.slice(p, p + this.nPaths * 4)); }
|
|
619
|
+
styledPaths() {
|
|
620
|
+
const ex = this.e.ex; const n = ex.qvp_styled_paths(this.h, 0, 0); if (!n) return [];
|
|
621
|
+
const buf = ex.qvp_alloc(n * 8); ex.qvp_styled_paths(this.h, buf, n);
|
|
355
622
|
const v = new Uint32Array(this.e.mem.buffer.slice(buf, buf + n * 8)); ex.qvp_dealloc(buf, n * 8);
|
|
356
623
|
const out = new Array(n); for (let i = 0; i < n; i++) out[i] = [v[i * 2], v[i * 2 + 1]]; return out;
|
|
357
624
|
}
|
|
358
625
|
colorOf(i) { return this.e.ex.qvp_color_of(this.h, i) >>> 0; }
|
|
359
626
|
|
|
360
627
|
// ── highlights ──
|
|
361
|
-
/** style: {mode:'ink'|'band'|'both', height:'
|
|
362
|
-
highlight(target, style = {}) { return this.e.ex.
|
|
363
|
-
|
|
364
|
-
restyleHighlight(handle, style) { return !!this.e.ex.
|
|
365
|
-
|
|
366
|
-
clearHighlights() { this.e.ex.
|
|
628
|
+
/** style: {mode:'ink'|'band'|'both', height:'lineSpacing'|'ink', ink, band, padX, padY, radius, seam, ms, layer} */
|
|
629
|
+
highlight(target, style = {}) { return this.e.ex.qvp_highlight_add(this.h, this._target(target), writeHl(this.e, this.e.scratch2 + 24576, style)); }
|
|
630
|
+
moveHighlight(handle, target) { return !!this.e.ex.qvp_highlight_move(this.h, handle, this._target(target)); }
|
|
631
|
+
restyleHighlight(handle, style) { return !!this.e.ex.qvp_highlight_restyle(this.h, handle, writeHl(this.e, this.e.scratch2 + 24576, style)); }
|
|
632
|
+
removeHighlight(handle) { return !!this.e.ex.qvp_highlight_remove(this.h, handle); }
|
|
633
|
+
clearHighlights() { this.e.ex.qvp_highlight_clear(this.h); }
|
|
367
634
|
highlightHandles() { const n = this.e.ex.qvp_highlight_handles(this.h, this.e.scratch, 1024); return Array.from(new Uint32Array(this.e.mem.buffer, this.e.scratch, Math.min(n, 1024))); }
|
|
368
635
|
highlightWords(h) { const n = this.e.ex.qvp_highlight_words(this.h, h, this.e.scratch, 4096); return Array.from(new Uint32Array(this.e.mem.buffer, this.e.scratch, Math.min(n, 4096))); }
|
|
369
636
|
/** animated band boxes in viewport px; draw each id as one nonzero path behind the ink */
|
|
370
|
-
|
|
371
|
-
|
|
637
|
+
highlightBoxesView() { const n = this.e.ex.qvp_highlight_boxes_view(this.h, this.e.scratch, 1024); return readBoxes(this.e, this.e.scratch, Math.min(n, 1024)); }
|
|
638
|
+
wordBands(words, { height = 'lineSpacing', padX = DEFAULTS.HIGHLIGHT_PAD_X, padY = DEFAULTS.HIGHLIGHT_PAD_Y } = {}) { const p = this.e.putU32(Uint32Array.from(words)); const n = this.e.ex.qvp_word_bands(this.h, p, words.length, height === 'ink' ? 1 : 0, padX, padY, this.e.scratch, 64); return readBoxes(this.e, this.e.scratch, Math.min(n, 64)); }
|
|
372
639
|
|
|
373
640
|
// ── selection ──
|
|
374
641
|
select(anchor, focus = anchor) { this.e.ex.qvp_select(this.h, anchor < 0 ? NONE : anchor, focus < 0 ? NONE : focus); }
|
|
375
642
|
clearSelection() { this.e.ex.qvp_select(this.h, NONE, NONE); }
|
|
376
643
|
selection() { const n = this.e.ex.qvp_selection(this.h, this.e.scratch, 4096); return Array.from(new Uint32Array(this.e.mem.buffer, this.e.scratch, Math.min(n, 4096))); }
|
|
377
|
-
selectionText(form = 'rasm_uthmani',
|
|
644
|
+
selectionText(form = 'rasm_uthmani', includeCitation = false) { this.e.ex.qvp_selection_text(this.h, FORM[form] ?? 0, includeCitation ? 1 : 0, this.e.scratch); return this.e.qstr(this.e.scratch); }
|
|
378
645
|
|
|
379
646
|
// ── memorisation ──
|
|
380
647
|
mask(target, mode = 'hide') { this.e.ex.qvp_mask(this.h, this._target(target), { hide: 0, block: 1, blur: 2 }[mode] ?? 0); }
|
|
381
648
|
maskFrom(i, mode = 'hide') { this.e.ex.qvp_mask_from(this.h, i, { hide: 0, block: 1, blur: 2 }[mode] ?? 0); }
|
|
382
|
-
maskOptions({ blockColor =
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
649
|
+
maskOptions({ blockColor = DEFAULTS.MASK_BLOCK, padX = DEFAULTS.MASK_PAD, padY = DEFAULTS.MASK_PAD, radius = DEFAULTS.MASK_RADIUS, reverse = false } = {}) { this.e.ex.qvp_mask_options(this.h, rgba(blockColor), padX, padY, radius, reverse ? 1 : 0); }
|
|
650
|
+
unmaskNext(n = 1) { return this.e.ex.qvp_unmask_next(this.h, n); }
|
|
651
|
+
maskBack(n = 1) { return this.e.ex.qvp_mask_back(this.h, n); }
|
|
652
|
+
unmaskWord(i) { return !!this.e.ex.qvp_unmask_word(this.h, i); }
|
|
653
|
+
maskWord(i) { return !!this.e.ex.qvp_mask_word(this.h, i); }
|
|
654
|
+
unmaskAll() { this.e.ex.qvp_unmask_all(this.h); }
|
|
655
|
+
maskAll() { this.e.ex.qvp_mask_all(this.h); }
|
|
389
656
|
unmask() { this.e.ex.qvp_unmask(this.h); }
|
|
390
657
|
maskHidden() { const n = this.e.ex.qvp_mask_hidden(this.h, this.e.scratch, 4096); return Array.from(new Uint32Array(this.e.mem.buffer, this.e.scratch, Math.min(n, 4096))); }
|
|
391
658
|
maskWords() { const n = this.e.ex.qvp_mask_words(this.h, this.e.scratch, 4096); return Array.from(new Uint32Array(this.e.mem.buffer, this.e.scratch, Math.min(n, 4096))); }
|
|
392
|
-
|
|
659
|
+
/** band boxes for a word list in viewport px through the current layout */
|
|
660
|
+
wordBandsView(words, { height = 'lineSpacing', padX = DEFAULTS.HIGHLIGHT_PAD_X, padY = DEFAULTS.HIGHLIGHT_PAD_Y } = {}) { const p = this.e.putU32(Uint32Array.from(words)); const n = this.e.ex.qvp_word_bands_view(this.h, p, words.length, height === 'ink' ? 1 : 0, padX, padY, this.e.scratch, 64); return readBoxes(this.e, this.e.scratch, Math.min(n, 64)); }
|
|
661
|
+
maskBoxesView() { const n = this.e.ex.qvp_mask_boxes_view(this.h, this.e.scratch, 1024); return readBoxes(this.e, this.e.scratch, Math.min(n, 1024)); }
|
|
393
662
|
/** greyed page with a lit window: {lit, byAyah, grey, ink, ayahMarks, ms} → steps */
|
|
394
|
-
revealStart({ lit =
|
|
663
|
+
revealStart({ lit = DEFAULTS.REVEAL_LIT, byAyah = false, grey = DEFAULTS.REVEAL_GREY, ink = DEFAULTS.INK, ayahMarks = true, ms = 0 } = {}) { return this.e.ex.qvp_reveal_start(this.h, lit, byAyah ? 1 : 0, rgba(grey), rgba(ink), ayahMarks ? 1 : 0, ms); }
|
|
395
664
|
revealGoto(at) { return !!this.e.ex.qvp_reveal_goto(this.h, BigInt(at)); }
|
|
396
|
-
|
|
397
|
-
|
|
665
|
+
revealPosition() { const v = Number(this.e.ex.qvp_reveal_position(this.h)); return v === -2 ? null : v; }
|
|
666
|
+
revealStepCount() { return this.e.ex.qvp_reveal_step_count(this.h); }
|
|
398
667
|
revealStepOf(i) { return Number(this.e.ex.qvp_reveal_step_of(this.h, i)); }
|
|
399
668
|
revealStop() { this.e.ex.qvp_reveal_stop(this.h); }
|
|
400
669
|
|
|
401
670
|
// ── crop ──
|
|
402
|
-
|
|
403
|
-
cropSvg(target, { pad =
|
|
671
|
+
cropBounds(target, { pad = DEFAULTS.CROP_PAD, keepAyahMarks = true } = {}) { if (!this.e.ex.qvp_crop_bounds(this.h, this._target(target), pad, keepAyahMarks ? 1 : 0, this.e.scratch)) return null; const d = this.e.dv(), s = this.e.scratch; return { x0: d.getFloat32(s, true), y0: d.getFloat32(s + 4, true), x1: d.getFloat32(s + 8, true), y1: d.getFloat32(s + 12, true), nWords: d.getUint32(s + 16, true), ayahMarkDecoration: d.getUint32(s + 20, true) }; }
|
|
672
|
+
cropSvg(target, { pad = DEFAULTS.CROP_PAD, keepAyahMarks = true, background = null } = {}) { if (!this.e.ex.qvp_crop_svg(this.h, this._target(target), pad, keepAyahMarks ? 1 : 0, background ? rgba(background) : 0, this.e.scratch)) return null; return this.e.qstr(this.e.scratch); }
|
|
404
673
|
}
|
|
405
674
|
|
|
406
675
|
class QvpAtlas {
|
|
@@ -408,18 +677,22 @@
|
|
|
408
677
|
free() { this.e.ex.qvp_atlas_free(this.h); this.h = 0; }
|
|
409
678
|
pageOf(surah, ayah) { const p = this.e.ex.qvp_atlas_page_of(this.h, surah, ayah); return p < 0 ? null : p; }
|
|
410
679
|
pageRange(page) { if (!this.e.ex.qvp_atlas_page_range(this.h, page, this.e.scratch)) return null; const v = new Uint16Array(this.e.mem.buffer, this.e.scratch, 4); return { first: [v[0], v[1]], last: [v[2], v[3]] }; }
|
|
411
|
-
_surah(s) { const d = this.e.dv(); return {
|
|
680
|
+
_surah(s) { const d = this.e.dv(); return { number: d.getUint16(s, true), page: d.getUint16(s + 2, true), ayahCount: d.getUint16(s + 4, true), place: names('place')[d.getUint8(s + 6)] || '', arabic: this.e.qstr(s + 8), latin: this.e.qstr(s + 16), english: this.e.qstr(s + 24) }; }
|
|
412
681
|
surah(n) { return this.e.ex.qvp_atlas_surah(this.h, n, this.e.scratch) ? this._surah(this.e.scratch) : null; }
|
|
413
|
-
surahs() { const n = this.e.ex.
|
|
682
|
+
surahs() { const n = this.e.ex.qvp_atlas_surah_count(this.h), out = []; for (let i = 0; i < n; i++) { this.e.ex.qvp_atlas_surah_at(this.h, i, this.e.scratch); out.push(this._surah(this.e.scratch)); } return out; }
|
|
414
683
|
pageOfSurah(n) { const s = this.surah(n); return s ? s.page : null; }
|
|
684
|
+
/** How many pages the atlas covers. */
|
|
685
|
+
pageCount() { return this.e.ex.qvp_atlas_page_count(this.h); }
|
|
686
|
+
/** The whole atlas as JSON text. */
|
|
687
|
+
json() { this.e.ex.qvp_atlas_json(this.h, this.e.scratch); return this.e.qstr(this.e.scratch); }
|
|
415
688
|
division(kind, n) { if (!this.e.ex.qvp_atlas_division(this.h, { juz: 0, hizb: 1, nisf: 2, rubu_al_hizb: 3 }[kind], n, this.e.scratch)) return null; const v = new Uint16Array(this.e.mem.buffer, this.e.scratch, 4); return { rubuAlHizb: v[0], surah: v[1], ayah: v[2], page: v[3], ayahKey: `${v[1]}:${v[2]}` }; }
|
|
416
689
|
juz(n) { return this.division('juz', n); }
|
|
417
690
|
hizb(n) { return this.division('hizb', n); }
|
|
418
691
|
rubuAlHizb(n) { return this.division('rubu_al_hizb', n); }
|
|
419
|
-
|
|
420
|
-
|
|
692
|
+
divisionOf(kind, surah, ayah) { const v = this.e.ex.qvp_atlas_division_of(this.h, { juz: 0, hizb: 1, nisf: 2, rubu_al_hizb: 3 }[kind], surah, ayah); return v < 0 ? null : v; }
|
|
693
|
+
juzOf(surah, ayah) { return this.divisionOf('juz', surah, ayah); }
|
|
421
694
|
pagesOfJuz(n) { if (!this.e.ex.qvp_atlas_pages_of_juz(this.h, n, this.e.scratch)) return null; const v = new Uint16Array(this.e.mem.buffer, this.e.scratch, 2); return [v[0], v[1]]; }
|
|
422
|
-
|
|
695
|
+
searchSurahs(text) { const [p, n] = this.e.putStr(text); const c = this.e.ex.qvp_atlas_search_surahs(this.h, p, n, this.e.scratch, 128); return Array.from(new Uint16Array(this.e.mem.buffer, this.e.scratch, Math.min(c, 128))).map(k => this.surah(k)); }
|
|
423
696
|
}
|
|
424
697
|
|
|
425
698
|
/**
|
|
@@ -430,16 +703,35 @@
|
|
|
430
703
|
constructor(canvas) {
|
|
431
704
|
this.canvas = canvas; this.ctx = canvas.getContext('2d');
|
|
432
705
|
this.base = document.createElement('canvas'); this.baseKey = '';
|
|
706
|
+
// the engine's draw list for the band in hand, kept until the band moves
|
|
707
|
+
this.list = null; this.places = null; this.listKey = '';
|
|
708
|
+
/// How much ink to keep ready at once: a page that fits is drawn once and scrolling never
|
|
709
|
+
/// draws again. About 17 MB for a page of this mushaf at the first zoom step.
|
|
710
|
+
this.inkBudget = 64 << 20;
|
|
433
711
|
this.stats = { baseMs: 0, overlayMs: 0, basePaths: 0, overlayPaths: 0, bands: 0 };
|
|
434
712
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
713
|
+
/** transform of one group of paths: a point p is drawn at (kx·p.x + dx, ky·p.y + dy), page units */
|
|
714
|
+
groupTransform(page, view, group, dpr) {
|
|
715
|
+
const L = page.currentLayout || { scale: 1, offsetX: 0, offsetY: 0, lineDy: null, groups: null };
|
|
716
|
+
let dx = 0, dy = 0, kx = 1, ky = 1;
|
|
717
|
+
if (L.groups && L.groups.length > group * 4) { dx = L.groups[group * 4]; dy = L.groups[group * 4 + 1]; kx = L.groups[group * 4 + 2]; ky = L.groups[group * 4 + 3]; }
|
|
718
|
+
else if (L.lineDy) dy = L.lineDy[group] || 0;
|
|
719
|
+
return this.placementTransform(page, view, { dx, dy, kx, ky }, dpr);
|
|
439
720
|
}
|
|
721
|
+
/** transform of one placement: [a, d, tx, ty] for setTransform(a, 0, 0, d, tx, ty) */
|
|
722
|
+
placementTransform(page, view, p, dpr) {
|
|
723
|
+
const L = page.currentLayout || { scale: 1, offsetX: 0, offsetY: 0 };
|
|
724
|
+
const s = dpr * view.scale * L.scale;
|
|
725
|
+
return [s * p.kx, s * p.ky, dpr * (view.offsetX + view.scale * (L.offsetX + p.dx * L.scale)), dpr * (view.offsetY + view.scale * (L.offsetY + p.dy * L.scale))];
|
|
726
|
+
}
|
|
727
|
+
lineTransform(page, view, line, dpr) { const [a, , tx, ty] = this.groupTransform(page, view, line, dpr); return [a, tx, ty]; }
|
|
728
|
+
/** put a context into one group's space, so a host overlay lands where the group is drawn */
|
|
729
|
+
setGroupTransform(c, page, view, group, dpr) { const [a, d, tx, ty] = this.groupTransform(page, view, group, dpr); c.setTransform(a, 0, 0, d, tx, ty); }
|
|
730
|
+
/** the group a path is drawn with: its reflow group, or its printed line */
|
|
731
|
+
pathGroup(page, i) { const L = page.currentLayout; return L && L.pathGroup ? L.pathGroup[i] : page.pathLine(i); }
|
|
440
732
|
/** boxes are in layout viewport px; view adds pan/zoom on top */
|
|
441
733
|
drawBoxes(c, boxes, view, dpr) {
|
|
442
|
-
c.setTransform(dpr * view.scale, 0, 0, dpr * view.scale, dpr * view.
|
|
734
|
+
c.setTransform(dpr * view.scale, 0, 0, dpr * view.scale, dpr * view.offsetX, dpr * view.offsetY);
|
|
443
735
|
let cur = null, col = 0;
|
|
444
736
|
const flush = () => { if (cur) { c.fillStyle = css(col); c.fill(cur, 'nonzero'); cur = null; } };
|
|
445
737
|
for (const b of boxes) {
|
|
@@ -450,22 +742,54 @@
|
|
|
450
742
|
}
|
|
451
743
|
draw(page, view, dpr) {
|
|
452
744
|
const paths = page.buildPaths();
|
|
453
|
-
const styled = page.
|
|
745
|
+
const styled = page.styledPaths();
|
|
454
746
|
const styledSet = new Set(styled.map(s => s[0]));
|
|
455
|
-
const L = page.currentLayout || { scale: 1,
|
|
747
|
+
const L = page.currentLayout || { scale: 1, offsetX: 0, offsetY: 0, lineDy: null, lineSpacing: 0 };
|
|
456
748
|
const ink = page.defaultInk;
|
|
457
|
-
const key = `${view.scale.toFixed(4)}|${view.ox.toFixed(1)}|${view.oy.toFixed(1)}|${dpr}|${ink}|${L.scale}|${L.pitch}|${L.lineDy ? L.lineDy[0] : ''}|${[...styledSet].sort((a, b) => a - b).join(',')}`;
|
|
458
749
|
const W = this.canvas.width, H = this.canvas.height;
|
|
459
|
-
|
|
460
|
-
|
|
750
|
+
// The cached ink is a band of the page, not the screen, so scrolling inside it is one
|
|
751
|
+
// blit and nothing is drawn again. A page that is several screens of ink used to redraw
|
|
752
|
+
// every path on every frame of a drag, because the cache was keyed on where the reader
|
|
753
|
+
// had scrolled to. The band is rebuilt only when the drag has travelled half a screen.
|
|
754
|
+
const viewH = H / dpr, slack = viewH / 2, step = viewH / 2;
|
|
755
|
+
const scale = view.scale || 1;
|
|
756
|
+
// A page that fits the ink budget is drawn once, the parts past both ends of the screen
|
|
757
|
+
// included, and then scrolling draws nothing at all. A taller one keeps a band of itself,
|
|
758
|
+
// two screens tall, redrawn when the reader scrolls out of it.
|
|
759
|
+
const contentH = (L.contentH || 0) * scale;
|
|
760
|
+
const whole = contentH > 0 && W * contentH * dpr * 4 <= this.inkBudget && contentH * dpr <= 16384;
|
|
761
|
+
const bandTop = whole ? 0 : Math.floor((-view.offsetY - slack) / step) * step; // view px
|
|
762
|
+
const bandH = whole ? contentH : viewH + 2 * slack;
|
|
763
|
+
// the ink drawn into the band, in layout px: what the engine is asked for
|
|
764
|
+
const cullTop = bandTop / scale, cullBottom = (bandTop + bandH) / scale;
|
|
765
|
+
// the view the band is rendered under: the reader's, less where the band starts
|
|
766
|
+
const bandView = { ...view, offsetY: -bandTop };
|
|
767
|
+
const key = `${view.scale.toFixed(4)}|${view.offsetX.toFixed(1)}|${bandTop.toFixed(1)}|${dpr}|${ink}|${L.scale}|${L.lineSpacing}|${L.lineDy ? L.lineDy[0] : ''}|${L.groups ? L.groups.length + ':' + L.groups[1] : ''}|${(L.repeats || []).length}|${L.omitted ? L.omitted.size : 0}|${[...styledSet].sort((a, b) => a - b).join(',')}`;
|
|
768
|
+
// The engine says what is drawn and where: each path once under its placement, again for
|
|
769
|
+
// every row a decoration repeats over, and nothing for a path this layout leaves out. It
|
|
770
|
+
// is asked once per band, not once per frame: a scroll inside the band changes neither.
|
|
771
|
+
const listKey = `${page.page}|${cullTop.toFixed(1)}|${cullBottom.toFixed(1)}|${L.scale}|${L.rows}|${L.contentH}`;
|
|
772
|
+
if (listKey !== this.listKey) {
|
|
773
|
+
this.list = page.layoutDrawList(cullTop, cullBottom);
|
|
774
|
+
this.places = page.layoutPlacements();
|
|
775
|
+
this.listKey = listKey;
|
|
776
|
+
}
|
|
777
|
+
const list = this.list, places = this.places;
|
|
778
|
+
const setPlace = (c, g, v = view) => {
|
|
779
|
+
const [a, d, tx, ty] = this.placementTransform(page, v, { dx: places[g * 4], dy: places[g * 4 + 1], kx: places[g * 4 + 2], ky: places[g * 4 + 3] }, dpr);
|
|
780
|
+
c.setTransform(a, 0, 0, d, tx, ty);
|
|
781
|
+
};
|
|
782
|
+
const baseH = Math.ceil(bandH * dpr);
|
|
783
|
+
if (key !== this.baseKey || this.base.width !== W || this.base.height !== baseH) {
|
|
461
784
|
const t0 = performance.now();
|
|
462
|
-
this.base.width = W; this.base.height =
|
|
785
|
+
this.base.width = W; this.base.height = baseH;
|
|
463
786
|
const b = this.base.getContext('2d');
|
|
464
787
|
b.fillStyle = css(ink);
|
|
465
788
|
let n = 0, cur = -1;
|
|
466
|
-
for (let
|
|
789
|
+
for (let k = 0; k < list.length; k += 2) {
|
|
790
|
+
const i = list[k], g = list[k + 1];
|
|
467
791
|
if (styledSet.has(i)) continue;
|
|
468
|
-
|
|
792
|
+
if (g !== cur) { setPlace(b, g, bandView); cur = g; }
|
|
469
793
|
b.fill(paths[i], page.pathEvenOdd(i) ? 'evenodd' : 'nonzero'); n++;
|
|
470
794
|
}
|
|
471
795
|
this.baseKey = key; this.stats.baseMs = performance.now() - t0; this.stats.basePaths = n;
|
|
@@ -473,20 +797,23 @@
|
|
|
473
797
|
const t1 = performance.now();
|
|
474
798
|
const c = this.ctx;
|
|
475
799
|
c.setTransform(1, 0, 0, 1, 0, 0); c.clearRect(0, 0, W, H);
|
|
476
|
-
const bands = page.
|
|
800
|
+
const bands = page.highlightBoxesView();
|
|
477
801
|
this.drawBoxes(c, bands, view, dpr);
|
|
478
802
|
c.setTransform(1, 0, 0, 1, 0, 0);
|
|
479
|
-
c.drawImage(this.base, 0,
|
|
803
|
+
c.drawImage(this.base, 0, Math.round(dpr * (view.offsetY + bandTop)));
|
|
804
|
+
const colors = new Map(styled);
|
|
480
805
|
let cur = -1;
|
|
481
|
-
for (
|
|
482
|
-
|
|
483
|
-
const
|
|
806
|
+
for (let k = 0; k < list.length; k += 2) {
|
|
807
|
+
const i = list[k], g = list[k + 1];
|
|
808
|
+
const col = colors.get(i);
|
|
809
|
+
if (col === undefined || (col & 255) === 0) continue;
|
|
810
|
+
if (g !== cur) { setPlace(c, g); cur = g; }
|
|
484
811
|
c.fillStyle = css(col); c.fill(paths[i], page.pathEvenOdd(i) ? 'evenodd' : 'nonzero');
|
|
485
812
|
}
|
|
486
|
-
this.drawBoxes(c, page.
|
|
813
|
+
this.drawBoxes(c, page.maskBoxesView(), view, dpr);
|
|
487
814
|
this.stats.overlayMs = performance.now() - t1; this.stats.overlayPaths = styled.length; this.stats.bands = bands.length;
|
|
488
815
|
}
|
|
489
816
|
}
|
|
490
817
|
|
|
491
|
-
global.QVP = { QvpEngine, QvpPage, QvpAtlas, CanvasRenderer, Sel, T, KIND, FAMILY, CATEGORY,
|
|
818
|
+
global.QVP = { QvpEngine, QvpPage, QvpAtlas, CanvasRenderer, Sel, T, KIND, FAMILY, CATEGORY, DECORATION, FORM, LAYER, NAMES_TABLE, DEFAULTS, NONE, css, rgba, parseTarget };
|
|
492
819
|
})(typeof window !== 'undefined' ? window : globalThis);
|