11

I want to make a page scroll down slowly and smoothly. Well, the speed should actually adjustable. The user should also be able to scroll up manually while the script is scrolling down. First I tried this:

var autoScrollDelay = 1
var autoScrollSpeed = 1
var autoScrollTimer
function setAutoScroll(newValue) {
    autoScrollSpeed = newValue ? newValue : autoScrollSpeed
    if (autoScrollTimer) {
        clearInterval(autoScrollTimer)
    }
    if (autoScrollDelay) {
        autoScrollTimer = setInterval(function(){
            window.scrollBy(0,autoScrollSpeed)
      },autoScrollDelay)
    }
}
setAutoScroll(1) // higher number =  faster scrolling

But it was causing a very heavy CPU load and the slowest speed was too fast. And in addition to that manually scrolling up did not work properly while the code was running.

Then I tried:

var autoScrollDelay = 1
var autoScrollSpeed = 1
var autoScrollTimer
function setAutoScroll(newValue) {
    autoScrollDelay = newValue ? newValue : autoScrollDelay //using autoScrollDelay instead of autoScrollSpeed
    if (autoScrollTimer) {
        clearInterval(autoScrollTimer)
    }
    if (autoScrollDelay) {
        autoScrollTimer = setInterval(function(){
            window.scrollBy(0,autoScrollSpeed)
      },autoScrollDelay)
    }
}
setAutoScroll(200) // higher number scrolls slower

But the scrolling was not smooth when setting it too slow (e.g. 200).

Then I tried:

$("html, body").animate({
    scrollTop: $('html, body').get(0).scrollHeight, 
}, 40000, "linear");

But again the CPU load was unreasonably high and scrolling up or down manually wasn't possible this way.

Is there a better way to do this?

Forivin
  • 14,780
  • 27
  • 106
  • 199
  • CSS translateY might be your best option. – Jarek Kulikowski Jul 23 '17 at 22:41
  • Can you link to your page? What's the page weight - do you have a lot of heavy images or animations or anything going on as the page scrolls? – Michael Coker Jul 23 '17 at 22:53
  • It didn't matter what page I ran the code on. You can try it right here on Stackoverflow. My Firefox shows 10-12% cpu load which is probably as much as a single threaded script could take on an 8 thread cpu. – Forivin Jul 23 '17 at 23:04
  • my answer will be : do not mess with scroll .. there is no plugin/good way to create custom scroll behavior – Wordica Jul 26 '17 at 22:50

2 Answers2

19

Here is one possible implementation. The refresh rate is fixed, and corresponds to fps in the code below. To make sure that the speed is constant, I consider the time elapsed since the previous scroll when calculating the new scroll position. Manual scrolling is allowed (with the scroll bar, with the mouse wheel, or with touch on mobile devices) and taken into account by processing scroll, wheel and touchmove events. You can see the code at work in this codepen.

var fps = 100;
var speedFactor = 0.001;
var minDelta = 0.5;
var autoScrollSpeed = 10;
var autoScrollTimer, restartTimer;
var isScrolling = false;
var prevPos = 0, currentPos = 0;
var currentTime, prevTime, timeDiff;

window.addEventListener("scroll", function (e) {
    // window.pageYOffset is the fallback value for IE
    currentPos = window.scrollY || window.pageYOffset;
});

window.addEventListener("wheel", handleManualScroll);
window.addEventListener("touchmove", handleManualScroll);

function handleManualScroll() {
    // window.pageYOffset is the fallback value for IE
    currentPos = window.scrollY || window.pageYOffset;
    clearInterval(autoScrollTimer);
    if (restartTimer) {
        clearTimeout(restartTimer);
    }
    restartTimer = setTimeout(() => {
        prevTime = null;
        setAutoScroll();
    }, 50);
}

function setAutoScroll(newValue) {
    if (newValue) {
        autoScrollSpeed = speedFactor * newValue;
    }
    if (autoScrollTimer) {
        clearInterval(autoScrollTimer);
    }
    autoScrollTimer = setInterval(function(){
        currentTime = Date.now();
        if (prevTime) {
            if (!isScrolling) {
                timeDiff = currentTime - prevTime;
                currentPos += autoScrollSpeed * timeDiff;
                if (Math.abs(currentPos - prevPos) >= minDelta) {
                    isScrolling = true;
                    window.scrollTo(0, currentPos);
                    isScrolling = false;
                    prevPos = currentPos;
                    prevTime = currentTime;
                }
            }
        } else {
            prevTime = currentTime;
        }
    }, 1000 / fps);
}

