Search code examples
javascriptmacosscrollmousewheel

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.


Solution

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