Leseposition: Binärsuche über die Blöcke statt alle vom Buchanfang an messen
getPositionAnchor maß jeden Block vom Buchanfang bis zur Leseposition und suchte die Blöcke bei jedem Aufruf neu – im E-Reader-Modus bei jedem Umblättern (die Prozentanzeige), beim Speichern des Fortschritts sowieso. In Das Kapital ab Kapitel 15: 0,8 s bei 6-fach gedrosselter CPU; auf dem tolino filtert compat61 das :has() dazu in JavaScript. - Blockliste samt Kapitel je Block einmal je geöffnetem Buch (verworfen bei jeder Änderung am Inhalt, MutationObserver) - Binärsuche nur über die angezeigten Kapitel; gleiche Regel wie vorher (letzter Block mit Höhe, der an/über der Oberkante beginnt, bei gleicher Höhe der erste; sonst der erste darunter) - Geprüft an 480 Stellen (Kapital, Hyperpolitik; E-Reader und Scrollen): gleiche Anker wie vorher; 53/281 ms → 4 ms; Umblättern 21–48 ms statt bis 0,9 s - sw v98 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3f2fcec3b8
commit
53ca3b0002
2 changed files with 76 additions and 14 deletions
|
|
@ -2120,26 +2120,88 @@ function _checkScrollJump(contentEl) {
|
||||||
|
|
||||||
const EPUB_BLOCK_SELECTOR = 'p, h1, h2, h3, h4, h5, h6, li, blockquote, dt, dd, figcaption, div:not(:has(*))';
|
const EPUB_BLOCK_SELECTOR = 'p, h1, h2, h3, h4, h5, h6, li, blockquote, dt, dd, figcaption, div:not(:has(*))';
|
||||||
|
|
||||||
function getPositionAnchor(contentEl) {
|
// The open book's text blocks (EPUB_BLOCK_SELECTOR), with the chapter each one
|
||||||
|
// belongs to — found once and kept until the book's content changes (a
|
||||||
|
// highlight wrapping text can turn a leaf div into a non-leaf one). Selecting
|
||||||
|
// them is the expensive part on an e-reader: Chrome 61 has no :has(), so
|
||||||
|
// compat61.js filters every div in JavaScript.
|
||||||
|
let _blockCache = null; // {content, blocks, chapterOf: Int32Array, chapters: Element[]}
|
||||||
|
|
||||||
|
function _epubBlocks(contentEl) {
|
||||||
|
if (_blockCache && _blockCache.content === contentEl) return _blockCache;
|
||||||
const blocks = Array.from(contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR));
|
const blocks = Array.from(contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR));
|
||||||
|
const chapters = Array.from(contentEl.querySelectorAll('[data-epub-src]'));
|
||||||
|
const chapterOf = new Int32Array(blocks.length);
|
||||||
|
for (let i = 0, ch = -1; i < blocks.length; i++) {
|
||||||
|
while (ch + 1 < chapters.length && chapters[ch + 1].compareDocumentPosition(blocks[i]) & Node.DOCUMENT_POSITION_FOLLOWING) ch++;
|
||||||
|
chapterOf[i] = ch;
|
||||||
|
}
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (_blockCache && _blockCache.content === contentEl) _blockCache = null;
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
observer.observe(contentEl, {childList: true, subtree: true});
|
||||||
|
_blockCache = {content: contentEl, blocks, chapterOf, chapters};
|
||||||
|
return _blockCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The anchor of the reading position: "block:inner" — the last block (with a
|
||||||
|
// height) that starts at or above the top of the view, the first of several
|
||||||
|
// starting at the same height; else the first block below. Blocks are in
|
||||||
|
// document order and so top to bottom: a binary search over the chapters on
|
||||||
|
// screen (the e-reader mode shows one), not every block from the book's start.
|
||||||
|
function getPositionAnchor(contentEl) {
|
||||||
|
const {blocks, chapterOf, chapters} = _epubBlocks(contentEl);
|
||||||
if (!blocks.length) return '';
|
if (!blocks.length) return '';
|
||||||
const containerTop = contentEl.getBoundingClientRect().top;
|
const containerTop = contentEl.getBoundingClientRect().top;
|
||||||
const containerBottom = containerTop + contentEl.clientHeight;
|
const containerBottom = containerTop + contentEl.clientHeight;
|
||||||
let bestIndex = 0;
|
// Index ranges of the blocks in chapters that are laid out (all, when reading by scrolling).
|
||||||
let bestDelta = Infinity;
|
const shown = new Set();
|
||||||
|
chapters.forEach((ch, i) => { if (ch.getClientRects().length) shown.add(i); });
|
||||||
|
const order = [];
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
const rect = blocks[i].getBoundingClientRect();
|
if (chapterOf[i] < 0 || shown.has(chapterOf[i])) {
|
||||||
if (rect.height < 1) continue;
|
let j = i;
|
||||||
if (rect.top > containerBottom) break;
|
while (j + 1 < blocks.length && chapterOf[j + 1] === chapterOf[i]) j++;
|
||||||
const delta = rect.top - containerTop;
|
order.push([i, j]);
|
||||||
if (delta <= 0 && Math.abs(delta) < Math.abs(bestDelta)) {
|
i = j;
|
||||||
bestIndex = i;
|
|
||||||
bestDelta = delta;
|
|
||||||
} else if (delta > 0 && bestDelta === Infinity) {
|
|
||||||
bestIndex = i;
|
|
||||||
bestDelta = delta;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const count = order.reduce((n, r) => n + r[1] - r[0] + 1, 0);
|
||||||
|
const at = k => { // k-th block on screen -> block index
|
||||||
|
for (const [a, b] of order) { if (k <= b - a) return a + k; k -= b - a + 1; }
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
const rectOf = k => blocks[at(k)].getBoundingClientRect();
|
||||||
|
// A block without height says nothing about the position: use the next one with a height.
|
||||||
|
const measured = k => {
|
||||||
|
for (let n = k; n < count; n++) { const r = rectOf(n); if (r.height >= 1) return {k: n, top: r.top}; }
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
// Binary search for the last block with a height whose top <= containerTop.
|
||||||
|
let lo = 0, hi = count - 1, best = -1;
|
||||||
|
while (lo <= hi) {
|
||||||
|
const mid = (lo + hi) >> 1;
|
||||||
|
const m = measured(mid);
|
||||||
|
if (m && m.top <= containerTop) { best = m.k; lo = m.k + 1; }
|
||||||
|
else hi = mid - 1;
|
||||||
|
}
|
||||||
|
let bestIndex;
|
||||||
|
if (best >= 0) {
|
||||||
|
// Several blocks starting at the same height (a blockquote and its first paragraph): the first.
|
||||||
|
const top = rectOf(best).top;
|
||||||
|
let k = best;
|
||||||
|
for (let n = best - 1; n >= 0; n--) {
|
||||||
|
const r = rectOf(n);
|
||||||
|
if (r.height < 1) continue;
|
||||||
|
if (r.top !== top) break;
|
||||||
|
k = n;
|
||||||
|
}
|
||||||
|
bestIndex = at(k);
|
||||||
|
} else {
|
||||||
|
const first = measured(0);
|
||||||
|
bestIndex = first && first.top <= containerBottom ? at(first.k) : 0;
|
||||||
|
}
|
||||||
const rect = blocks[bestIndex].getBoundingClientRect();
|
const rect = blocks[bestIndex].getBoundingClientRect();
|
||||||
const innerFraction = Math.max(0, Math.min(1, (containerTop - rect.top) / (rect.height || 1)));
|
const innerFraction = Math.max(0, Math.min(1, (containerTop - rect.top) / (rect.height || 1)));
|
||||||
return `${bestIndex}:${innerFraction.toFixed(6)}`;
|
return `${bestIndex}:${innerFraction.toFixed(6)}`;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* diora service worker — caches the app shell for offline use.
|
* diora service worker — caches the app shell for offline use.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE = 'diora-v97';
|
const CACHE = 'diora-v98';
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
'/static/css/app.css',
|
'/static/css/app.css',
|
||||||
'/static/js/app.js',
|
'/static/js/app.js',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue