Smoothscroll

Smooth scrolling on pages using javascript

目前為 2024-01-18 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

var Smoothscroll = {};


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


//debug
Smoothscroll.Debug = 0; //0-none, 1-some, etc.
//autodetected
Smoothscroll.BaseRefreshrate = 60; //initial fps, no need to change

//internal settings
Smoothscroll.Refreshrate = Smoothscroll.BaseRefreshrate;
Smoothscroll.AnimationRefreshrate = Smoothscroll.BaseRefreshrate/2;
Smoothscroll.MinimalRefreshrate = Smoothscroll.BaseRefreshrate/4;

//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, now) {
  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( Smoothscroll.Refreshrate, -1/(Smoothscroll.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() {
      AnimateScroll(target);
    });
  } else
  {
	RequestAnimationUpdate(function() {
		ScrollPixels(target, 0);
    });
    target.scrollanimated = false;
  }
}

function RequestAnimationUpdate(cb) {
    if (Smoothscroll.Refreshrate > Smoothscroll.AnimationRefreshrate) {
        window.requestAnimationFrame(function() {
            cb();
        });
    } else {
        if (Smoothscroll.Refreshrate > Smoothscroll.MinimalRefreshrate) {
            setTimeout(function() {
                cb();
            }, 1000/Smoothscroll.Refreshrate);
        } else {
            cb();
        }
    }
}


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

		//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 && Smoothscroll.Refreshrate > Smoothscroll.MinimalRefreshrate) {
    StartScroll(e, target);
  }
}
function ClickEvent(e) {
  StopScroll(e);
}

function UpdateRefreshrate(cb) {
    window.requestAnimationFrame((before) => {
        window.requestAnimationFrame((after) => {
            var calculatedFps = 1000 / (after - before + 1);
            //NOTE: Update with half power
            Smoothscroll.Refreshrate = (Smoothscroll.Refreshrate + calculatedFps)/2;
            if (Smoothscroll.Debug > 3) {
                console.log("Smoothscroll.Refreshrate: " + Smoothscroll.Refreshrate);
            }

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

var updateRefreshrateLoopTimer = null;
function UpdateRefreshrateLoop() {
    if (updateRefreshrateLoopTimer) {
        clearTimeout(updateRefreshrateLoopTimer);
    }
	UpdateRefreshrate(()=>{
        updateRefreshrateLoopTimer = setTimeout(()=>{
            UpdateRefreshrateLoop();
        },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);

  UpdateRefreshrateLoop();

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