Greasy Fork is available in English.

Smoothscroll

Smooth scrolling on pages using javascript

// ==UserScript==
// @name Smoothscroll
// @author       Creec Winceptor
// @description  Smooth scrolling on pages using javascript
// @namespace https://greasyfork.org/users/3167
// @include     *
// @version 12
// ==/UserScript==

var Smoothscroll = {};


//settings
Smoothscroll.Smoothness = 0.5;
Smoothscroll.Acceleration = 0.5;


//debug
Smoothscroll.Debug = 0; //0-none, 1-some, etc.

/*
//initial fps, no need to change
Smoothscroll.BaseRefreshrate = 60;
Smoothscroll.MaxRefreshrate = Smoothscroll.BaseRefreshrate*3;
Smoothscroll.MinRefreshrate = Smoothscroll.BaseRefreshrate/3;
*/

//automatically calculated
Smoothscroll.Refreshrate = 60;

Smoothscroll.MaxRefreshrate = 300;
Smoothscroll.MinRefreshrate = 1;

//scrolling and animation
function ScrollSubpixels(element, newvalue)
{
  if (newvalue!=undefined)
  {
    element.scrollsubpixels = newvalue;
    return newvalue;
  }
	else
  {
    var olddelta = element.scrollsubpixels;
    if (olddelta!=undefined)
    {
      return olddelta;
    }
    return 0;
  }
}
function ScrollPixels(element, newvalue)
{
  if (newvalue!=undefined)
  {
    element.scrollpixels = newvalue;
    
    ScrollSubpixels(element, 0);
    
    return newvalue;
  }
	else
  {
    var olddelta = element.scrollpixels;
    if (olddelta!=undefined)
    {
      return olddelta;
    }
    return 0;
  }
}

var last = 0;
function AnimateScroll(target, refreshrate) {
  var scrollsubpixels = ScrollSubpixels(target);
  var scrollpixels = ScrollPixels(target);

  if (Smoothscroll.Debug>3) {
    console.log("scrollpixels: " + scrollpixels);
  }
  
  if (Smoothscroll.Debug>3) {
    console.log("target: ", target);
    
    if (target == document.documentElement) {
      console.log("document.documentElement");
    }
  }

  var scrolldirection = 0;
  if (scrollpixels>0) {
    scrolldirection = 1;
  }
  if (scrollpixels<0) {
    scrolldirection = -1;
  }

  var scrollratio = 1-Math.pow( refreshrate, -1/(refreshrate*Smoothscroll.Smoothness));
  
  var scrollrate = scrollpixels*scrollratio;
  
  if (Math.abs(scrollpixels)>2) {
    
    var fullscrolls = Math.floor(Math.abs(scrollrate))*scrolldirection;
    var scrollsubpixelsadded = scrollrate - fullscrolls;

    var additionalscrolls = Math.floor(Math.abs(scrollsubpixels + scrollsubpixelsadded))*scrolldirection;
    var scrollsubpixelsleft = scrollsubpixels + scrollsubpixelsadded - additionalscrolls;

    ScrollPixels(target, scrollpixels-fullscrolls-additionalscrolls);
    ScrollSubpixels(target, scrollsubpixelsleft);
	
    var scrolldelta = fullscrolls + additionalscrolls;  
    if (Smoothscroll.Debug>1) {
      console.log("scrolldelta: " + scrolldelta);
    }
/*
      if (target.scrollBy != null) {
            target.scrollBy({
                top: scrolldelta,
                left: 0,
                behavior: 'auto'
            });


          if (Smoothscroll.Debug>1) {
              console.log("target.scrollBy: " + scrolldelta);
          }
        } else {
        */
      target.style.scrollBehavior="auto"; // fix for pages with changed scroll-behavior
      target.scrollTop = target.scrollTop + scrolldelta;


      if (Smoothscroll.Debug>1) {
          console.log("target.scrollTop: " + target.scrollTop);
      }

	target.scrollanimated = true;
	RequestAnimationUpdate(function(newrefreshrate) {
      AnimateScroll(target, newrefreshrate);
    });
  } else
  {
	RequestAnimationUpdate(function(newrefreshrate) {
		ScrollPixels(target, 0);
    });
    target.scrollanimated = false;
  }
}