setAutoScroll(20);
ConnorsFan
  • 70,558
  • 13
  • 122
  • 146
  • Is there any way to allow manual scrolling while this is running? – Forivin Aug 01 '17 at 07:21
  • @Forivin - With the current code, I can scroll manually as the auto scroll is going on (tested with [the codepen](https://codepen.io/ConnorsFan/pen/yoOzGx) on Chrome, FF and IE). You cannot? – ConnorsFan Aug 01 '17 at 09:36
  • Not really. It feels very unresponsive. Even if I try to manually scroll up or down as fast as I can, I barely see a reaction. It's like the manual scrolling distance is set to 1px and only works every 2 seconds. Tested in chromium and firefox (with smooth scrolling enabled). If I disable smooth scrolling in firefox it actually works fine. But I need a solution that works for both modes. – Forivin Aug 01 '17 at 10:41
  • 2
    I was testing "manual scrolling" with the scrollbar, without any problem. Maybe you were using the mouse wheel. You can try [this codepen](https://codepen.io/ConnorsFan/pen/rzebZw), which includes some code for mouse wheel events. – ConnorsFan Aug 01 '17 at 12:33
  • Yes I was using the mouse wheel. And with the new codepen that actually works quite well. But on my smartphone the problem is still the same. I guess the touch screen scrolling doesn't trigger the event properly? Any ideas? – Forivin Aug 01 '17 at 12:55
  • 1
    You can try [this codepen](https://codepen.io/ConnorsFan/pen/rzeXNe). The manual scroll is processed for `wheel` and `touchmove` events. My tests on iPad were not successful however (the auto scroll itself does not work). Scrolling on iOS seems to be quite different... – ConnorsFan Aug 01 '17 at 14:25
  • It's very unfortunate to hear that it doesn't work on iOS. But at least all the other stuff works now. Thank you. – Forivin Aug 01 '17 at 17:00
5

The function from this article uses vanilla JS to implement smooth scrolling at various speeds. Here is a demo:

document.getElementById("scrollBottomButton").onclick = function() {
  var duration = document.getElementById("bottomScrollDuration").value * 1000;
  scrollIt(document.querySelector("#bottom-row"), duration, "easeOutQuad");
};

document.getElementById("scrollTopButton").onclick = function() {
  var duration = document.getElementById("topScrollDuration").value * 1000;
  scrollIt(document.getElementById("top-row"), duration, "easeOutQuad");
};

// thanks to https://pawelgrzybek.com/page-scroll-in-vanilla-javascript/
function scrollIt(destination, duration = 200, easing = "linear", callback) {
  const easings = {
    linear(t) {
      return t;
    },
    easeOutQuad(t) {
      return t * (2 - t);
    }
  };

  const start = window.pageYOffset;
  const startTime = "now" in window.performance
  ? performance.now()
  : new Date().getTime();

  const documentHeight = Math.max(
    document.body.scrollHeight,
    document.body.offsetHeight,
    document.documentElement.clientHeight,
    document.documentElement.scrollHeight,
    document.documentElement.offsetHeight
  );
  const windowHeight =
        window.innerHeight ||
        document.documentElement.clientHeight ||
        document.getElementsByTagName("body")[0].clientHeight;
  const destinationOffset = typeof destination === "number"
  ? destination
  : destination.offsetTop;
  const destinationOffsetToScroll = Math.round(
    documentHeight - destinationOffset < windowHeight
    ? documentHeight - windowHeight
    : destinationOffset
  );

  if ("requestAnimationFrame" in window === false) {
    window.scroll(0, destinationOffsetToScroll);
    if (callback) {
      callback();
    }
    return;
  }

  function scroll() {
    const now = "now" in window.performance
    ? performance.now()
    : new Date().getTime();
    const time = Math.min(1, (now - startTime) / duration);
    const timeFunction = easings[easing](time);
    window.scroll(
      0,
      Math.ceil(timeFunction * (destinationOffsetToScroll - start) + start)
    );

    if (window.pageYOffset === destinationOffsetToScroll) {
      if (callback) {
        callback();
      }
      return;
    }

    requestAnimationFrame(scroll);
  }

  scroll();
}


// scroll testing    
var middleHtml = [];

const schiller = "Nur Beharrung führt zum Ziel, Nur die Fülle führt zur Klarheit, Und im Abgrund wohnt die Wahrheit.".split(' ')

for(var i=0; i<schiller.length;i+=1){
  middleHtml.push("<div class=' container row' id='scrolling'><h1 style='margin: 30rem 10rem 30rem 0;font-size: 3.5em;font-family: Helvetica, sans-serif;color: #fff;'>"+schiller[i]+"</h1></div>");
}


document.getElementById('middle').innerHTML = middleHtml.join('');
.container-fluid {
background: #e52d27;
background: -webkit-linear-gradient(to top, #b31217, #e52d27);
background: linear-gradient(to top, #b31217, #e52d27);
}

.container-fluid input, .container-fluid .btn {
  border-radius: 0;
}

.btn {
  background: rgba(210,200,200,0.95);
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet"/>

<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet"/>

<div class='container-fluid'>
  <div class='row' id='top-row'>
    <div class='col-sm-8'>
      <input class='form-control' id='bottomScrollDuration' placeholder='Enter duration in seconds (4, 25, 40, etc...)' />
    </div>
    <div class='col-sm-4'>
      <button class='btn' id='scrollBottomButton'>Scroll to bottom</button>
    </div>    
  </div>
  <div id='middle'>    
  </div>

  <div class='row' id='bottom-row'>
    <div class='col-sm-8'>
      <input class='form-control' id='topScrollDuration' placeholder='Enter duration in seconds (4, 25, 40, etc...)' />
    </div>
    <div class='col-sm-4'>
      <button class='btn' id='scrollTopButton'>Scroll to top</button>
    </div>
  </div>
</div>

See CodePen Demo

Update

You could try this if you just want to adjust the speed and keep a constant scrolling behavior:

function pageScroll(speed) {
    window.scrollBy(0,1);
    scrolldelay = setTimeout(pageScroll,speed);
}

And then call the function with a speed of your choice i.e.:

pageScroll(1);

I ran it in Chrome, and it didn't tax my CPU usage. The CPU does spike more when it's run in Firefox.

Dan Kreiger
  • 5,358
  • 2
  • 23
  • 27
  • With this code seems to be lighter on the CPU, the speed is dependent on the height of the page because you set a scroll duration and not a scroll speed. Oh and the scrolling shouldn't stop when the bottom is reached. – Forivin Jul 26 '17 at 22:32
  • You can choose where you want to scroll to in this function, but I guess that's not what you're looking for. If the bottom of the page doesn't stop the scrolling, how is the scrolling stopped? Does the user stop the scrolling? Does it timeout? – Dan Kreiger Jul 26 '17 at 23:09
  • @Forivin I've updated my answer with a smaller function that doesn't spike the CPU usage in Chrome. However, the CPU usage does spike when it's run in Firefox. – Dan Kreiger Jul 26 '17 at 23:43
  • That function has the exact same problems that my first code snippet has. It scrolls way too fast even at a speed of 1 and it causes the same CPU load (at least on the latest Firefox) and manually scrolling up doesn't work properly. – Forivin Jul 30 '17 at 16:11
  • 1
    You should also add .stop to break the scrolling if user wants to stop at some point. Take a look at this as reference: https://stackoverflow.com/a/18445654/4465062 – Atilla Arda Açıkgöz Jul 31 '17 at 11:31
  • @AtillaArdaAçıkgöz Do you know how to do that in vanilla JS? I also thought about that (read my previous comment in this thread), but it wasn't part of the request in this question. – Dan Kreiger Aug 01 '17 at 08:30
  • @DanKreiger i have this in my project to bind mousewheel but it is jquery. $(window).bind("mousewheel", function() { $("html, body").stop(); }); – Atilla Arda Açıkgöz Aug 01 '17 at 10:10
  • Just ran the codepen, if I scroll to the bottom, then scroll to the top, it jumps back to the bottom? Is that expected? – Mark Redman Dec 04 '18 at 15:42