Reader: j/k-Scrollen ohne Ramp-up, an feste Kadenz statt Display-Hz gekoppelt (SW v34)
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 17s

Die requestAnimationFrame-Schleife aus dem letzten Fix lief mit der tatsächlichen
Bildwiederholrate des Displays statt einer festen Rate — auf Laptops mit
120/144/165Hz-Panels bedeutete das 2-3x mehr scrollBy-Aufrufe (und damit
Scroll-Listener-Durchläufe) pro Sekunde als beabsichtigt, was sich als Lag
statt als Beschleunigung äußerte. Zurück zu setInterval, aber mit fester
20ms-Kadenz unabhängig vom Display und dt-kompensierter, konstanter
Geschwindigkeit (~1200px/s, kein Ramp-up mehr).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-08-14 23:50:23 +02:00
parent e70bb881cc
commit 5053d0036a
2 changed files with 20 additions and 23 deletions

View file

@ -2240,13 +2240,10 @@ document.addEventListener('keydown', e => {
const continuousKeys = {'j': 1, 'k': -1}; const continuousKeys = {'j': 1, 'k': -1};
if (continuousKeys[e.key] !== undefined && !e.repeat) { if (continuousKeys[e.key] !== undefined && !e.repeat) {
e.preventDefault(); e.preventDefault();
const dir = continuousKeys[e.key]; _readerScrollDir = continuousKeys[e.key];
const now = performance.now(); if (!_readerScrollInterval) {
if (_readerScrollDir !== dir) _readerScrollStartTs = now; _readerScrollLastTs = performance.now();
_readerScrollDir = dir; _readerScrollInterval = setInterval(_readerScrollTick, READER_SCROLL_TICK_MS);
_readerScrollLastTs = now;
if (!_readerScrollRaf) {
_readerScrollRaf = requestAnimationFrame(_readerScrollTick);
} }
} }
if (e.key === 'd') { e.preventDefault(); contentEl.scrollBy({top: large / 2, behavior: 'smooth'}); } if (e.key === 'd') { e.preventDefault(); contentEl.scrollBy({top: large / 2, behavior: 'smooth'}); }
@ -2266,8 +2263,8 @@ document.addEventListener('keydown', e => {
document.addEventListener('keyup', e => { document.addEventListener('keyup', e => {
if (e.key === 'j' || e.key === 'k') { if (e.key === 'j' || e.key === 'k') {
if (_readerScrollRaf) cancelAnimationFrame(_readerScrollRaf); clearInterval(_readerScrollInterval);
_readerScrollRaf = null; _readerScrollInterval = null;
_readerScrollDir = 0; _readerScrollDir = 0;
} }
}); });
@ -3189,21 +3186,21 @@ let pdfTotalPages = 0;
let _pdfPageTextBoxCache = {}; let _pdfPageTextBoxCache = {};
let _pdfRenderGen = 0; let _pdfRenderGen = 0;
let _touchStartX = 0; let _touchStartX = 0;
let _readerScrollRaf = null; let _readerScrollInterval = null;
let _readerScrollDir = 0; let _readerScrollDir = 0;
let _readerScrollStartTs = 0;
let _readerScrollLastTs = 0; let _readerScrollLastTs = 0;
const READER_SCROLL_MIN_SPEED = 700; // px/s right as j/k is pressed const READER_SCROLL_SPEED = 1200; // constant px/s, no ramp-up
const READER_SCROLL_MAX_SPEED = 1800; // px/s once ramped up const READER_SCROLL_TICK_MS = 20; // fixed cadence, independent of the
const READER_SCROLL_RAMP_MS = 300; // time to reach max speed // display's refresh rate — rAF runs at
function _readerScrollTick(ts) { // that rate (120/144/165Hz on plenty of
const dt = ts - _readerScrollLastTs; // laptops), which was driving 2-3x more
_readerScrollLastTs = ts; // scrollBy + scroll-listener work per
const held = ts - _readerScrollStartTs; // second than intended and read as lag.
const t = Math.min(held / READER_SCROLL_RAMP_MS, 1); function _readerScrollTick() {
const speed = READER_SCROLL_MIN_SPEED + (READER_SCROLL_MAX_SPEED - READER_SCROLL_MIN_SPEED) * t; const now = performance.now();
$('reader-content')?.scrollBy({top: _readerScrollDir * speed * dt / 1000}); const dt = now - _readerScrollLastTs;
_readerScrollRaf = requestAnimationFrame(_readerScrollTick); _readerScrollLastTs = now;
$('reader-content')?.scrollBy({top: Math.round(_readerScrollDir * READER_SCROLL_SPEED * dt / 1000)});
} }
let _pinchStartDist = 0; let _pinchStartDist = 0;
let _pinchStartZoom = 100; let _pinchStartZoom = 100;

View file

@ -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-v33'; const CACHE = 'diora-v34';
const PODCAST_CACHE = 'diora-podcast-v1'; const PODCAST_CACHE = 'diora-podcast-v1';
const SHELL = [ const SHELL = [
'/static/css/app.css', '/static/css/app.css',