Recently, I created smartscroll, a jQuery plugin for scrolljacking and auto-hashing. As with other scroll plugins, it becomes problematic when the trackpad or Apple Magic Mouse is used, because they implement inertial scrolling.

To tackle this, I created lethargy, a script which tells you whether the scroll event originated from the user, or from inertial scroll. Please do try the demo!

Problem

Inertial scrolling is a feature where scrolling of the frame continues in a decaying fashion after release of the trackpad or mousewheel, simulating the appearance of interacting with an object that has mass, and thus inertia and momentum.

It does this by firing scroll events with a decaying wheelDelta value, for as long as a second. This means if your scroll animation lasts for less than a second, after the animation has finished, inertial scroll will fire the next animation, and so instead of scrolling one frame, it will scroll for two, or three frames.

Tests

First, let's make some profiles of the wheelDelta values for different devices.

We recorded the wheelDelta values of each scroll action using this Gist and demo by Matthew Simpson.

These are the systems and device used to test.

MacBook Air MC965 - with trackpad and Apple Magic Mouse

Processor - 1.7 GHz Intel Core i5

- 1.7 GHz Intel Core i5 Memory - 4 GB 1333 MHz DDR3

- 4 GB 1333 MHz DDR3 Graphics - Intel HD Graphics 3000 384 MB

- Intel HD Graphics 3000 384 MB Software - Mac OS X Lion 10.7.5 (11G63)

- Mac OS X Lion 10.7.5 (11G63) Browser - Chrome v42.0.2311.90 (64-bit)

- Chrome v42.0.2311.90 (64-bit) uname -a output - Darwin d4nyll 11.4.2 Darwin Kernel Version 11.4.2: Thu Aug 23 16:25:48 PDT 2012; root:xnu-1699.32.7~1/RELEASE_X86_64 x86_64

ASUS A53E - with trackpad

Processor - Intel® Core™ i7-2670QM CPU @ 2.20GHz × 8

- Intel® Core™ i7-2670QM CPU @ 2.20GHz × 8 Memory - 5.6 GiB

- 5.6 GiB Graphics - Intel® Sandybridge Mobile

- Intel® Sandybridge Mobile Software - Ubuntu 14.04 LTS (64-bit)

- Ubuntu 14.04 LTS (64-bit) Browser - Chrome v42.0.2311.135 (64-bit)

- Chrome v42.0.2311.135 (64-bit) uname -a output - Linux ribosome 3.13.0-36-generic #63-Ubuntu SMP Wed Sep 3 21:30:07 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

Dell Optiplex 780 - with Dell Computer Corp. Optical 5-Button Wheel Mouse (MOA8BO)

Processor - Intel® Core™2 Duo CPU E8400 @ 3.00GHz × 2

- Intel® Core™2 Duo CPU E8400 @ 3.00GHz × 2 Memory - 3.7 GiB

- 3.7 GiB Graphics - Gallium 0.4 on AMD RV710

- Gallium 0.4 on AMD RV710 Software - Ubuntu 14.04 LTS (64-bit)

- Ubuntu 14.04 LTS (64-bit) Browser - Chrome v42.0.2311.90 (64-bit)

- Chrome v42.0.2311.90 (64-bit) uname -a output - Linux d4nyll 3.13.0-49-generic #83-Ubuntu SMP Fri Apr 10 20:11:33 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

Each device was tested with

One Scroll - Scrolling and letting go, and Continuous - Scrolling but holding onto the wheel, or keeping the finger in contact with the trackpad or Magic Mouse

Results

Here are the results.

Desktop Mouse - One Scroll

As you'd expect, there's no inertial scroll on desktop with the Dell mouse.

Desktop Mouse - Continuous

Continuous and One Scroll gives similar profiles.

ASUS Trackpad - One Scroll

The ASUS trackpad implements inertial scrolling, but the wheelDelta value does not decay gradually over time, but have discrete, fixed values.

ASUS Trackpad - Continuous

Continuous scrolling on the ASUS trackpad is the worst out of all that we tested, as some erratic peaks appear in the other direction.

MacBook Air Trackpad - One Scroll

This shows the decay pattern we expected, but note there are still some erratic peaks. Also, there apears to have two peaks, one immediately after the other. Here's are some sample numbers taken from this demo for your reference.

6 95 117 156 222 684 488 144 231 270 291 950 1509 360 348 336 1215 291 279 273 261 251 945 228 228 432 204 195 366 171 162 318 153 435 138 135 132 255 123 120 117 114 99 282 90 87 165 81 78 75 75 72 69 69 66 231 51 45 48 42 45 156 36 36 66 30 30 30 54 27 24 114 21 39 54 18 15 15 42 15 12 12 36 9 12 9 9 9 9 9 15 9 6 9 6 6 6 6 6 6 6 3 6 6 3 3 6 3

MacBook Air Trackpad - Continuous

Not letting go of the trackpad gave a much nicer pattern, there is a clear major peak and less erratic ones.

Apple Magic Mouse - One Scroll

The Magic Mouse shows a similar pattern to the MacBook Air trackpad, but the peaks seems to be much higher.

Apple Magic Mouse - Continuous

Again, the Magic Mouse shows a similar pattern to the MacBook Air trackpad, but the peaks seems to be much higher.

With such diverse profiles, it's makes it very hard to aggregates these events as one intent, and stop the tail of the inertia scroll from triggering another scroll animation.

But here are some methods that have been suggested.

Solutions

Higher Threshold

Usually, we detect whether a scroll event is a scrollup or scrolldown event by testing whether e.originalEvent.wheelDelta > 0 is true or false , respectively.

Since the unofficial standard wheelDelta used by browsers is 120 , we can simply set a threshold to something close to 120 . This will mean the decaying scroll events won't reach the threshold and nothing will happen.

However, this is, at best, a super-dirty hack, because the mouse event from users who scrolls lightly will not reach the threshold and the page will not scroll.

Furthermore, a person doing a big scroll may well have events firing above the threshold for a longer time than you'd expect.

Also, the threshold is not official nor documented, and so the behaviour can become unpredictable.

Peak Detection

This solution comes close, but no cigar. This is because there are some erratic peaks (see the 'Apple Magic Mouse - One Scroll' profile). So even in one inertial scroll, there can still be many peaks.

Also, you may have noticed in some of the profiles, there are two major peaks, one right after the scrolling starts, and another one soon after.

Decay Detection

My solution, then, is Decay Detection.

First I will calculate two rolling averages, each lasting for time X ; one starting at time -X , the other at time -2X .

After this I will compare the first rolling average with the second, and see if the value of the second has decreased significantly. And if so, it means it is in the decay phase of inertial scroll, and whatever handler we assigned should not be fired.

This does mean that multiple scroll events will fire while the wheelDelta values are rising, but the duration of the rise is short enough for it to not cause a problem, since most animations will last longer than the time required to reach the peak.

All this is a bit complicated to explain, so I encourage you to take a look inside the source code (Better documentation and in-line comments soon!), and also try out the demo! (Open up your console if you want to see the outputs)

Limitations

I am aware that for some trackpads (such as the ASUS one above), there are still some false positives. Suggestions for how to improve on those are always welcomed; please do file an issue on GitHub!

Further Development

Instead of simply using the average, I can work out whether a peak is an anomaly and not include it into the rolling average, giving me a more accurate result.