Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detecting type of mouse scroll wheel (Smooth vs Notched) with javascript

I've been working in a web project that uses mouse scroll wheel for different actions over a video. At some point I have to establish a coefficient for the relation between the deltaY and the number of frames that deltaY should roll. So different types of mouses return very different deltaY, specially smooth scroll ones.

In the fiddle I provide bellow this is done in:

targetOffset = targetOffset + (e.deltaY/1000); // 16000 aprox for smooth scroll mice

And 1000 is the coefficient that works well for a Notched Scroll Wheel common mouse. But if I use that coefficient with a Smooth Scroll Touch "wheel", like those of mac computers (that don't have a wheel really) that coefficient is "just too much", like 16 times "too much".

Is there something that could be done to detect this or to callibrate the coefficient in some way?

var FF = !(window.mozInnerScreenX == null); // is firefox?
var vid = document.getElementById("v");
var canvas = document.getElementById("c");
var context = canvas.getContext('2d');
var targetFrame = document.getElementById('t');
var cw = 200;
var ch = Math.round(cw/1.7777);
canvas.width = cw;
canvas.height = ch;
var directionScroll = 0;
var targetOffset = 0;
var coefficient = 1000;
var modes = ['pixels', 'lines', 'page'];
vid.pause();
	vid.addEventListener('seeked', function() {
  		context.drawImage(vid, 0, 0, cw, ch);
	});
window.addEventListener('wheel', function(e) {
  e.preventDefault();
  // Normally scrolling this should be a substraction 
  //   not a sum but "I like it like this!"
  
  // Changed this with help of @Kaiido 's answer as partially solves the discrepancies between Firefox and Chrome
  // alert(modes[e.deltaMode]);
  if (modes[e.deltaMode]=='pixels') coefficient = 1000;
  else if (modes[e.deltaMode]=='lines') coefficient = 30; // This should correspond to line-height??
  else return false; // Disable page scrolling, modes[e.deltaMode]=='page'
  
  targetOffset = targetOffset + (e.deltaY/coefficient); // e.deltaY is the thing!!
  if (e.deltaY < 0) directionScroll = 1;
  if (e.deltaY > 0) directionScroll = -1;
  targetFrame.value = targetOffset;
  return false;
});

var renderLoop = function(){
  requestAnimationFrame( function(){
      context.drawImage(vid,0,0,cw,ch);
    if (vid.paused || vid.ended) {
      targetOffset = targetOffset*0.9;
      targetFrame.value=Math.round(targetOffset*100)/100;
      var vct = vid.currentTime-targetOffset;
      if (vct<0) {
        vct = vid.duration + vct;
      } else if (vct>vid.duration) {
        vct = vct - vid.duration;
      }
      vid.currentTime = vct;
    }
    renderLoop();
  });
};
renderLoop();
.column {
    float: left;
    width: 50%;
}

/* Clear floats after the columns */
.row:after {
    content: "";
    display: table;
    clear: both;
}
#c {
  border:1px solid black;
}
<h3>
  scroll up is forward
</h3>
<div class="row">
  <div class="column">
<div>
  Video element:
</div>
<video controls height="120" id="v" tabindex="-1" autobuffer="auto" preload="auto">
    <source type="video/webm" src="https://www.html5rocks.com/tutorials/video/basics/Chrome_ImF.webm"></source>
</video>
</div>
  <div class="column">
<div>
  Canvas element:
</div>
<canvas id="c"></canvas>
<div>
  Momentum: <input type=text id="t">
</div>
  </div>
</div>

Any help appreciated.

Edit 1:

I've updated the code so that a simple condition is applied to the coefficient, but that does not quite solve the issue as many variants are posible due to browser/plattform/mouse. Some way of callibrate the mouse could work?

Edit 2:

@Kaiido 's answer turned to resolve Firefox and Chrome differences. Firefox returns lines as deltaMode while Chrome returns pixels. I've edited the snippet to consider this.

But the problem still stands with the 'smooth scroll' mouse. To puzzle me even more, that mouse needs a coefficient opposite to the one of lines, it needs a coefficient larger instead of smaller.

like image 500
lalengua Avatar asked Jan 28 '23 05:01

lalengua


2 Answers

That's a wild guess, because I don't have such a Notched mouse to test with, but this 16 times factor really sounds like your delta values are not set on the same mode.

Indeed, the wheel event has 3 possible modes:

  • 0 => pixels (probably smooth scroll / Apple mice)
  • 1 => lines (probably the Notched Mice)
  • 2 => pages (keyboard ?)

So you may have to check your wheel event's deltaMode property to react accordingly.

onwheel = e => {
  var modes = ['pixels', 'lines', 'page'];
  console.log('scrolled by %s %s', e.deltaY, modes[e.deltaMode]);
}
<h1> scroll </h1>
like image 44
Kaiido Avatar answered Jan 31 '23 08:01

Kaiido


See UPDATE at the end!


My original answer:

I don't have a mac nor a 'smooth' mouse, but I've tested your snippet both on Chrome and Firefox both on Windows and Linux boxes.

Works great on Chrome both on Windows and Linux but...

looks like the coefficient isn't the right one for Firefox... it works better (not as good as in Chrome) with 200.

One more thing:

Have you tested the mac fancy mouse on windows and vice-versa? Could it be a mac related problem?

UPDATE:

Other answers are great but I got puzzled by your question and learned a lot with the code and with what other answers pointed out, but something kept in my mind like a bug.

Searching for this topic I found this question very informative. It included a possible mouse scroll calibration script in this answer and a function getScrollLineHeight for Detecting the line-height used by DOM_DELTA_LINE triggered scroll events.

I've copied this function in the snippet for completeness, but at the end it's not needed for what I've thought. I've commented out the line that calls getScrollLineHeight because it does not work in this site for security reasons, but works in this fiddle.

My confusion was to think of scrolling as I normally do, in terms of pixels on a page. But your code really doesn't care about that. I mean, does not care about mouse scroll wheel event.deltaY magnitude. Only if it's positive or negative and consider that one step forward or backwards in a video timeline.

So this does not resolve the problem of "touch sensitive scroll mice", but it does resolve easily Firefox/Chrome and any Pixel/Line/Page deltaMode also. Now it runs smoothly both in Chrome and Firefox. I can't test on other browser because of WEBM video format, and I haven't been able to create a video in any format that works (look at my P.D. at the end).

So, every call is just one step: -1 or 1. Though it seems that only Firefox returns anything than "pixels" for deltaMode. I used this fiddle to test... Now you can focus on that smooth scrolling mouse and see how fast it sends every call, that is what really matters in this particular case (note that many macs have smooth scrolling software or inverted scrolling).

I've commented every line of your code and my modifications for my self but may be useful for others.

// detect if browser firefox as it appears to be the only
//  browser that return deltaModes different than DOM_DELTA_PIXEL
//  Ref: https://stackoverflow.com/a/37474225/4146962
var FF = !(window.mozInnerScreenX == null);

// Function grabbed from the reference above
// It tries to read current line-height of document (for 'lines' deltaMode)
function getScrollLineHeight() {
    var r;
    var iframe = document.createElement('iframe');
    iframe.src = '#';
    document.body.appendChild(iframe);
    var iwin = iframe.contentWindow;
    var idoc = iwin.document;
    idoc.open();
    idoc.write('<!DOCTYPE html><html><head></head><body><span>a</span></body></html>');
    idoc.close();
    var span = idoc.body.firstElementChild;
    r = span.offsetHeight;
    document.body.removeChild(iframe);
    return r;
}

// html5 elements
var vid = document.getElementById("v"); // HTML5 video element
var canvas = document.getElementById("c"); // HTML5 canvas element
var context = canvas.getContext('2d'); // Canvas context
var momentum = document.getElementById('m'); // Current momentum display
var delta = document.getElementById('d'); // Current deltaMode display
var lineheight = document.getElementById('l'); // Current deltaMode display

// global variables
var ch = 120; // canvas with (could be window.innerHeight)
var cw = Math.round(ch * (16 / 9)); // 16/9 proportion width
var targetOffset = 0; // Video offset target position when scrolling

// deltaY to FPS coefficients (for fine tuning)
// Possible mouse scroll wheel 'event.deltaMode'
//  modes are: 0:'pixels', 1:'lines', 2:'page'
var pc = 1000; // 'pixels' deltaY coefficient
var lh = "disabled"; //getScrollLineHeight(); // get line-height of deltaMode 'lines'
lineheight.value = lh; // display current document line height
coefficient = 30;
var deltaModes = ['pixels', 'lines', 'page']; // For deltaMode display

// Sets canvas dimensions
canvas.width = cw;
canvas.height = ch;

// Pauses video (this also starts to load the video)
vid.pause();

// Listens video changes time position
vid.addEventListener('seeked', function() {
  // Updates canvas with current video frame
  context.drawImage(vid, 0, 0, cw, ch);
});

// Listens mouse scroll wheel
window.addEventListener('wheel', function(e) {

  // Don't do what scroll wheel normally does
  e.preventDefault();

  // You don't need an amount, just positive or negative value: -1, 1
  var deltabs = 1;
  if (e.deltaY<0) deltabs = -1;

  // Disable page scrolling, modes[e.deltaMode]=='page'
  if (e.deltaMode>1) return false;

	delta.value = deltaModes[e.deltaMode];
  // Normally scrolling this should be a subtraction 
  //   not a sum but "I like it like this!"
  // targetOffset = targetOffset + (e.deltaY / coefficient); // e.deltaY is the thing!!
  targetOffset = targetOffset + (deltabs/coefficient);

  // Shows current momentum
  momentum.value = targetOffset;

  return false;
});

// Updates canvas on a loop (both for play or pause state)
var renderLoop = function() {
  requestAnimationFrame(function() {

    // This parts updates canvas when video is paused
    // Needs 'seeked' listener above
    if (vid.paused || vid.ended) {

      // Reduce target offset gradually
      targetOffset = targetOffset * 0.9;
      // Show current momentum
      momentum.value = Math.round(targetOffset * 100) / 100;

      // this part joins start and end of video when scrolling
      // forward & backwards
      var vct = vid.currentTime - targetOffset;
      if (vct < 0) {
        vct = vid.duration + vct;
      } else if (vct > vid.duration) {
        vct = vct - vid.duration;
      }
      vid.currentTime = vct;

      // This parts updates canvas when video is playing
    } else {
      // update canvas with current video frame
      context.drawImage(vid, 0, 0, cw, ch);
    }

    renderLoop(); // Recursive call to loop
  });
};
renderLoop(); // Initial call to loop
input {
  width: 50px;
}

.column {
  float: left;
  width: 50%;
}

/* Clear floats after the columns */
.row:after {
  content: "";
  display: table;
  clear: both;
}
<h3>
  mouse scroll video
</h3>
<div class="row">
  <div class="column">
    <div>
      Video element:
    </div>
    <video controls height="120" id="v" tabindex="-1" autobuffer="auto" preload="auto">
      <source type="video/webm" src="https://www.html5rocks.com/tutorials/video/basics/Chrome_ImF.webm"/>
    </video>
  </div>
  <div class="column">
    <div>
      Canvas element:
    </div>
    <canvas id="c"></canvas>
    <div>
      Momentum: <input type=text id="m">
    </div>
    <div>
      deltaMode: <input type=text id="d">
    </div>
    <div>
      lineHeight: <input type=text id="l">
    </div>
  </div>
</div>

P.D. I have a question (too specific for explaining elsewhere)... I've tested with my own videos and got very bad results... why is that? Something to do with specific video encoding settings? Do you know which encoding cmd would be needed for FFMPEG conversion to WEBM format like the video used in your example?

like image 63
Sanxofon Avatar answered Jan 31 '23 08:01

Sanxofon