Normalizing mousewheel speed across browsers
Asked Answered
C

10

163

For a different question I composed this answer, including this sample code.

In that code I use the mouse wheel to zoom in/out of an HTML5 Canvas. I found some code that normalizes speed differences between Chrome and Firefox. However, the zoom handling in Safari is much, much faster than in either of those.

Here's the code I currently have:

var handleScroll = function(e){
  var delta = e.wheelDelta ? e.wheelDelta/40 : e.detail ? -e.detail/3 : 0;
  if (delta) ...
  return e.preventDefault() && false;
};
canvas.addEventListener('DOMMouseScroll',handleScroll,false); // For Firefox
canvas.addEventListener('mousewheel',handleScroll,false);     // Everyone else

What code can I use to get the same 'delta' value for the same amount of mouse wheel rolling across Chrome v10/11, Firefox v4, Safari v5, Opera v11 and IE9?

This question is related, but has no good answer.

Edit: Further investigation shows that one scroll event 'up' is:

                  | evt.wheelDelta | evt.detail
------------------+----------------+------------
  Safari v5/Win7  |       120      |      0
  Safari v5/OS X  |       120      |      0
  Safari v7/OS X  |        12      |      0
 Chrome v11/Win7  |       120      |      0
 Chrome v37/Win7  |       120      |      0
 Chrome v11/OS X  |         3 (!)  |      0      (possibly wrong)
 Chrome v37/OS X  |       120      |      0
        IE9/Win7  |       120      |  undefined
  Opera v11/OS X  |        40      |     -1
  Opera v24/OS X  |       120      |      0
  Opera v11/Win7  |       120      |     -3
 Firefox v4/Win7  |    undefined   |     -3
 Firefox v4/OS X  |    undefined   |     -1
Firefox v30/OS X  |    undefined   |     -1

Further, using the MacBook trackpad on OS X gives different results even when moving slowly:

  • On Safari and Chrome, the wheelDelta is a value of 3 instead of 120 for mouse wheel.
  • On Firefox the detail is usually 2, sometimes 1, but when scrolling very slowly NO EVENT HANDLER FIRES AT ALL.

So the question is:

What is the best way to differentiate this behavior (ideally without any user agent or OS sniffing)?

Credential answered 3/4, 2011 at 4:36 Comment(9)
Sorry, I deleted my question. I'm writing up an answer right now. Before I get much further, are you talking about the scrolling on Safari on Mac OS X? When you scroll a little, it scrolls a little, but if you keep a constant rate, it progressively gets faster?Croatian
@Croatian I am testing on OS X right now, and yes, Safari is the outlier which is zooming about 20x faster than Chrome. Unfortunately I don't have a physical mouse attached, so my testing is restricted to two-finger-swipes of ≈equivalent distances and speeds.Credential
I've updated the question with details on the behavior of the top 5 browsers across OS X and Win7. It's a minefield, with Chrome on OS X appearing to be the problematic outlier.Credential
@Credential Shouldn't it be e.wheelDelta/120?Zaragoza
@ŠimeVidas Yes, the code I copied and was using was clearly wrong. You can see better code in my answer below.Credential
@Credential I can confirm the results for Win7 (Demo is here) - -3 in Firefox and 120 in all other browsers. The issue seems to be with OS X. You should try to get a confirmation from an OS X user (especially for Chrome).Zaragoza
Would be interesting to have this or another question addressing DOM3 deltaX, deltaY and deltaZ propertiesVeiled
There should be a web standard for how often and when scroll/wheel events fire for web pages.Autoxidation
developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode is the key, see @George's answerStructure
C
60

Edit September 2014

Given that:

  • Different versions of the same browser on OS X have yielded different values in the past, and may do so in the future, and that
  • Using the trackpad on OS X yields very similar effects to using a mouse wheel, yet gives very different event values, and yet the device difference cannot be detected by JS

…I can only recommend using this simple, sign-based-counting code:

var handleScroll = function(evt){
  if (!evt) evt = event;
  var direction = (evt.detail<0 || evt.wheelDelta>0) ? 1 : -1;
  // Use the value as you will
};
someEl.addEventListener('DOMMouseScroll',handleScroll,false); // for Firefox
someEl.addEventListener('mousewheel',    handleScroll,false); // for everyone else

Original attempt to be correct follows.

Here is my first attempt at a script to normalize the values. It has two flaws on OS X: Firefox on OS X will produce values 1/3 what they should be, and Chrome on OS X will produce values 1/40 what they should be.

// Returns +1 for a single wheel roll 'up', -1 for a single roll 'down'
var wheelDistance = function(evt){
  if (!evt) evt = event;
  var w=evt.wheelDelta, d=evt.detail;
  if (d){
    if (w) return w/d/40*d>0?1:-1; // Opera
    else return -d/3;              // Firefox;         TODO: do not /3 for OS X
  } else return w/120;             // IE/Safari/Chrome TODO: /3 for Chrome OS X
};

You can test out this code on your own browser here: http://phrogz.net/JS/wheeldelta.html

Suggestions for detecting and improving the behavior on Firefox and Chrome on OS X are welcome.

Edit: One suggestion from @Tom is to simply count each event call as a single move, using the sign of the distance to adjust it. This will not give great results under smooth/accelerated scrolling on OS X, nor handle perfectly cases when the mouse wheel is moved very fast (e.g. wheelDelta is 240), but these happen infrequently. This code is now the recommended technique shown at the top of this answer, for the reasons described there.

Credential answered 4/4, 2011 at 17:29 Comment(11)
@ŠimeVidas Thanks, that's basically what I have, except that I also account for the 1/3 difference on Opera OS X.Credential
@Phrogz, do you have an updated version in Sept' 2014 with all OS X /3 added ? This would be a great addition for community!Richierichlad
@Phrogz, this would be great. I don't have a Mac here to test... (I would be happy to give a bounty for that, even if I don't have much reputation myself ;))Richierichlad
@Richierichlad I have updated the test results in the original question. Given that (a) Safari on OS X now yields 12 instead of 120, while all other browsers on OS X have gotten better, and (b) there is no way to differentiate events coming from the trackpad versus a mouse, yet they give drastically different event details, I am editing my answer above to recommend the simple wheelDirection test.Credential
thanks @Credential for having investigated on what happens in OS X. The problem is that if we only use the "sign-based counting" method, it will give very different results : try my tool bigpicture.bi/demo (which uses basic counting like the one you suggest) : it works on FF, Chrome, IE (Win7). But it seems that zooming is far too fast with OS X... what do you think ?Richierichlad
@Richierichlad Yes, with the track pad you get many many events on OS X. Only other hack I can think of is to wrap the content in an infinitely-scrolling container, make the content position-fixed, and then detect the scrollOffset changes of the container.Credential
@Flek Your edit was incorrect, and drastically changed the answer. Please post a comment for such a drastic edit in the future. Note, from the "investigation" posted in the question, the wheelDelta and detail are opposite signs. The evt.detail test is necessary for FireFox.Credential
@Credential Oh I've mixed something up with my own scripts. Sorry for the confusion.Teetotum
On windows Firefox 35.0.1, wheelDelta is undefined and detail is always 0, which makes the supplied code fail.Folklore
@MaxStrater Faced the same problem, I've added "deltaY" to overcome this in direction like that (((evt.deltaY <0 || evt.wheelDelta>0) || evt.deltaY < 0) ? 1 : -1) not sure what QA finds out with that, though.Brewery
The best and most comprehensive solution is to use this code from Facebook https://mcmap.net/q/119136/-normalizing-mousewheel-speed-across-browsersPasteurizer
S
37

Our friends at Facebook put together a great solution to this problem.

I have tested on a data table that I'm building using React and it scrolls like butter!

This solution works on a variety of browsers, on Windows/Mac, and both using trackpad/mouse.

// Reasonable defaults
var PIXEL_STEP  = 10;
var LINE_HEIGHT = 40;
var PAGE_HEIGHT = 800;

