スクロールに応じてテキストを動作(アニメーション)させるなるべく軽い方法

スクロールをするとテキストがスライドしながらフェードインするなど、スクロールに応じたアニメーションをよく見るようになりました。
まめわざでも近々実装を予定しておりますが、ここではその実装方法についてなるべく軽い方法を検証してみます。

設計

  • Javascriptでスクロールを監視する
  • アニメーションをしたい要素にスクロールが達した際に、Javascriptでアニメーションを起動させる

という流れで設計を検討します。ただし、今回は「なるべく軽く」がテーマなので、次の方針で設計します。

  • Javascriptの使用を最小限に留め、アニメーションはCSSのanimationを利用する(Javascriptではクラスを付与することでアニメーションを実行させる)
  • スクロール監視をしなくても全てのコンテンツが正常に見れるようにして「スマートフォンで利用しない」選択肢を用意する
  • アニメーションは1度だけ起動し、全てのアニメーションが終了後はスクロール監視をやめる

サンプル

以下で説明する内容を盛り込んだのがこちらのサンプルです。

なるべく軽量にしたスクロールの監視

次のような流れでスクロールを監視し、アニメーションを実行(animationプロパティを設定したクラスの付与)します。

  • アニメーションを実行させたい要素には、予めクラスを設定
  • ページ読み込み時に指定したクラスを持つ要素のy座標を保存し、スクロールイベントを定義
  • スクロール時に、スクロールイベントによってスクロール量と保存したy座標を比較。
    y座標に達した場合はクラスを付与し監視を停止
  • 監視対象の要素が無くなった場合は、スクロールイベントを停止

HTMLとCSS

例えばh3見出しに入った文字をスクロールに応じてアニメーションさせたい場合、
<h3 class="anim1">アニメーションさせる文字</h3>
というようにHTMLをセットし、CSSでは
.anim1 {
    animation: 1.5s forwards anim1;
}
@keyframes anim1 {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}
のようにanim1クラスにanim1アニメーションを定義します。
これによりページを読み込み時にアニメーションが実行されます。

以下のJavascriptでは、ページ読み込み時に一旦anim1クラスを除去し、ウィンドウのスクロールがanim1クラスを持つ要素の位置に達した場合にanim1クラスを付与する、という処理を行うことで、スクロールに応じたアニメーションを実現します。 

Javascriptコードと解説

(function($) {
$.mamewaza_scroll = function(cls) {
    if($.isEmptyObject(cls) ) {
        return "";
    }

    var daemon = function() {
        var y = $(window).scrollTop() + $(window).height();
        for(var i = arr.length - 1; i >= 0; i--) {
            if(arr[i].y > y) {
                break;
            }

            arr[i].elm.addClass(arr[i].cls);

            arr.splice(i, 1);
        }

        if(arr.length == 0) {
            $(window).unbind("scroll", daemon);
        }
    };

    var arr = [];
    var y = $(window).scrollTop() + $(window).height();
    for(var i = 0; i < cls.length; i++) {
        if(!cls[i] || !$("." + cls[i])[0]) {
            continue;
        }

        var j = 0;
        $("." + cls[i]).each(function() {
            var y_this = $(this).offset().top;
            if(y_this <= y) {
                return "";
            }

            $(this).removeClass(cls[i]);
            arr.push( {
                "y": y_this,
                "elm": $(this),
                "cls": cls[i],
            } );
        } );
    }

    if(arr.length == 0) {
        return "";
    }

    //sort
    arr.sort(function(a, b) {
        return b.y - a.y;
    } );

    $(window).bind("scroll", daemon);
    daemon();
};
})(jQuery); 

今回はjQueryプラグイン形式にしています。

関数は次のように実行します。
$.mamewaza_scroll( ["anim1", "anim2"] );
引数は配列形式で、アニメーションを定義済みのクラスを含めます。

中身ですが、まず
var arr = [];
以降をご覧ください。この下のループで、指定されたクラスを持つ要素をスキャンし、それぞれの要素のy座標をarrに保持します。加えてクラスを除外します。
ウィンドウがスクロール済みの場合は、スクロールより上の要素は無視します。
次に、arrをy座標の降順にソートします(後にarrを逆からループするため)。
最後にスクロールイベントを
$(window).bind("scroll", daemon);
としてセットします。
daemon()
として初回のイベントも実行します。
ここまでが関数実行時の処理です。

daemon関数は、スクロールに応じて呼び出されます。
arrをループしてスクロール量と比較し、スクロール量がy座標を上回った要素にクラスをセットします。
クラスをセットした際、splice関数で該当要素を配列から除去するため、配列を逆ループしています。
また、予めy座標の降順でソートすることで、スクロール量がy座標に達していない場合は、それ以降のループを停止し、無駄な処理を避けています。
最後に、全ての要素にクラスがセットされ配列が空になったら
$(window).unbind("scroll", daemon);
としてスクロールイベントを除去します。

テキストを格好良く表示するCSSのanimation

まずはテキストを格好良く表示するアニメーションを考えてみます。
全てtransformとopacityの組合せをkeyframes内に定義しています。
HTMLコードは共通で以下です(○にはCSSで定義する数字が入ります)。

<h2 class="anim○">祇園精舎の鐘の声</h2>
<p class="anim○">諸行無常の響あり<br />
娑羅双樹の花の色、盛者必衰の理をあらはす</p> 

その1 スライドイン

.anim1 {
    -webkit-animation: 1.5s forwards anim1;
    animation: 1.5s forwards anim1;
}
    @-webkit-keyframes anim1 {
        0% {
            -webkit-transform: translate(0, 20px);
            opacity: 0;
        }
        100% {
            -webkit-transform: translate(0, 0);
            opacity: 1;
        }
    }
    @keyframes anim1 {
        0% {
            transform: translate(0, 20px);
            opacity: 0;
        }
        100% {
            transform: translate(0, 0);
            opacity: 1;
        }
    } 

亜種

transform: translate(0, -20px);
の方向を変えて
transform: translate(0, -20px);
transform: translate(-100px, 0);
transform: translate(100px, 0);
にしたものも用意しました。上をクリックしてご覧ください。

その2 残像スライドイン

.anim2 {
    -webkit-animation: 2s forwards anim2;
    animation: 2s forwards anim2;
}
    @-webkit-keyframes anim2 {
        0% {
            -webkit-text-shadow: rgba(0, 0, 0, 1) 0 10px 0;
            opacity: 0;
        }
        100% {
            -webkit-text-shadow: rgba(0, 0, 0, 0) 0 0 0;
            opacity: 1;
        }
    }
    @keyframes anim2 {
        0% {
            text-shadow: rgba(0, 0, 0, 1) 0 10px 0;
            opacity: 0;
        }
        100% {
            text-shadow: rgba(0, 0, 0, 0) 0 0 0;
            opacity: 1;
        }
    } 

亜種

text-shadow: rgba(0, 0, 0, 1) 0 10px 0;
の方向を変えて
text-shadow: rgba(0, 0, 0, 1) 0 -10px 0;
text-shadow: rgba(0, 0, 0, 1) -10px 0px 0;
text-shadow: rgba(0, 0, 0, 1) 10px 0px 0;
にしたものも用意しました。上をクリックしてご覧ください。

その3 ズーム

.anim3 {
    -webkit-animation: 1.5s forwards anim3;
    animation: 1.5s forwards anim3;
}
    @-webkit-keyframes anim3 {
        0% {
            -webkit-transform: scale(0.2, 0.2);
            opacity: 0;
        }
        100% {
            -webkit-transform: scale(1, 1);
            opacity: 1;
        }
    }
    @keyframes anim3 {
        0% {
            transform: scale(0.2, 0.2);
            opacity: 0;
        }
        100% {
            transform: scale(1, 1);
            opacity: 1;
        }
    } 

亜種

transform: scale(0.2, 0.2);

transform: scale(1.5, 1.5);
にして縮小にしたもの、
transform: scale(1, 5);
transform: scale(1, 0);
にして縦方向のみのズームにしたものも用意しました。上をクリックしてご覧ください。

その4 回転

.anim4 {
    -webkit-animation: 1.5s forwards anim4;
    animation: 1.5s forwards anim4;
}
    @-webkit-keyframes anim4 {
        0% {
            -webkit-transform: rotate(-720deg) scale(0.1, 0.1);
            opacity: 0;
        }
        100% {
            -webkit-transform: rotate(0deg) scale(1, 1);
            opacity: 1;
        }
    }
    @keyframes anim4 {
        0% {
            transform: rotate(-720deg) scale(0.1, 0.1);
            opacity: 0;
        }
        100% {
            transform: rotate(0deg) scale(1, 1);
            opacity: 1;
        }
    } 

亜種

回転方法を縦方向・横方向のみにしたものを用意しました。

.anim4-2 {
    -webkit-animation: 1s forwards anim4-2;
    animation: 1s forwards anim4-2;
}
    @-webkit-keyframes anim4-2 {
        0% {
            -webkit-transform: scale(1, 1);
        }
        20% {
            -webkit-transform: scale(1, -1);
        }
        80% {
            -webkit-transform: scale(1, -1);
        }
        100% {
            -webkit-transform: scale(1, 1);
        }
    }
    @keyframes anim4-2 {
        0% {
            transform: scale(1, 1);
        }
        20% {
            transform: scale(1, -1);
        }
        80% {
            transform: scale(1, -1);
        }
        100% {
            transform: scale(1, 1);
        }
    } 

.anim4-3 {
    -webkit-animation: 1.5s forwards anim4-3;
    animation: 1.5s forwards anim4-3;
}
    @-webkit-keyframes anim4-3 {
        0% {
            -webkit-transform: scale(-1, 1);
            opacity: 0;
        }
        100% {
            -webkit-transform: scale(1, 1);
            opacity: 1;
        }
    }
    @keyframes anim4-3 {
        0% {
            transform: scale(-1, 1);
            opacity: 0;
        }
        100% {
            transform: scale(1, 1);
            opacity: 1;
        }
    } 

その5 ブルブル

.anim5 {
    -webkit-animation: 1.5s linear forwards anim5;
    animation: 1.5s linear forwards anim5;
}
    @-webkit-keyframes anim5 {
        0% {
            -webkit-transform: translate(20px, 20px) scale(1.2, 1.2);
        }
        5% {
            -webkit-transform: translate(-18px, -18px) scale(1.15, 1.15);
        }
        15% {
            -webkit-transform: translate(16px, 16px) scale(1.1, 1.1);
        }
        20% {
            -webkit-transform: translate(-14px, -14px) scale(1.05, 1.05);
        }
        25% {
            -webkit-transform: translate(12px, 12px) scale(1, 1);
        }
        30% {
            -webkit-transform: translate(-10px, -10px) scale(1, 1);
        }
        35% {
            -webkit-transform: translate(8px, 8px) scale(1, 1);
        }
        40% {
            -webkit-transform: translate(-6px, -6px) scale(1, 1);
        }
        45% {
            -webkit-transform: translate(4px, 4px) scale(1, 1);
        }
        50% {
            -webkit-transform: translate(-2px, -2px) scale(1, 1);
        }
        55% {
            -webkit-transform: translate(0, 0) scale(1, 1);
        }
        85% {
            -webkit-transform: translate(0, 0) scale(1, 1);
        }
        90% {
            -webkit-transform: translate(2px, 2px) scale(1, 1);
        }
        95% {
            -webkit-transform: translate(-2px, -2px) scale(1, 1);
        }
        100% {
            -webkit-transform: translate(0, 0) scale(1, 1);
        }
    }
    @keyframes anim5 {
        0% {
            transform: translate(20px, 20px) scale(1.2, 1.2);
        }
        5% {
            transform: translate(-18px, -18px) scale(1.15, 1.15);
        }
        15% {
            transform: translate(16px, 16px) scale(1.1, 1.1);
        }
        20% {
            transform: translate(-14px, -14px) scale(1.05, 1.05);
        }
        25% {
            transform: translate(12px, 12px) scale(1, 1);
        }
        30% {
            transform: translate(-10px, -10px) scale(1, 1);
        }
        35% {
            transform: translate(8px, 8px) scale(1, 1);
        }
        40% {
            transform: translate(-6px, -6px) scale(1, 1);
        }
        45% {
            transform: translate(4px, 4px) scale(1, 1);
        }
        50% {
            transform: translate(-2px, -2px) scale(1, 1);
        }
        55% {
            transform: translate(0, 0) scale(1, 1);
        }
        85% {
            transform: translate(0, 0) scale(1, 1);
        }
        90% {
            transform: translate(2px, 2px) scale(1, 1);
        }
        95% {
            transform: translate(-2px, -2px) scale(1, 1);
        }
        100% {
            transform: translate(0, 0) scale(1, 1);
        }
    } 

その6 影スライド

