michaschwab/easypz

zoom is unusably faster in chrome than ff

ancms2600 opened this issue · 6 comments

nice lib.

here's my hack for trying to even out scroll speed between browsers when using touchpads:

@@ -945,7 +945,15 @@ EasyPZ.addMode(function (easypz) {
             var delta = eventData.event.wheelDelta ? eventData.event.wheelDelta : -1 * eventData.event.deltaY;
             var change = delta / Math.abs(delta);
             var zoomingIn = change > 0;
-            var scale = zoomingIn ? mode.settings.zoomInScaleChange : mode.settings.zoomOutScaleChange;
+            // Only Firefox seems to use the line unit (which we assume to
+            // be 25px), otherwise the delta is already measured in pixels.
+            var scale;
+            if (eventData.event.deltaMode === 1) {
+                scale = zoomingIn ? 0.92 : 1.08;
+            }
+            else {
+                scale = zoomingIn ? 0.98 : 1.02;
+            }
             var relativeScale = 1 - scale;
             var absScale = Math.abs(relativeScale) * mode.settings.momentumSpeedPercentage;
             var scaleSign = sign(relativeScale);

you can probably make it cleaner. but its normalizing the scale between browsers by checking WheelEvent.deltaMode.

see related:
weaveworks/scope#2788
https://github.com/weaveworks/scope/blob/d8ffea47815559ed908dd04f8593408ecb1e5b87/client/app/scripts/utils/zoom-utils.js
https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode

also you are doing event handling wrong.
in FF, events are emitted much less frequently than Chrome.
Chrome literally floods with events. You should be _.debounce()ing them.
see also:
https://stackoverflow.com/a/25991510
http://demo.nimius.net/debounce_throttle/

also, you should not be doing the work inside the event callback. the callback should just be setting a few calculation values like delta, and you should have a separate function doing the work at a controlled (probably 12-24fps) frame rate (using requestAnimationFrame(), or setTimeout if you are a savage).

sorry i don't have time to make a PR. hope this helps :)

better version

            var delta = eventData.event.wheelDelta ? eventData.event.wheelDelta : -1 * eventData.event.deltaY;
            // Only Firefox seems to use the line unit (which we assume to
            // be 25px), otherwise the delta is already measured in pixels.
            var scale = 1 + (-1 * ((delta * (eventData.event.deltaMode === 1 ? 25 : 1)) / 1000));

implemented as Mithril.js component:

Components.PanZoom = {
    LEFT_CLICK: 0,
    MIDDLE_CLICK: 1,
    RIGHT_CLICK: 2,

    oninit(v) {
        v.state.pointer = {};
        v.state.translateX = 0;
        v.state.translateY = 0;
        v.state.scale = 1.0;
    },

    oncreate(v) {
        const fn = Components.PanZoom.handleEvent.bind(null,v);
        v.dom.addEventListener('mousemove',   fn, { passive: true });
        v.dom.addEventListener('mousedown',   fn);
        v.dom.addEventListener('mouseup',     fn, { capture: true });
        v.dom.addEventListener('wheel',       fn, { capture: true });
        v.dom.addEventListener('contextmenu', fn);
        v.dom.addEventListener('touchmove',   fn);
        v.dom.addEventListener('touchstart',  fn);
        v.dom.addEventListener('touchend',    fn, { passive: true });
    },

    handleEvent(v,e) {
        if (true === v.attrs.debug && 'mousemove' !== e.type && 'touchmove' !== e.type) {
            console.debug(`PanZoom debug ${e.type}`, e);
        }
        if ('mousemove' === e.type || 'mousedown' === e.type || 'touchmove' === e.type) {
            let x,y;
            if ('touchmove' === e.type) {
                x = e.changedTouches[0].clientX;
                y = e.changedTouches[0].clientY;
                // prevent page drag
                e.preventDefault();
            }
            else {
                x = e.clientX;
                y = e.clientY;
            }
            v.state.pointer.lastX = v.state.pointer.x;
            v.state.pointer.lastY = v.state.pointer.y;
            v.state.pointer.x = x;
            v.state.pointer.y = y;
            v.state.pointer.deltaX = v.state.pointer.x - v.state.pointer.lastX;
            v.state.pointer.deltaY = v.state.pointer.y - v.state.pointer.lastY;
            if (null != v.state.pointer.panningBy) {
                v.state.translateX += (v.state.pointer.deltaX || 0);
                v.state.translateY += (v.state.pointer.deltaY || 0);
                Components.PanZoom.update(v);
            }
            else if (true === v.attrs.debug) {
                m.redraw();
            }
        }
        if (('mousedown' === e.type || 'touchstart' === e.type) && v.dom === e.target) {
            v.state.pointer.button = e.button;
            if (Components.PanZoom.LEFT_CLICK === e.button || Components.PanZoom.MIDDLE_CLICK === e.button || 'touchstart' === e.type) {
                v.state.pointer.panningBy  = e.type;
                let x,y;
                if ('touchstart' === e.type) {
                    x = e.changedTouches[0].clientX;
                    y = e.changedTouches[0].clientY;
                }
                else {
                    x = e.clientX;
                    y = e.clientY;
                    // prevent text selection
                    e.preventDefault();
                }
                v.state.pointer.lastX = x;
                v.state.pointer.lastY = y;
                v.state.pointer.x = x;
                v.state.pointer.y = y;
                v.state.pointer.deltaX = 0;
                v.state.pointer.deltaY = 0;
                Components.PanZoom.update(v);
            }
        }
        else if ('mouseup' === e.type || 'touchend' === e.type) {
            if ('mouseup'  === e.type) {
                v.state.pointer.button = undefined;
            }
            if (
                ('mouseup'  === e.type && 'mousedown'  === v.state.pointer.panningBy) ||
                ('touchend' === e.type && 'touchstart' === v.state.pointer.panningBy)
            ) {
                v.state.pointer.panningBy  = undefined;
            }
        }
        else if ('wheel' === e.type) {
            // prevent page scroll
            e.preventDefault();
            v.state.scale = Utils.clamp(v.state.scale + (
                (
                    (e.wheelDelta ? e.wheelDelta : -e.deltaY) *
                    // Only Firefox seems to use the line unit (which we assume to be 25px),
                    // otherwise the delta is already measured in pixels.
                    (e.deltaMode === 1 ? 25 : 1)
                ) / 1000
            ), 0.2, 2);
            Components.PanZoom.update(v);
        }
        else if ('contextmenu' === e.type && v.dom === e.target) {
            // disable right-click menu
            e.preventDefault();
        }
    },

    update(v) {
        v.state.lens.dom.style.transform =
            `matrix(${v.state.scale}, 0, 0, ${v.state.scale}, ${v.state.translateX}, ${v.state.translateY})`;
    },

    view(v) {
        return m('.pan-zoom-container', v.state.lens = m('.pan-zoom-lens',
            (true === v.attrs.debug &&
                m('pre.debug', JSON.stringify(_.pick(v.state, ['pointer','translateX','translateY','scale']), null, 2))),
            v.children));
    },
};

Thank you, this looks promising! I will take a look next week.

If you'd like to see changes regarding the event handling and timing, feel free to create a separate issue for that. I am well aware of issues when heavy work is put on events rather than animation frames, but I'm not sure that's the case here so for the time being I'm ok with the event handling as-is.

I've played with deltaMode a little and it's not entirely clear to me when the delta is supposed to be different. I've been able to reproduce the faster zoom on Firefox, but only when using a track pad to zoom, and not a scroll wheel. Is this the use case? A more concise example of the problem, e.g. on jsfiddle, would make your point much more understandable.

Yes correct. Different input devices have different scale resolutions in different browsers. This is by design so any software not accounting for it is going to provide an experience that is not consistent for all users.

It's not well documented but some googling regarding the purpose of WheelEvent.deltaMode may help. Also see my prior links to PRs in other projects which were eventually forced to deal with this same issue.

It was also unusable for me on a trackpad in Safari without the fix mentioned above.