function normalizeWheel(/*object*/ event) /*object*/ {
  var sX = 0, sY = 0,       // spinX, spinY
      pX = 0, pY = 0;       // pixelX, pixelY

  // Legacy
  if ('detail'      in event) { sY = event.detail; }
  if ('wheelDelta'  in event) { sY = -event.wheelDelta / 120; }
  if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
  if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }

  // side scrolling on FF with DOMMouseScroll
  if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
    sX = sY;
    sY = 0;
  }

  pX = sX * PIXEL_STEP;
  pY = sY * PIXEL_STEP;

  if ('deltaY' in event) { pY = event.deltaY; }
  if ('deltaX' in event) { pX = event.deltaX; }

  if ((pX || pY) && event.deltaMode) {
    if (event.deltaMode == 1) {          // delta in LINE units
      pX *= LINE_HEIGHT;
      pY *= LINE_HEIGHT;
    } else {                             // delta in PAGE units
      pX *= PAGE_HEIGHT;
      pY *= PAGE_HEIGHT;
    }
  }

  // Fall-back if spin cannot be determined
  if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
  if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }

  return { spinX  : sX,
           spinY  : sY,
           pixelX : pX,
           pixelY : pY };
}

The source code can be found here: https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js

Stickweed answered 9/5, 2015 at 0:50 Comment(8)
A more direct link that has not been bundled to the original code for normalizeWHeel.js github.com/facebook/fixed-data-table/blob/master/src/…Cradling
Thanks @RobinLuiten, updating original post.Stickweed
This stuff is brilliant. Just made use of it and works like a charm! Good job Facebook :)Redd
Could you give some example of how to use it? I tried it and it works in FF but not in Chrome or IE (11)..? ThanksTortuga
For anyone using npm there's a ready to use package of just this code already extracted from Facebook's Fixed Data Table. See here for more details npmjs.com/package/normalize-wheelPasteurizer
Could you enlighten why would it work? I understand the calculation and all. But not quite sure how to tie this into event handling loop. This function (as far as I can tell), simply returns an object. How does the browsers know to use this object to determine its scrolling distance?Preinstruct
This works because of developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode where deltaMode of 1 is multiplied by 40 and delta mode of 2 is multiplied by 800. 40 and 800 I believe were derived empiricallyStructure
I use this for a zoom control, but for me there is a huge difference between using the Macbook trackpad and using my Logitech MX Anywhere 2 mouse with this code.Polysyllabic
S
27

Here is my crazy attempt to produce a cross browser coherent and normalized delta ( -1 <= delta <= 1 ) :

var o = e.originalEvent,
    d = o.detail, w = o.wheelDelta,
    n = 225, n1 = n-1;

// Normalize delta
d = d ? w && (f = w/d) ? d/f : -d/1.35 : w/120;
// Quadratic scale if |d| > 1
d = d < 1 ? d < -1 ? (-Math.pow(d, 2) - n1) / n : d : (Math.pow(d, 2) + n1) / n;
// Delta *should* not be greater than 2...
e.delta = Math.min(Math.max(d / 2, -1), 1);

This is totally empirical but works quite good on Safari 6, FF 16, Opera 12 (OS X) and IE 7 on XP

Seep answered 30/11, 2012 at 17:58 Comment(6)
If I could upvote another 10 times I could. Thank you so much!Baird
Can you please have the full functional code in a demo (e.g. jsFiddle) ?Cosmotron
Is there a reason to cache the event-object in o?Irretrievable
Nope there is none. The o variable is there to show we want the original event and not a wrapped event like jQuery or other libraries may pass to event handlers.Seep
@Seep could you please explain n and n1? What are these variables for?Prairial
I think you should also declare f in the var.Outgoings
V
12

I made a table with different values returned by different events/browsers, taking into account the DOM3 wheel event that some browsers already support (table under).

Based on that I made this function to normalize the speed:

http://jsfiddle.net/mfe8J/1/

function normalizeWheelSpeed(event) {
    var normalized;
    if (event.wheelDelta) {
        normalized = (event.wheelDelta % 120 - 0) == -0 ? event.wheelDelta / 120 : event.wheelDelta / 12;
    } else {
        var rawAmmount = event.deltaY ? event.deltaY : event.detail;
        normalized = -(rawAmmount % 3 ? rawAmmount * 10 : rawAmmount / 3);
    }
    return normalized;
}

Table for mousewheel, wheel and DOMMouseScroll events:

| mousewheel        | Chrome (win) | Chrome (mac) | Firefox (win) | Firefox (mac) | Safari 7 (mac) | Opera 22 (mac) | Opera 22 (win) | IE11      | IE 9 & 10   | IE 7 & 8  |
|-------------------|--------------|--------------|---------------|---------------|----------------|----------------|----------------|-----------|-------------|-----------|
| event.detail      | 0            | 0            | -             | -             | 0              | 0              | 0              | 0         | 0           | undefined |
| event.wheelDelta  | 120          | 120          | -             | -             | 12             | 120            | 120            | 120       | 120         | 120       |
| event.wheelDeltaY | 120          | 120          | -             | -             | 12             | 120            | 120            | undefined | undefined   | undefined |
| event.wheelDeltaX | 0            | 0            | -             | -             | 0              | 0              | 0              | undefined | undefined   | undefined |
| event.delta       | undefined    | undefined    | -             | -             | undefined      | undefined      | undefined      | undefined | undefined   | undefined |
| event.deltaY      | -100         | -4           | -             | -             | undefined      | -4             | -100           | undefined | undefined   | undefined |
| event.deltaX      | 0            | 0            | -             | -             | undefined      | 0              | 0              | undefined | undefined   | undefined |
|                   |              |              |               |               |                |                |                |           |             |           |
| wheel             | Chrome (win) | Chrome (mac) | Firefox (win) | Firefox (mac) | Safari 7 (mac) | Opera 22 (mac) | Opera 22 (win) | IE11      | IE 10 & 9   | IE 7 & 8  |
| event.detail      | 0            | 0            | 0             | 0             | -              | 0              | 0              | 0         | 0           | -         |
| event.wheelDelta  | 120          | 120          | undefined     | undefined     | -              | 120            | 120            | undefined | undefined   | -         |
| event.wheelDeltaY | 120          | 120          | undefined     | undefined     | -              | 120            | 120            | undefined | undefined   | -         |
| event.wheelDeltaX | 0            | 0            | undefined     | undefined     | -              | 0              | 0              | undefined | undefined   | -         |
| event.delta       | undefined    | undefined    | undefined     | undefined     | -              | undefined      | undefined      | undefined | undefined   | -         |
| event.deltaY      | -100         | -4           | -3            | -0,1          | -              | -4             | -100           | -99,56    | -68,4 | -53 | -         |
| event.deltaX      | 0            | 0            | 0             | 0             | -              | 0              | 0              | 0         | 0           | -         |
|                   |              |              |               |               |                |                |                |           |             |           |
|                   |              |              |               |               |                |                |                |           |             |           |
| DOMMouseScroll    |              |              | Firefox (win) | Firefox (mac) |                |                |                |           |             |           |
| event.detail      |              |              | -3            | -1            |                |                |                |           |             |           |
| event.wheelDelta  |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.wheelDeltaY |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.wheelDeltaX |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.delta       |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.deltaY      |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.deltaX      |              |              | undefined     | undefined     |                |                |                |           |             |           |
Veiled answered 6/7, 2014 at 11:59 Comment(2)
Results in different scrolling speeds in current Safari and Firefox under macOS.Agapanthus
This answer is dated, as all browsers have deltaY now, plus values are not accurate for me. In Windows the deltaY is constant across wheel events, but varies per browser, while in macOS deltaY gets bigger the faster you roll the wheel (varies from 4 to 16 for a single roll click across browsers, up to 160 to 400 across browsers, on my MacBook). What a nightmare trying to get the same delta across all browser+OS combinations!Limner
I
6