function RequestAnimationUpdate(cb) {
    var before = performance.now();
    window.requestAnimationFrame(() => {
        var after = performance.now();
        var frametime = after - before;
        var calculatedFps = 1000 / Math.max(frametime, 1);

        var refreshrate = Math.min(Math.max(calculatedFps, Smoothscroll.MinRefreshrate), Smoothscroll.MaxRefreshrate);
        //Smoothscroll.Refreshrate = refreshrate;

        cb(refreshrate);
    });
}


Smoothscroll.Stop = function(target) {
	if (target) {
		ScrollPixels(target, 0);
	}
}
Smoothscroll.Start = function(target, scrollamount) {
	if (target) {
        var scrolltotal = ScrollPixels(target, scrollamount);
        if (!target.scrollanimated) {
            AnimateScroll(target, Smoothscroll.Refreshrate);
        }

		//var scrollpixels = ScrollPixels(target);

	}
}

if (typeof module !== 'undefined') {
	module.exports = Smoothscroll;
}


function CanScroll(element, dir) {

  
  if (dir<0)
	{
	  return element.scrollTop>0;
	}
	if (dir>0)
	{
    if (element==document.body) {
      
      if (element.scrollTop==0) {
        element.scrollTop = 3;
        if (element.scrollTop==0) {
          return false;
        }
        element.scrollTop = 0;
      }
      
      return Math.round(element.clientHeight+element.scrollTop)<(element.offsetHeight);
    } 
		return Math.round(element.clientHeight+element.scrollTop)<(element.scrollHeight);
	}
}
function HasScrollbar(element)
{
  //TODO: problem with webkit, body not scrollable?
  if (element==window || element==document) {
    return false;
  }
  
  if (element==document.body) {
    return window.getComputedStyle(document.body)['overflow-y']!="hidden";
  }  
  
  //THANK YOU TO: https://tylercipriani.com/blog/2014/07/12/crossbrowser-javascript-scrollbar-detection/
  if (element==document.documentElement) {
    return window.innerWidth > document.documentElement.clientWidth;
  } else {
    //return (element.clientWidth-element.clientWidth)>0;
    var style = window.getComputedStyle(element);
    return style['overflow-y']!="hidden" && style['overflow-y']!="visible";
  }

}

function Scrollable(element, dir)
{
  //TODO: problem with webkit, body not scrollable?
  if (element==document.body) {
    //return false;
  }  
  
  var scrollablecheck = CanScroll(element, dir);
  if (!scrollablecheck) {  
    if (Smoothscroll.Debug>1) {
      console.log("scrollablecheck: " + scrollablecheck);
    }
    return false;
  }  
  
  var scrollbarcheck = HasScrollbar(element);
  if (!scrollbarcheck) {
    if (Smoothscroll.Debug>1) {
      console.log("scrollbarcheck: " + scrollbarcheck);
    }
    return false;
  }  

  if (Smoothscroll.Debug>1) {
    console.log("scrollablecheck: " + scrollablecheck);
    console.log("scrollbarcheck: " + scrollbarcheck);
  }
	return true;
}
function GetPath(e) {
  if (e.path) {
    return e.path;
  }
  if (e.composedPath) {
    return e.composedPath();
  }
  if (Smoothscroll.Debug>1) {
    console.log("Smoothscroll: e.path is undefined");
  }
  return null;
}

function GetTarget(e) {
  var direction = e.deltaY;
  var nodes = GetPath(e);
  if (!nodes) {
    return null;
  }
  
  if (Smoothscroll.Debug>2) {
    console.log("nodes: ");
    console.log(nodes);
  
    console.log("target: ");
  }
  
  for (var i=0; i<(nodes.length); i++) { 
    var node = nodes[i];
    
    if (Smoothscroll.Debug>2) {
      console.log(node);
    }
    
    if (Scrollable(node, direction))
    {
      if (Smoothscroll.Debug>2) {
        console.log("true");
        
      }
      return node;
    }
    
   
  }
  if (Smoothscroll.Debug>1) {
    console.log("false");

  }

  return null;
}

function GetStyleProperty(el, styleprop){
	if(window.getComputedStyle){
		var heightprop = document.defaultView.getComputedStyle(el, null).getPropertyValue(styleprop);
    if (heightprop) {
      return parseInt(heightprop);
    }
	}
	else if(el.currentStyle){
		var heightprop = el.currentStyle[styleprop.encamel()];
    if (heightprop) {
      return parseInt(heightprop);
    }
	}
	return null;
}

//mouse event scroll handlers
function StopScroll(e) {
  var nodes = GetPath(e);
  if (!nodes) {
    return null;
  }

  for (var i=0; i<(nodes.length); i++) { 
    var node = nodes[i];
    
    Smoothscroll.Stop(node);
  }
}
function StartScroll(e, target) {

  if (e.defaultPrevented)
  {
    return true;
  }
  else
  {
	  var delta = e.deltaY;
      if (Smoothscroll.Debug) {
          console.log("e: ", e);
      }

    if (e.deltaMode && e.deltaMode==1) {
      var line = GetStyleProperty(target, 'line-height');
        if (Smoothscroll.Debug) {
            console.log("line: " + line);
        }
      if (line && line>0) {
        delta = e.deltaY * line;
      }
    }

    if (e.deltaMode && e.deltaMode==2) {
      var page = target.clientHeight;
        if (Smoothscroll.Debug) {
            console.log("page: " + page);
        }
      if (page && page>0) {
        delta = e.deltaY * page;
      }
    }

    var scrollpixels = ScrollPixels(target);

    var accelerationratio = Math.sqrt(Math.abs(scrollpixels/delta*Smoothscroll.Acceleration));

    var acceleration = Math.round(delta*accelerationratio);

    var totalscroll = scrollpixels + delta + acceleration;
      if (Smoothscroll.Debug) {
          console.log("scrollpixels: " + scrollpixels);
          console.log("delta: " + delta);
          console.log("acceleration: " + acceleration);
          console.log("totalscroll: " + totalscroll);
      }

    Smoothscroll.Start(target, totalscroll);

    e.preventDefault();
  }
}

//mouse event call handlers
function WheelEvent(e) {
  var target = GetTarget(e);

  if (target) {
    StartScroll(e, target);
  }
}
function ClickEvent(e) {
  StopScroll(e);
}

/*
function GetFrametime(cb) {
    var before = performance.now();
    window.requestAnimationFrame(() => {
        var after = performance.now();
        var diff = after - before;

        if (cb) {
            cb(diff);
        }
    });
}

function UpdateRefreshrateInternal(cb) {
    GetFrametime((frametime) => {
        var calculatedFps = 1000 / Math.max(frametime, 1);
        Smoothscroll.Refreshrate = Math.min(Math.max(calculatedFps, Smoothscroll.MinRefreshrate), Smoothscroll.MaxRefreshrate);
        if (Smoothscroll.Debug > 3) {
            console.log("Smoothscroll.Refreshrate: " + Smoothscroll.Refreshrate);
        }
        if (cb) {
            cb();
        }
    });
}

var updateRefreshrateLoopTimer = null;
function UpdateRefreshrate() {
    if (updateRefreshrateLoopTimer) {
        clearTimeout(updateRefreshrateLoopTimer);
    }
	UpdateRefreshrateInternal(()=>{
        updateRefreshrateLoopTimer = setTimeout(()=>{
            UpdateRefreshrate();
        },1000/Smoothscroll.BaseRefreshrate);
    });
};
*/

//init function
function Init()
{
  if (window.top != window.self) {
    //console.log("Smoothscroll: ignoring iframe");
    return null;
  }
  if (window.Smoothscroll && window.Smoothscroll.Loaded) {
    //console.log("Smoothscroll: already loaded");
    return null;
  }

      //Smoothscroll.Refreshrate = Smoothscroll.BaseRefreshrate;

	if (!window.requestAnimationFrame) {
		window.requestAnimationFrame =
			window.mozRequestAnimationFrame ||
			window.webkitRequestAnimationFrame;
	}

  document.documentElement.addEventListener("wheel", function(e){
    WheelEvent(e);
    
    if (Smoothscroll.Debug>0) {
      console.log(e);
    }
  },{ passive: false });

  document.documentElement.addEventListener("mousedown", function(e){
    ClickEvent(e);
    
    if (Smoothscroll.Debug>0) {
      console.log(e);
    }
  });
  
  window.Smoothscroll = Smoothscroll;
  window.Smoothscroll.Loaded = true;
  
	//window.requestAnimationFrame(Fps);

  //UpdateRefreshrate();

  //UpdateRefreshrate();
  
  console.log("Smoothscroll: loaded");
}
Init();