diff --git a/frontend/src/components/PdfViewer.tsx b/frontend/src/components/PdfViewer.tsx index 4657a9c..3b4d275 100644 --- a/frontend/src/components/PdfViewer.tsx +++ b/frontend/src/components/PdfViewer.tsx @@ -39,9 +39,26 @@ pdfjs.GlobalWorkerOptions.workerSrc = workerUrl */ const standardFontDataUrl = '/pdfjs/standard_fonts/' -/** Zoomstufen des Knopfes. „Fit" liegt außerhalb und wird gerechnet. */ +/** Zoomstufen der **Knöpfe**. „Fit" liegt außerhalb und wird gerechnet. */ const zoomSteps = [0.5, 0.75, 1, 1.25, 1.5, 2, 3] +/** + * Stufenweite für **stufenloses** Zoomen: Finger und Mausrad gehen in + * Zehnteln, nicht auf der Leiter der Knöpfe. + * + * Der Unterschied ist gewollt. Ein Klick soll etwas bewegen – von 100 auf + * 110 % zu springen wäre ein Knopf, den man fünfmal drückt. Eine Geste + * dagegen ist ein Regler: Dort sind Viertelsprünge zu grob, man schießt über + * die Größe hinaus, die man eigentlich wollte. + * + * Multiplikativ statt in festen Prozentpunkten, weil Zoomen relativ ist: Von + * 50 auf 60 % ist ein Fünftel mehr, von 300 auf 310 % ein Dreißigstel. Zehn + * Prozent *vom aktuellen Wert* fühlen sich überall gleich an. + */ +const zoomFactor = 1.1 +const minScale = 0.25 +const maxScale = 4 + /** * Innenabstand der Seitenliste. Am Telefon fast keiner: Dort ist die Breite * das knappe Gut, und ein 16-Pixel-Rand links und rechts kostet zusammen mehr @@ -633,6 +650,15 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) { : [...zoomSteps].reverse().find(s => s < now - 0.001) } + /** Ein Zehntel weiter, für Finger und Mausrad. Undefined am Anschlag. */ + function nextFine(now: number, dir: 1 | -1): number | undefined { + const raw = dir > 0 ? now * zoomFactor : now / zoomFactor + // Auf ganze Prozent gerundet: Sonst stünden 71,50000000000001 % in der + // Leiste, und zwei Wege zum selben Zoom ergäben zwei verschiedene Zahlen. + const next = Math.round(Math.min(maxScale, Math.max(minScale, raw)) * 100) / 100 + return Math.abs(next - now) < 0.005 ? undefined : next + } + function zoom(dir: 1 | -1) { const next = nextStep(effScale, dir) if (next) { keepPage.current = current; setScale(next) } @@ -656,7 +682,7 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) { const root2 = scrollRef.current if (!root2) return const dir: 1 | -1 = e.deltaY < 0 ? 1 : -1 - const next = nextStep(effScale, dir) + const next = nextFine(effScale, dir) if (!next) return const r = root2.getBoundingClientRect() const cx = e.clientX - r.left @@ -684,12 +710,16 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) { * einem Finger bleibt erlaubt), und dieser Zuhörer setzt an ihre Stelle * unsere Stufen. * - * **Stufen statt stufenlos.** Jede Zwischengröße hieße, alle sichtbaren - * Seiten neu zu zeichnen – auf einem Telefon zwanzigmal während einer - * einzigen Geste. Deshalb rastet der Zoom auf dieselbe Leiter wie die - * Knöpfe, sobald der Abstand der Finger um ein Viertel gewachsen oder um - * ein Fünftel geschrumpft ist. Danach zählt der neue Abstand als Anfang, - * eine lange Geste geht also über mehrere Stufen weiter. + * **In Zehnteln, nicht stufenlos.** Jede Zwischengröße hieße, alle + * sichtbaren Seiten neu zu zeichnen – auf einem Telefon Dutzende Male + * während einer einzigen Geste. Der Zoom rastet deshalb auf Zehntel des + * aktuellen Werts, sobald der Fingerabstand um acht Prozent gewachsen oder + * um sieben geschrumpft ist. Danach zählt der neue Abstand als Anfang, eine + * lange Geste geht also über mehrere Stufen weiter. + * + * Die Leiter der Knöpfe (0,5 / 0,75 / 1 / …) wäre hier zu grob: Ein + * Viertelsprung schießt über die Größe hinaus, die man mit den Fingern + * gerade sucht. */ useEffect(() => { const root = scrollRef.current @@ -712,13 +742,13 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) { e.preventDefault() const now = distance(e.touches) const ratio = now / base - const dir: 1 | -1 | 0 = ratio > 1.25 ? 1 : ratio < 0.8 ? -1 : 0 + const dir: 1 | -1 | 0 = ratio > 1.08 ? 1 : ratio < 0.93 ? -1 : 0 if (!dir) return pinchBase.current = now const root2 = scrollRef.current if (!root2) return const from = scaleRef.current - const next = nextStep(from, dir) + const next = nextFine(from, dir) if (!next) return // Festgehalten wird die Mitte zwischen den Fingern – dieselbe Regel wie // beim Mausrad, nur dass der Zeiger hier aus zwei Punkten entsteht.