Another more or less self-contained solution...

This doesn't take time between events into account though. Some browsers seem to always fire events with the same delta, and just fire them faster when scrolling quickly. Others do vary the deltas. One can imagine an adaptive normalizer that takes time into account, but that'd get somewhat involved and awkward to use.

Working available here: jsbin/iqafek/2

var normalizeWheelDelta = function() {
  // Keep a distribution of observed values, and scale by the
  // 33rd percentile.
  var distribution = [], done = null, scale = 30;
  return function(n) {
    // Zeroes don't count.
    if (n == 0) return n;
    // After 500 samples, we stop sampling and keep current factor.
    if (done != null) return n * done;
    var abs = Math.abs(n);
    // Insert value (sorted in ascending order).
    outer: do { // Just used for break goto
      for (var i = 0; i < distribution.length; ++i) {
        if (abs <= distribution[i]) {
          distribution.splice(i, 0, abs);
          break outer;
        }
      }
      distribution.push(abs);
    } while (false);
    // Factor is scale divided by 33rd percentile.
    var factor = scale / distribution[Math.floor(distribution.length / 3)];
    if (distribution.length == 500) done = factor;
    return n * factor;
  };
}();

// Usual boilerplate scroll-wheel incompatibility plaster.

var div = document.getElementById("thing");
div.addEventListener("DOMMouseScroll", grabScroll, false);
div.addEventListener("mousewheel", grabScroll, false);

