Skip to content

fix(annotator): a mouse wheel zooms again, and a trackpad scroll still pans - #579

Merged
JArmandoAnaya merged 1 commit into
mainfrom
fix/mouse-wheel-zoom
Aug 14, 2026
Merged

fix(annotator): a mouse wheel zooms again, and a trackpad scroll still pans#579
JArmandoAnaya merged 1 commit into
mainfrom
fix/mouse-wheel-zoom

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

What and why

#578 made every bare wheel event a pan. That is what made a laptop workable — a two-finger
scroll is how anybody moves around a canvas, and until then no gesture on a trackpad moved the
picture at all — but it took the mouse's zoom away with it: rolling the wheel scrolled the picture
up and down instead of zooming.

The two devices want opposite things from the same event, so a bare event is now answered by
device. The modifier is asked first, exactly as before:

The event What it means
ctrlKey or metaKey held zoom, anchored at the cursor
bare, and shaped like a wheel notch zoom, anchored at the cursor
bare, anything else pan, both axes

isMouseWheel is the new test, and it sits in adapters/viewport.ts beside the rest of the wheel
arithmetic, so it is unit-tested without a browser. It reads three signals and none of them is
deltaY
, which the operating system accelerates and which overlaps completely between the two
devices:

  • a deltaMode other than pixels is a discrete wheel, and nothing else reports lines or pages
    (this is Firefox's mouse);
  • anything sideways is a scroll, because a wheel has one axis;
  • otherwise a whole number of 120-unit wheelDelta notches. Chrome quantises a discrete wheel to
    units of 120 however much it accelerated deltaY, and computes a precise device's as
    -3 * deltaY.

Every uncertain case answers "trackpad." A trackpad that zooms when it was asked to scroll is
the defect #578 fixed; a mouse this declines — a Magic Mouse reports as a precise device — still
zooms with the modifier, with the widget's /+, and with mod+0 to refit. The cost of being
wrong is one stray notch inside a hard flick, never a device with no gesture at all.

Nothing else in the navigation model moves: wheelZoomFactor and its 40 px softness split,
normalizedWheel, the touch pinch, Space, the hand tool, the non-primary drag and the zoom
widget are all untouched.

Findings

CDP's synthetic wheel reports wheelDeltaY as ±120 whatever deltaY says. Measured with a
throwaway probe before writing a single scenario: page.mouse.wheel(0, 7), (0, 12) and (0, 40)
all arrive as -120, while (-90, 0) arrives as 0. So a browser suite can spell a mouse notch
and a sideways scroll, and has no spelling at all for a trackpad's vertical scroll — that half
of the predicate is unit-only, and demo.spec.ts and docs/annotations.md say so rather than
leaving a reader hunting for a scenario that cannot exist.

TypeScript 6 removed the deprecated wheelDelta* members from lib.dom.d.ts, so
event.wheelDeltaY does not typecheck. It is read through an explicit widening at the one call
site, and a browser that does not have the property lands on the pan.

Tests

Six new unit cases in viewport.test.ts, one per rule plus the two shapes each device actually
produces. All four rules mutation-verified, each turning exactly one named test red: dropping
the deltaMode clause fails the Firefox case, dropping the deltaX clause fails the sideways case,
120 → 3 fails the trackpad-quantum case, and dropping the zero-delta clause fails the
no-legacy-property case.

In demo.spec.ts the two bare-wheel scenarios were rewritten — a bare wheel now zooms, and the
sideways scroll is the one gesture that is a pan on both sides of the device test. zoomWheel in
e2e/_frame.ts still holds the modifier, so annotate.spec.ts, showcase.spec.ts,
perf.spec.ts, cycle.spec.ts and the bench needed no edit.

Checks

Locally in the worktree: scripts/check.sh frontend PASSED, scripts/check.sh browser
PASSED (270 e2e + 1 cycle), scripts/check.sh docs generated PASSED. The python stage was
not run — the diff touches zero .py files, and neither openapi.json nor the generated client
moves. No CI job was added, removed or renamed, so the main ruleset is untouched.

Verified by hand on the hardware this is about — Chrome on macOS, mouse and trackpad both.

Docs

DESIGN.md's navigation-model entry and docs/annotations.md's input-model section carry the new
split and the heuristic's stated ceiling; the shortcut sheet's Navigate section moves the scroll
wheel from panning to zooming.

…l pans

#576 gave the whole of a bare wheel event to the trackpad. That is what made a
laptop workable — a two-finger scroll is how anybody moves around a canvas, and
until then there was no gesture on a trackpad that moved the picture at all —
but it took the mouse's zoom away with it: rolling the wheel scrolled the
picture up and down.

The two devices want opposite things from the same event, so the event is now
answered by device. `isMouseWheel` decides, beside the rest of the wheel
arithmetic in `adapters/viewport.ts`, and the listener asks the modifier first
as it always did:

    ctrlKey || metaKey || isMouseWheel(...)  ->  zoom about the cursor
    otherwise                                ->  pan, both axes

It reads three signals and none of them is `deltaY`, which the operating system
accelerates and which overlaps completely between the two devices: a `deltaMode`
other than pixels is a discrete wheel, anything sideways is a scroll, and
otherwise a whole number of 120-unit `wheelDelta` notches is a wheel. Every
uncertain case answers trackpad — a trackpad that zooms when it was asked to
scroll is the defect #576 fixed, while a mouse this declines still zooms with
the modifier, the widget's buttons and `mod+0`.

Nothing else in the navigation model moves: the softness split, the pinch,
`Space`, the hand tool, the non-primary drag and the zoom widget are untouched,
and `zoomWheel` in the e2e helpers still holds the modifier, so only the two
bare-wheel scenarios in `demo.spec.ts` needed rewriting.
@JArmandoAnaya
JArmandoAnaya merged commit 0b56ac0 into main Aug 14, 2026
15 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the fix/mouse-wheel-zoom branch August 14, 2026 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant