jQuery無しでイージングアニメーションを伴う機能をつくろう!その3(スムーススクロール・前編)

UI開発者 宇賀

2018年を迎えました。本年もどうぞよろしくお願いいたします。

さて、今回もjQueryを使わずにスムースなアニメーションを実装しようというシリーズで、本稿はその第3弾です。

この記事では様々なWebサイトで採用されているUIの1つ、スムーススクロール(スムーズスクロール)の実装方法を考えてみます。

スムーススクロールというと、jQueryを使った例では次のような方法で実現されていることが多いでしょう。

$('html, body').animate({
    scrollTop: position
}, speed, easing, callback);

今回はこのような処理をjQuery無しで実現してみようと思います。

※ この記事は、以前執筆したjQuery無しでイージングアニメーションを伴う機能をつくろう!その2(トグル編)の続きです。

JavaScriptでイージングを計算する

イージング(easing)とはなにか、CSSでのイージングの実現についてはイージングの紹介編で紹介した通りですが、今回はJavaScriptでイージングを計算する方法を紹介します。

実際の計算式については、検索してみると様々出てくるのですが今回はease-outについてのみ紹介します。

ease-outは次のような関数と計算で実現できます。

var easeOut = function (p) {
    return p * (2 - p);
};

この関数に現在の進捗(0~1)を渡すとease-outに当てはめたときの値を返してくれます。

たとえば、進捗25%のときease-outの計算上では「43.75%」なります。

var easeOut = function (p) {
    return p * (2 - p);
};

console.log(easeOut(0.25)); // 0.4375
console.log(easeOut(0.5)); // 0.75
console.log(easeOut(0.75)); // 0.9375

最初は早く、徐々に速度を落とすease-outの動きそのものですね。

これを利用して、800pxの距離をease-outでスムーススクロールさせたい場合は次のような進捗でスクロールを実施させるように書いていきます。

var range = 800;
var easeOut = function (p) {
    return p * (2 - p);
};

console.log(range * easeOut(0.1)); // 152
console.log(range * easeOut(0.3)); // 408
console.log(range * easeOut(0.5)); // 600
console.log(range * easeOut(0.7)); // 727.9999...
console.log(range * easeOut(0.9)); // 792.0000...
console.log(range * easeOut(1.0)); // 800

「p * (2 - p)」というたったこれだけの計算式でease-outが実現できるのは、なんだか不思議な感じがして楽しいですね。

計算方法は以上です。次はこの式を動きに当てはめていきます。

$('html, body').animateとwindow.scrollTo

jQueryでスムーススクロールを実装する場合、ブラウザの挙動の違いを解決する方法として最も有名なものが「html, body」両方をjQueryオブジェクトにもたせるというものではないでしょうか。

確かに簡潔ですぐに実践できる方法です。とはいえこのままではコールバックが2回呼ばれてしまうなどの欠点も存在するため、フラグ管理の必要性が出てくるなど実際にはいろいろと手間がかかる機能だったりします。

$('html, body').animate({
    scrollTop: position
}, speed, easing, function () {
    // 2回呼ばれるコールバック
});

元々JavaScriptの仕様でスクロールを試みる場合、各ブラウザ共通で次のような方法が存在します。

window.scrollTo(0, 800); // ページ上部から 800px の位置にスクロールする
console.log(window.pageYOffset); // 現在のY方向のスクロール量

※ より詳しい関数の仕様についてはMDNのwindow.scrollToをご覧ください。

window.scrollToの第2引数に、先ほど計算した「range * easeOut(進捗)」の結果を入れてスクロールさせます。

続いては、先ほどのeaseOut関数とwindow.scrollToを組み合わせてアニメーションのためにループ処理を組み合わせていきます。

window.requestAnimationFrame

setTimeoutやsetIntervalを用いる、という発想も場合に応じてあるかもしれませんが、今回はrequestAnimationFrameを見ていきましょう。

requestAnimationFrameはとても便利な関数で、第1引数に渡した関数をディスプレイのリフレッシュレートに合わせて呼び出します。

requestAnimationFrameが呼び出される回数は、だいたい1秒間に60回とされており、タブが非アクティブの場合や非表示になっているiframe等ではこのレートは下がるよう多くの場合実装されています。

次の例は60回コールバックが呼び出された際に、開始から何秒経ったかをコンソールに出力するサンプルです。コンソールに貼り付けて実行することで、動作が確認できます。だいたい1秒に1回経過時間がコンソールに出力されているのではないでしょうか。タブを非アクティブにしてみながらコンソールを確認してみてください。

終了させたい場合はブラウザをリロードしてください(F5キー)。

(function () {
    var i = 0;
    var start = performance.now();
    var move = function () {
        i++;

        if (i === 60) {
            i = 0;
            console.log(Math.round(performance.now() - start) / 1000 + '秒');
        }

        requestAnimationFrame(move);
    };

    requestAnimationFrame(move);
}());

※ より詳しい関数の仕様についてはMDNのwindow.requestAnimationFrameをご覧ください。

前述の3点を組み合わせて関数化してみる

var smoothScroll = function (range) {
    var position = 0; // スクロールする位置
    var progress = 0; // 現在の進捗 0 ~ 100
    var easeOut = function (p) { // ease-out に当てはめた値を返す
        return p * (2 - p);
    };
    var move = function () { // 実際にスクロールを行う
        progress++; // 進捗を進める
        position = range * easeOut(progress / 100); // スクロールする位置を計算する

        window.scrollTo(0, position); // スクロールさせる

        if (position < range) { // 現在位置が目的位置より進んでいなければアニメーションを続行させる
            requestAnimationFrame(move);
        }
    };

    requestAnimationFrame(move); // 初回呼び出し
};

smoothScroll(800); // 800px の位置までスムーススクロールする

いったん、目的の位置までイージングアニメーションを伴ってスクロールすることができたのではないでしょうか?

スクロールの中断や、スクロールが終わった後にコールバックを呼び出す機能など不足しているものがまだまだありますので、それらは次回「jQuery無しでイージングアニメーションを伴う機能をつくろう!その4(スムーススクロール・後編)」でご紹介しようと思います!

お楽しみに!