Normalizing mousewheel speed across browsers

JavascriptMousewheel

Javascript Problem Overview


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)?

Javascript Solutions


Solution 1 - Javascript

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.

Solution 2 - Javascript

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

Solution 3 - Javascript

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

Solution 4 - Javascript

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     |                |                |                |           |             |           |

Solution 5 - Javascript

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(); }
}

Solution 6 - Javascript

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;
}

Solution 7 - Javascript

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.

Solution 8 - Javascript

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).

Solution 9 - Javascript

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.

Solution 10 - Javascript

var onMouseWheel = function(e) {
    e = e.originalEvent;
    var delta = e.wheelDelta>0||e.detail<0?1:-1;
    alert(delta);
}
$("body").bind("mousewheel DOMMouseScroll", onMouseWheel);

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPhrogzView Question on Stackoverflow
Solution 1 - JavascriptPhrogzView Answer on Stackoverflow
Solution 2 - JavascriptGeorgeView Answer on Stackoverflow
Solution 3 - JavascriptsmrtlView Answer on Stackoverflow
Solution 4 - JavascriptSergioView Answer on Stackoverflow
Solution 5 - JavascriptMarijnView Answer on Stackoverflow
Solution 6 - JavascriptMarekView Answer on Stackoverflow
Solution 7 - JavascriptSimone GianniView Answer on Stackoverflow
Solution 8 - JavascriptrobocatView Answer on Stackoverflow
Solution 9 - Javascriptck_View Answer on Stackoverflow
Solution 10 - JavascriptMatthieu ChavignyView Answer on Stackoverflow