michaschwab / easypz

Easy Pan and Zoom JS Library
https://easypz.io
132 stars 16 forks source link

zoom is unusably faster in chrome than ff #4

Open ancms2600 opened 5 years ago

ancms2600 commented 5 years ago

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: https://github.com/weaveworks/scope/pull/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 :)

ancms2600 commented 5 years ago

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));
ancms2600 commented 5 years ago

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));
    },
};
michaschwab commented 5 years ago

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

michaschwab commented 5 years ago

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.

ancms2600 commented 5 years ago

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.

mcdemarco commented 4 years ago

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