diff --git a/frontend/src/components/PdfViewer.tsx b/frontend/src/components/PdfViewer.tsx index 7ad5f6d..b8b83ad 100644 --- a/frontend/src/components/PdfViewer.tsx +++ b/frontend/src/components/PdfViewer.tsx @@ -43,24 +43,19 @@ const standardFontDataUrl = '/pdfjs/standard_fonts/' const zoomSteps = [0.5, 0.75, 1, 1.25, 1.5, 2, 3] /** - * Stufenweite für **stufenloses** Zoomen: Finger und Mausrad gehen in - * Zwanzigsteln, nicht auf der Leiter der Knöpfe. + * Zoomschritt für Finger und Mausrad: **fünf Prozentpunkte**, auf runde + * Zahlen gerastet – 65, 70, 75, 80 … * - * Der Unterschied ist gewollt. Ein Klick soll etwas bewegen – von 100 auf - * 105 % zu springen wäre ein Knopf, den man zehnmal 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. + * Zuerst war es ein fester Faktor (fünf Prozent *vom aktuellen Wert*). + * Rechnerisch ist das die sauberere Größe: Sie fühlt sich unten wie oben + * gleich an. Abzulesen war sie aber nicht – die Anzeige sprang um drei + * Punkte, dann um vier, dann um sechs. Wer eine Zahl sieht, erwartet ein + * Raster, und das hier ist eines. * - * Multiplikativ statt in festen Prozentpunkten, weil Zoomen relativ ist: Von - * 50 auf 55 % ist ein Zehntel mehr, von 300 auf 305 % ein Sechzigstel. Fünf - * Prozent *vom aktuellen Wert* fühlen sich überall gleich an. - * - * **Achtung beim Ablesen:** Die angezeigte Zahl springt deshalb nicht um - * feste fünf Punkte. Bei 65 % sind fünf Prozent gut drei Punkte, bei 200 % - * sind es zehn. Erst 1,1 war zu grob (gemessen bei 65 % ein Sprung von sieben - * Punkten, kurz darauf von elf). + * Der Preis steht dazu: Bei 400 % ist ein Schritt gut ein Prozent, bei 25 % + * ein Fünftel. Das gleicht die Geste aus, nicht die Rasterung – siehe unten. */ -const zoomFactor = 1.05 +const zoomStepPercent = 5 const minScale = 0.25 const maxScale = 4 @@ -655,13 +650,28 @@ 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. */ + /** + * Die nächste runde Fünferzahl, für Finger und Mausrad. Undefined am + * Anschlag. + * + * Gerechnet wird in Prozentpunkten und nicht in Bruchteilen: Der Nutzer + * liest Prozente, und ein Raster, das nur *fast* auf 70 trifft, ist keins. + * Aus einer krummen Ausgangsgröße – „einpassen" ergibt zum Beispiel 65,3 % + * – führt der erste Schritt auf die nächste runde Zahl, danach geht es in + * Fünfern weiter. + */ 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 + // **Gerundet, bevor gerechnet wird.** 1,1 × 100 ergibt in Fließkomma + // 110.00000000000001, und daraus wird die „nächste kleinere Fünferzahl" + // wieder 110 – der Zoom blieb dann stehen. Gemessen: Beim Zuziehen kam + // genau eine Stufe, danach nichts mehr. + const p = Math.round(now * 100 * 1e6) / 1e6 + const step = zoomStepPercent + const raw = dir > 0 + ? Math.floor(p / step + 1) * step + : Math.ceil(p / step - 1) * step + const next = Math.min(maxScale * 100, Math.max(minScale * 100, raw)) / 100 + return Math.abs(next - now) < 0.001 ? undefined : next } function zoom(dir: 1 | -1) { @@ -750,17 +760,31 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) { e.preventDefault() const now = distance(e.touches) const ratio = now / base - // Die Schwelle folgt der Stufenweite: Wandern die Finger um eine - // Stufe auseinander, gibt es eine Stufe. Bliebe sie größer, ließe sich - // die feinere Weite gar nicht mehr treffen. - const dir: 1 | -1 | 0 = ratio > 1.04 ? 1 : ratio < 0.96 ? -1 : 0 + const dir: 1 | -1 | 0 = ratio > 1 ? 1 : ratio < 1 ? -1 : 0 if (!dir) return - pinchBase.current = now - const root2 = scrollRef.current - if (!root2) return const from = scaleRef.current const next = nextFine(from, dir) if (!next) return + + /** + * **Die Finger geben den Maßstab vor, nicht eine feste Schwelle.** + * + * Fünf Prozentpunkte sind bei 25 % ein Fünftel und bei 400 % ein + * Hundertstel. Eine feste Schwelle wäre deshalb an einem Ende zu träge + * und am anderen zu nervös: Von 100 auf 400 zu ziehen dauerte ewig, + * unten spränge es bei der kleinsten Bewegung. + * + * Verlangt wird stattdessen genau die Bewegung, die dem nächsten + * Schritt entspricht – zwei Prozent Fingerweg als Untergrenze, damit + * ein Zittern in der Hand nicht schon zoomt. + */ + const needed = next / from + if (dir > 0 && ratio < Math.max(1.02, needed)) return + if (dir < 0 && ratio > Math.min(0.98, needed)) return + + pinchBase.current = now + const root2 = scrollRef.current + if (!root2) return // Festgehalten wird die Mitte zwischen den Fingern – dieselbe Regel wie // beim Mausrad, nur dass der Zeiger hier aus zwei Punkten entsteht. const r = root2.getBoundingClientRect()