Skip to content

Bug: Wheel Zoom-Out Delta Overcompensation #252

@Copystrike

Description

@Copystrike

Description

When using the mouse wheel to zoom out, the canvas scale decreases too much, sometimes jumping to very small. This happens because the zoom-out calculation depends on the current scale, making the decrease too aggressive.

For example, at 100% scale, a single scroll can drop the scale from 1 to 0, making the canvas disappear. However, zooming in (scroll up) works correctly, increasing the scale smoothly.

The issue:
Image

Expected result

Zooming out with the mouse wheel should decrease the canvas scale incrementally and symmetrically to zooming in, but using an exponential scale approach.

For example, starting at a scale of 100%:

Scroll up: Increase exponentially by a fixed factor (e.g., 10% → 20% → 40% → 80%).
Scroll down: Decrease exponentially by the same fixed factor (e.g., 80 → 40% → 20% → 10%)

Actual result

Zooming out with the mouse wheel reduces the scale drastically in one step, proportional to the current scale, often resulting in complete zoom-out:

At 100%: Zooms out to 10%
At 50%: Zooms out to 10%

Zooming in works as expected, with exponentially increases.

Which browsers are you seeing the problem on?

Microsoft Edge

Additional details

As a reverse engineer, I decided to investigate this issue using DevTools since I really like this project—but the zooming behavior was frustrating.

Fix Implemented:

I updated wheelChange to use an exponential zooming approach, doubling the scale when zooming in and halving it when zooming out. Instead of applying a scale factor based on the current offset, the new implementation calculates the target scale and adjusts _delta accordingly.

Additionally, I added a safeguard to ensure the scale never drops below 0.1, preventing invalid values or sudden jumps to near-zero. The updated logic ensures smooth and predictable zooming behavior:

https://stately.ai/registry/_next/static/chunks/1339-d0e529723d5c15c1.js

 wheelChange(e) {
     let t = "uv" in e;
     !t && e.cancelable && e.preventDefault();
     let n = this.state;
-    n._delta = [-eU(e)[1] / 100 * n.offset[0], 0],
+    const direction = -eU(e)[1] > 0 ? 1 : -1; // 1 for zoom in, -1 for zoom out
+    const newScale = direction > 0 ? n.offset[0] * 2 : n.offset[0] / 2; // Double or halve
+    n._delta = [newScale - n.offset[0], 0]; // Difference to reach target scale
+    n._delta[0] = Math.max(n._delta[0], -n.offset[0] + 0.1); // Prevent scale ≤ 0, min 0.1
     eE.addTo(n._movement, n._delta),
     e4(n),
     this.state.origin = [e.clientX, e.clientY],
     this.compute(e),
     this.emit()
 }

OR a oneline solution by Grok;

    wheelChange(e) {
        let t = "uv" in e;
        !t && e.cancelable && e.preventDefault();
        let n = this.state;
-       n._delta = [-eU(e)[1] / 100 * n.offset[0], 0],
+       n._delta = [Math.max((-eU(e)[1] > 0 ? n.offset[0] * 2 : n.offset[0] / 2) - n.offset[0], -n.offset[0] + 0.1), 0]
        eE.addTo(n._movement, n._delta);
        e4(n);
        this.state.origin = [e.clientX, e.clientY];
        this.compute(e);
        this.emit();
    }

An example of the result:
Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions