いいとこすぎて移住しちゃいました / LAMP壱岐
いいとこすぎて移住しちゃいました / LAMP壱岐
2015.07.07

requestAnimationFrameでスクロールアニメーションをさせるTweenクラスの実装方法

いなば

こんにちは、フロントエンドエンジニアの稲葉です。

相変わらずAngularJS漬けの日々を過ごしています。
先日AngularJSを使った案件で、イニシャルコストを下げるためにjQuery非依存にコードを修正しました。その際にスクロールアニメーションだけはCSS3でのアニメーションで……というわけにいかないので、しかたなく簡単なTweenクラスを実装してみました。
車輪の再発明は勉強になりますね。

今回はそのときに実装したTweenクラスの実装と使い方をご紹介したいと思います。

Tweenクラスの実装

今回実装したTweenクラスはこちらです。
https://github.com/i78s/Tween.js

必要最低限の機能しか持たせていません。
requestAnimationFrameを内部で使用しています。

tween.jsというライブラリもあるので基本的にはそっちを使えばよいかと思いますが、そのうち気分でバージョンアップしていくかもしれません。

今回作ってみたTweenクラス

(function(){
    var Tween = function(start,end,option){
        this.start = start;
        this.end = end;

        this.setting = {
            duration: 200,
            easing: 'linear',
            step: function(){},
            complete: function(){}
        };
        this.setting = this._extend(this.setting,option);

        this.timer = null;
        this.isPlaying = false;
        this._startTime = Date.now();

        var self = this;
        this._loopHandler = function () {
            self.update();
        };

        this.init();
    };
    Tween.prototype = {
        init: function(){
            this.play();
        },
        play: function(){
            this.isPlaying = true;
            this.timer = window.requestAnimationFrame(this._loopHandler);
        },
        stop: function(){
            this.isPlaying = false;
            if (this.timer) {
                this.timer = null;
                window.cancelAnimationFrame(this._loopHandler);
            }
            return this;
        },
        update: function(){
            var now = Date.now();
            var elapsedTime = now - this._startTime;
            var val = {};

            for(var key in this.end){
                var start = this.start[key];
                var variation = this.end[key] - start;
                var eased = Tween.Easing[this.setting.easing](elapsedTime, start, variation, this.setting.duration);
                val[key] = eased;
            }

            this.setting.step.apply(this,[val]);

            if(this.setting.duration <= elapsedTime){
                this.stop();
                this.setting.complete.apply(this,[]);
            }else{
                this.timer = window.requestAnimationFrame(this._loopHandler);
            }
        },
        _extend: function(arg){
            if (arguments.length < 2) {
                return arg;
            }
            if (!arg) {
                arg = {};
            }
            for (var i = 1; i < arguments.length; i++) {
                for (var key in arguments[i]) {
                    if (arguments[i][key] !== null && typeof(arguments[i][key]) === "object") {
                        arg[key] = this._extend(arg[key],arguments[i][key]);
                    } else {
                        arg[key] = arguments[i][key];
                    }
                }
            }
            return arg;
        }
    };

    Tween.Easing = {
        linear: function (t, b, c, d) {
            return c * t / d + b;
        },
        easeInQuad: function (t, b, c, d) {
            return c * (t /= d) * t + b;
        },
        easeOutQuad: function (t, b, c, d) {
            return -c * (t /= d) * (t - 2) + b;
        },
        easeInOutQuad: function (t, b, c, d) {
            if ((t /= d / 2) < 1) return c / 2 * t * t + b;
            return -c / 2 * ((--t) * (t - 2) - 1) + b;
        },
        easeInCubic: function (t, b, c, d) {
            return c * (t /= d) * t * t + b;
        },
        easeOutCubic: function (t, b, c, d) {
            return c * ((t = t / d - 1) * t * t + 1) + b;
        },
        easeInOutCubic: function (t, b, c, d) {
            if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
            return c / 2 * ((t -= 2) * t * t + 2) + b;
        },
        easeInQuart: function (t, b, c, d) {
            return c * (t /= d) * t * t * t + b;
        },
        easeOutQuart: function (t, b, c, d) {
            return -c * ((t = t / d - 1) * t * t * t - 1) + b;
        },
        easeInOutQuart: function (t, b, c, d) {
            if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
            return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
        },
        easeInQuint: function (t, b, c, d) {
            return c * (t /= d) * t * t * t * t + b;
        },
        easeOutQuint: function (t, b, c, d) {
            return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
        },
        easeInOutQuint: function (t, b, c, d) {
            if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b;
            return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
        },
        easeInSine: function (t, b, c, d) {
            return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
        },
        easeOutSine: function (t, b, c, d) {
            return c * Math.sin(t / d * (Math.PI / 2)) + b;
        },
        easeInOutSine: function (t, b, c, d) {
            return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
        },
        easeInExpo: function (t, b, c, d) {
            return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
        },
        easeOutExpo: function (t, b, c, d) {
            return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
        },
        easeInOutExpo: function (t, b, c, d) {
            if (t == 0) return b;
            if (t == d) return b + c;
            if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
            return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
        },
        easeInCirc: function (t, b, c, d) {
            return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
        },
        easeOutCirc: function (t, b, c, d) {
            return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
        },
        easeInOutCirc: function (t, b, c, d) {
            if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
            return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
        },
        easeInElastic: function (t, b, c, d) {
            var s = 1.70158;
            var p = 0;
            var a = c;
            if (t == 0) return b;
            if ((t /= d) == 1) return b + c;
            if (!p) p = d * .3;
            if (a < Math.abs(c)) {
                a = c;
                var s = p / 4;
            }
            else var s = p / (2 * Math.PI) * Math.asin(c / a);
            return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
        },
        easeOutElastic: function (t, b, c, d) {
            var s = 1.70158;
            var p = 0;
            var a = c;
            if (t == 0) return b;
            if ((t /= d) == 1) return b + c;
            if (!p) p = d * .3;
            if (a < Math.abs(c)) {
                a = c;
                var s = p / 4;
            }
            else var s = p / (2 * Math.PI) * Math.asin(c / a);
            return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
        },
        easeInOutElastic: function (t, b, c, d) {
            var s = 1.70158;
            var p = 0;
            var a = c;
            if (t == 0) return b;
            if ((t /= d / 2) == 2) return b + c;
            if (!p) p = d * (.3 * 1.5);
            if (a < Math.abs(c)) {
                a = c;
                var s = p / 4;
            }
            else var s = p / (2 * Math.PI) * Math.asin(c / a);
            if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
            return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * .5 + c + b;
        },
        easeInBack: function (t, b, c, d, s) {
            if (s == undefined) s = 1.70158;
            return c * (t /= d) * t * ((s + 1) * t - s) + b;
        },
        easeOutBack: function (t, b, c, d, s) {
            if (s == undefined) s = 1.70158;
            return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
        },
        easeInOutBack: function (t, b, c, d, s) {
            if (s == undefined) s = 1.70158;
            if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
            return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
        },
        easeInBounce: function (t, b, c, d) {
            return c - Tween.Easing.easeOutBounce(d - t, 0, c, d) + b;
        },
        easeOutBounce: function (t, b, c, d) {
            if ((t /= d) < (1 / 2.75)) {
                return c * (7.5625 * t * t) + b;
            } else if (t < (2 / 2.75)) {
                return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
            } else if (t < (2.5 / 2.75)) {
                return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
            } else {
                return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
            }
        },
        easeInOutBounce: function (t, b, c, d) {
            if (t < d / 2) return Tween.Easing.easeInBounce(t * 2, 0, c, d) * .5 + b;
            return Tween.Easing.easeOutBounce(t * 2 - d, 0, c, d) * .5 + c * .5 + b;
        }
    };

    if(!window.Tween){
        window.Tween = Tween;
    }
})();

 

使い方

アニメーションを実行させたいタイミングで、Tweenクラスをnew演算子でインスタンス化します。
第1引数にはアニメーションの開始位置、第2引数にはアニメーションの終了位置をオブジェクトで指定します。
オプションではアニメーションの実行時間とイージングの指定、callback関数としてstepとcompleteを設定することができます。

step関数は毎フレームごとに実行されるcallback関数になります。
この中でアニメーションさせたい要素のプロパティを更新し続けることで、アニメーションをさせることができます。
complete関数はアニメーション終了時に実行されるcallback関数です。

実際に動くデモはこちらに用意しました。

http://i78s.me/ligblog-sample/tween/demo/

デモではスクロールTOPへ戻るボタンとアンカースクロールを実装しています。

app.js

(function(){
    var target = navigator.userAgent.indexOf('WebKit') < 0 ? document.documentElement : document.body;
    var tween;

    var $pageTop = document.getElementById('pageTop');
    $pageTop.addEventListener('click',function(e){
        e.preventDefault();
        if(tween){
            return;
        }

        tween = new Tween({
            scrollTop: target.scrollTop
        },{
            scrollTop: 0
        },{
            duration: 200,
            easing: 'linear',
            step: function(val){
                target.scrollTop = val.scrollTop;
            },
            complete: function(){
                tween = null;
            }
        });
    });

    var offset = document.querySelector('.nav-wrapper').clientHeight;
    var pool = {};
    var $menu = document.querySelectorAll('.menu');
    Array.prototype.forEach.call($menu,function(el){
        el.addEventListener('click',function(e){
            e.preventDefault();
            if(tween){
                return;
            }

            var hash = this.hash;
            if(!pool[hash]){
                pool[hash] = document.querySelector(hash).offsetTop;
            }

            tween = new Tween({
                scrollTop: target.scrollTop
            },{
                scrollTop: pool[hash] - offset
            },{
                duration: 200,
                easing: 'linear',
                step: function(val){
                    target.scrollTop = val.scrollTop;
                },
                complete: function(){
                    target.scrollTop = pool[hash] - offset;
                    tween = null;
                }
            });
        })
    })
})();

まとめ

いかがでしたでしょうか?
ライブラリやプラグインはとても便利ですが、必要でない機能も付いてきてしまうことが多いですね。
どういう実装方法をしているのかを見てみたり、試しに自分で書いてみるなどすると新しい気づきがあって面白いかもしれません。