feat(pdf): Finger und Mausrad zoomen in Zehnteln statt in Vierteln

Die Leiter der Knöpfe (0,5 / 0,75 / 1 / 1,25 / …) ist für eine Geste zu
grob – man schießt über die Größe hinaus, die man gerade sucht. Finger
und Mausrad gehen jetzt zehn Prozent pro Schritt, die Knöpfe behalten
ihre Leiter: Ein Klick soll etwas bewegen, eine Geste ist ein Regler.

Multiplikativ, nicht in festen Prozentpunkten: 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. Auf ganze Prozent
gerundet, sonst stünde 71,50000000000001 % in der Leiste.

Gemessen, dieselbe Geste wie vorher: 65 % -> 116 % statt 65 % -> 125 %,
zurück auf 71 % statt 50 %.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-31 21:17:57 +02:00
1 parent 574ba3e6c5
commit 0723d853f0
1 file changed
+40 -10
+40 -10
View File
@@ -39,9 +39,26 @@ pdfjs.GlobalWorkerOptions.workerSrc = workerUrl
*/ */
const standardFontDataUrl = '/pdfjs/standard_fonts/' 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] 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 * 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 * 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) : [...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) { function zoom(dir: 1 | -1) {
const next = nextStep(effScale, dir) const next = nextStep(effScale, dir)
if (next) { keepPage.current = current; setScale(next) } if (next) { keepPage.current = current; setScale(next) }
@@ -656,7 +682,7 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) {
const root2 = scrollRef.current const root2 = scrollRef.current
if (!root2) return if (!root2) return
const dir: 1 | -1 = e.deltaY < 0 ? 1 : -1 const dir: 1 | -1 = e.deltaY < 0 ? 1 : -1
const next = nextStep(effScale, dir) const next = nextFine(effScale, dir)
if (!next) return if (!next) return
const r = root2.getBoundingClientRect() const r = root2.getBoundingClientRect()
const cx = e.clientX - r.left 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 * einem Finger bleibt erlaubt), und dieser Zuhörer setzt an ihre Stelle
* unsere Stufen. * unsere Stufen.
* *
* **Stufen statt stufenlos.** Jede Zwischengröße hieße, alle sichtbaren * **In Zehnteln, nicht stufenlos.** Jede Zwischengröße hieße, alle
* Seiten neu zu zeichnen – auf einem Telefon zwanzigmal während einer * sichtbaren Seiten neu zu zeichnen – auf einem Telefon Dutzende Male
* einzigen Geste. Deshalb rastet der Zoom auf dieselbe Leiter wie die * während einer einzigen Geste. Der Zoom rastet deshalb auf Zehntel des
* Knöpfe, sobald der Abstand der Finger um ein Viertel gewachsen oder um * aktuellen Werts, sobald der Fingerabstand um acht Prozent gewachsen oder
* ein Fünftel geschrumpft ist. Danach zählt der neue Abstand als Anfang, * um sieben geschrumpft ist. Danach zählt der neue Abstand als Anfang, eine
* eine lange Geste geht also über mehrere Stufen weiter. * 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(() => { useEffect(() => {
const root = scrollRef.current const root = scrollRef.current
@@ -712,13 +742,13 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) {
e.preventDefault() e.preventDefault()
const now = distance(e.touches) const now = distance(e.touches)
const ratio = now / base 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 if (!dir) return
pinchBase.current = now pinchBase.current = now
const root2 = scrollRef.current const root2 = scrollRef.current
if (!root2) return if (!root2) return
const from = scaleRef.current const from = scaleRef.current
const next = nextStep(from, dir) const next = nextFine(from, dir)
if (!next) return if (!next) return
// Festgehalten wird die Mitte zwischen den Fingern – dieselbe Regel wie // Festgehalten wird die Mitte zwischen den Fingern – dieselbe Regel wie
// beim Mausrad, nur dass der Zeiger hier aus zwei Punkten entsteht. // beim Mausrad, nur dass der Zeiger hier aus zwei Punkten entsteht.