スクロールをするとテキストがスライドしながらフェードインするなど、スクロールに応じたアニメーションをよく見るようになりました。
まめわざでも近々実装を予定しておりますが、ここではその実装方法についてなるべく軽い方法を検証してみます。
という流れで設計を検討します。ただし、今回は「なるべく軽く」がテーマなので、次の方針で設計します。
以下で説明する内容を盛り込んだのがこちらのサンプルです。
次のような流れでスクロールを監視し、アニメーションを実行(animationプロパティを設定したクラスの付与)します。
例えば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クラスを付与する、という処理を行うことで、スクロールに応じたアニメーションを実現します。
(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);
としてスクロールイベントを除去します。
まずはテキストを格好良く表示するアニメーションを考えてみます。
全てtransformとopacityの組合せをkeyframes内に定義しています。
HTMLコードは共通で以下です(○にはCSSで定義する数字が入ります)。
<h2 class="anim○">祇園精舎の鐘の声</h2>
<p class="anim○">諸行無常の響あり<br />
娑羅双樹の花の色、盛者必衰の理をあらはす</p>
.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;
}
}
.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;
}
}
.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;
}
}
.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;
}
}
.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);
}
}
.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"] );
}
のようにして分岐処理をしてください。
処理をなるべく少なくしていますが、スクロールたびにループ処理を実行しているので若干重くなります。アニメーションの対象が多いほどループ回数が増えて負担になります。動作が重くなる場合は、ユーザビリティに配慮して利用しない選択も考えましょう。
スクロールに応じてテキストを少しだけ動かすのは、例えば画像が際限なくスライドするのに比べるとかなり控えめな「インタラクティブコンテンツ」と言えます。アニメーションはすぐに終了し、判読性に悪影響がないので、ユーザビリティに配慮したアニメーションです。