function grabScroll(e) {
  var dx = -(e.wheelDeltaX || 0), dy = -(e.wheelDeltaY || e.wheelDelta || 0);
  if (e.detail != null) {
    if (e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
    else if (e.axis == e.VERTICAL_AXIS) dy = e.detail;
  }
  if (dx) {
    var ndx = Math.round(normalizeWheelDelta(dx));
    if (!ndx) ndx = dx > 0 ? 1 : -1;
    div.scrollLeft += ndx;
  }
  if (dy) {
    var ndy = Math.round(normalizeWheelDelta(dy));
    if (!ndy) ndy = dy > 0 ? 1 : -1;
    div.scrollTop += ndy;
  }
  if (dx || dy) { e.preventDefault(); e.stopPropagation(); }
}
Idona answered 31/7, 2012 at 11:5 Comment(2)
This solution doesn't work at all with Chrome on Mac with Trackpad.Baird
@Norris I believe it does now. Just found this question and the example here works on my macbook with chromeBoo
H
4

Simple and working solution:

private normalizeDelta(wheelEvent: WheelEvent):number {
    var delta = 0;
    var wheelDelta = wheelEvent.wheelDelta;
    var deltaY = wheelEvent.deltaY;
    // CHROME WIN/MAC | SAFARI 7 MAC | OPERA WIN/MAC | EDGE
    if (wheelDelta) {
        delta = -wheelDelta / 120; 
    }
    // FIREFOX WIN / MAC | IE
    if(deltaY) {
        deltaY > 0 ? delta = 1 : delta = -1;
    }
    return delta;
}
Herd answered 4/3, 2018 at 13:6 Comment(0)
A
3

This is a problem I've been fighting with for some hours today, and not for the first time :(

I've been trying to sum up values over a "swipe" and see how different browsers report values, and they vary a lot, with Safari reporting order of magnitude bigger numbers on almost all platforms, Chrome reporting quite more (like 3 times more) than firefox, firefox being balanced on the long run but quite different among platforms on small movements (on Ubuntu gnome, nearly only +3 or -3, seems like it sums up smaller events and then send a big "+3")

The current solutions found right now are three :

  1. The already mentioned "use only the sign" which kills any kind of acceleration
  2. Sniff the browser up to minor version and platform, and adjust properly
  3. Qooxdoo recently implemented a self adapting algorithm, which basically tries to scale the delta based on minimum and maximum value received so far.

The idea in Qooxdoo is good, and works, and is the only solution I've currently found to be completely consistent cross browser.

Unfortunately it tends to renormalize also the acceleration. If you try it (in their demos), and scroll up and down at maximum speed for a while, you'll notice that scrolling extremely fast or extremely slow basically produce nearly the same amount of movement. On the opposite if you reload the page and only swipe very slowly, you'll notice that it will scroll quite fast".

This is frustrating for a Mac user (like me) used to give vigorous scroll swipes on the touchpad and expecting to get to the top or bottom of the scrolled thing.

Even more, since it scales down the mouse speed based on the maximum value obtained, the more your user tries to speed it up, the more it will slow down, while a "slow scrolling" user will experience quite fast speeds.

This makes this (otherwise brilliant) solution a slightly better implementation of solution 1.

I ported the solution to the jquery mousewheel plugin : http://jsfiddle.net/SimoneGianni/pXzVv/

If you play with it for a while, You'll see that you'll start getting quite homogeneous results, but you'll also notice that it tend to +1/-1 values quite fast.

I'm now working on enhancing it to detect peaks better, so that they don't send everything "out of scale". It would also be nice to also obtain a float value between 0 and 1 as the delta value, so that there is a coherent output.

Abysm answered 3/6, 2011 at 3:4 Comment(0)
W
3

For zoom support on touch devices, register for the gesturestart, gesturechange and gestureend events and use the event.scale property. You can see example code for this.

For Firefox 17 the onwheel event is planned to be supported by desktop and mobile versions (as per MDN docs on onwheel). Also for Firefox maybe the Gecko specific MozMousePixelScroll event is useful (although presumably this is now deprecated since the DOMMouseWheel event is now deprecated in Firefox).

For Windows, the driver itself seems to generate the WM_MOUSEWHEEL, WM_MOUSEHWHEEL events (and maybe the WM_GESTURE event for touchpad panning?). That would explain why Windows or the browser doesn't seem to normalise the mousewheel event values itself (and might mean you cannot write reliable code to normalise the values).

For onwheel (not onmousewheel) event support in Internet Explorer for IE9 and IE10, you can also use the W3C standard onwheel event. However one notch can be a value different from 120 (e.g. a single notch becomes 111 (instead of -120) on my mouse using this test page). I wrote another article with other details wheel events that might be relevant.

Basically in my own testing for wheel events (I am trying to normalise the values for scrolling), I have found that I get varying values for OS, browser vendor, browser version, event type, and device (Microsoft tiltwheel mouse, laptop touchpad gestures, laptop touchpad with scrollzone, Apple magic mouse, Apple mighty mouse scrollball, Mac touchpad, etc etc).

And have to ignore a variety of side-effects from browser configuration (e.g. Firefox mousewheel.enable_pixel_scrolling, chrome --scroll-pixels=150), driver settings (e.g. Synaptics touchpad), and OS configuration (Windows mouse settings, OSX Mouse preferences, X.org button settings).

Wingless answered 18/10, 2012 at 2:47 Comment(0)
S
1

There is definitely no simple way to normalize across all users in all OS in all browsers.

It gets worse than your listed variations - on my WindowsXP+Firefox3.6 setup my mousewheel does 6 per one-notch scroll - probably because somewhere I've forgotten I've accelerated the mouse wheel, either in the OS or somewhere in about:config

However I am working on a similar problem (with a similar app btw, but non-canvas) and it occurs to me by just using the delta sign of +1 / -1 and measuring over time the last time it fired, you'll have a rate of acceleration, ie. if someone scrolls once vs several times in a few moments (which I would bet is how google maps does it).

The concept seems to work well in my tests, just make anything less than 100ms add to the acceleration.

Summers answered 30/7, 2011 at 6:59 Comment(0)
I
-2
var onMouseWheel = function(e) {
    e = e.originalEvent;
    var delta = e.wheelDelta>0||e.detail<0?1:-1;
    alert(delta);
}
$("body").bind("mousewheel DOMMouseScroll", onMouseWheel);
Intelligence answered 31/7, 2013 at 16:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.