.anim6 {
    -webkit-animation: 1.5s forwards anim6;
    animation: 1.5s forwards anim6;
}
    @-webkit-keyframes anim6 {
        0% {
            -webkit-text-shadow: #333 30px 0px 0, #333 29px 0px 0, #333 28px 0px 0, #333 27px 0px 0, #333 26px 0px 0, #333 25px 0px 0, #333 24px 0px 0, #333 23px 0px 0, #333 22px 0px 0, #333 21px 0px 0, #333 20px 0px 0, #333 19px 0px 0, #333 18px 0px 0, #333 17px 0px 0, #333 16px 0px 0, #333 15px 0px 0, #333 14px 0px 0, #333 13px 0px 0, #333 12px 0px 0, #333 11px 0px 0, #333 10px 0px 0, #333 9px 0px 0, #333 8px 0px 0, #333 7px 0px 0, #333 6px 0px 0, #333 5px 0px 0, #333 4px 0px 0, #333 3px 0px 0, #333 2px 0px 0, #333 1px 0px 0;
        }
        100% {
            -webkit-text-shadow: rgba(0, 0, 0, 0) 0 0 0;
        }
    }
    @keyframes anim6 {
        0% {
            text-shadow: #333 30px 0px 0, #333 29px 0px 0, #333 28px 0px 0, #333 27px 0px 0, #333 26px 0px 0, #333 25px 0px 0, #333 24px 0px 0, #333 23px 0px 0, #333 22px 0px 0, #333 21px 0px 0, #333 20px 0px 0, #333 19px 0px 0, #333 18px 0px 0, #333 17px 0px 0, #333 16px 0px 0, #333 15px 0px 0, #333 14px 0px 0, #333 13px 0px 0, #333 12px 0px 0, #333 11px 0px 0, #333 10px 0px 0, #333 9px 0px 0, #333 8px 0px 0, #333 7px 0px 0, #333 6px 0px 0, #333 5px 0px 0, #333 4px 0px 0, #333 3px 0px 0, #333 2px 0px 0, #333 1px 0px 0;
        }
        100% {
            text-shadow: rgba(0, 0, 0, 0) 0 0 0;
        }
    } 

亜種

影の方向を変えて
text-shadow:・・・

text-shadow: #333 0 -30px 0, #333 0 -29px 0, #333 0 -28px 0, #333 0 -27px 0, #333 0 -26px 0, #333 0 -25px 0, #333 0 -24px 0, #333 0 -23px 0, #333 0 -22px 0, #333 0 -21px 0, #333 0 -20px 0, #333 0 -19px 0, #333 0 -18px 0, #333 0 -17px 0, #333 0 -16px 0, #333 0 -15px 0, #333 0 -14px 0, #333 0 -13px 0, #333 0 -12px 0, #333 0 -11px 0, #333 0 -10px 0, #333 0 -9px 0, #333 0 -8px 0, #333 0 -7px 0, #333 0 -6px 0, #333 0 -5px 0, #333 0 -4px 0, #333 0 -3px 0, #333 0 -2px 0, #333 0 -1px 0;
にしたものも用意しました。上をクリックしてご覧ください。

その他いろいろ

アニメーションの利用について

ここで解説したアニメーションはご自由に使っていただいて結構です(そのうちツールに追加したいと思っています)。改変OK、報告不要です。
上で説明したサンプルを開いて、そのソースからコピーするのが最も簡単だと思われます。
気に入っていただけましたら、シェアをお願いいたします。

動作ブラウザ

CSSのanimationを実装する各種モダンブラウザ、スマホブラウザで動作します。
animation-fill-modeを利用しているため、Android2.3では動作しません。

スマートフォンで起動させない

if(!navigator.userAgent.match(/iPod|iPhone|iPad|Android/i) ) {
$.mamewaza_scroll( ["anim1", "anim2"] );
}
のようにして分岐処理をしてください。

重くなる?利用すべきか?

処理をなるべく少なくしていますが、スクロールたびにループ処理を実行しているので若干重くなります。アニメーションの対象が多いほどループ回数が増えて負担になります。動作が重くなる場合は、ユーザビリティに配慮して利用しない選択も考えましょう。
スクロールに応じてテキストを少しだけ動かすのは、例えば画像が際限なくスライドするのに比べるとかなり控えめな「インタラクティブコンテンツ」と言えます。アニメーションはすぐに終了し、判読性に悪影響がないので、ユーザビリティに配慮したアニメーションです。

2016/10/26