複数のアニメーションのscrollイベントはブラウザの負荷が気になる!!どうしたらいいかな?
scrollイベントは1つのページにたくさん使いすぎると1pxスクロールするごとにJavaScriptが動いてしまうので、ブラウザに負荷がかかりすぎてとスクロールがカクついたりします。
なので、scrollイベントはたくさん設置するのは不向きになります!
ですが、ホームページ制作をしていたりすると、スクロールするたびにいろんな箇所がアニメーションをするサイトを一度は見たことがあるんじゃないでしょうか?
そういうアニメーションを設置する時に使える、JavaScriptがデフォルトで用意してくれているオブジェクト「intersectionObserverAPI」を紹介したいと思います。
名前からして難しそうに思いますが、実は無茶苦茶簡単です!記述量はscrollイベントよりも少ないくらい!取得するHTMLはアニメーションさせるHTMLのみです!
非同期でスクロールを邪魔しないintersectionObserverAPIを必ずマスターしておきましょう!
intersectionObserverで複数のアニメーションを設置してみよう
intersectionObserverで複数のアニメーションを設置する方法
まずは完成イメージはこのような感じ。
まずはこのような感じで複数の画像をスクロールきっかけでフェードインさせていきます!
ファイル構造を載せておきます。こちらの通りにファイルを作ってください。
├── images
│ ├── blog.jpg
│ └── blog2.jpg
├── index.html
├── index.js
└── style.css
まずはHTMLを書いていきます
index.htmlに下記内容を書いていってください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>intersectionObserverProject</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<section class="sec"></section>
<section class="sec2"><img src="images/blog.jpg" alt="" class="sec__img anim"></section>
<section class="sec3"><img src="images/blog2.jpg" alt="" class="sec__img anim"></section>
<section class="sec4"><img src="images/blog.jpg" alt="" class="sec__img anim"></section>
<section class="sec5"><img src="images/blog2.jpg" alt="" class="sec__img anim"></section>
<section class="sec6"><img src="images/blog.jpg" alt="" class="sec__img anim"></section>
<section class="sec7"></section>
<script src="index.js"></script>
</body>
</html>
シンプルにsectionタグと画像を並べただけのHTML。画像は好きなものを配置してください。
画像にanimクラスを設置してますが、animクラスはJavaScriptを接続する用のクラスになっております。
CSSで見た目を整えていきます
style.cssはこのような感じで書いておきました。
body {
margin: 0;
}
.sec, .sec7 {
height: 800px;
}
.sec2,
.sec3,
.sec4,
.sec5,
.sec6 {
height: 350px;
display: flex;
align-items: flex-end;
}
.sec2, .sec3, .sec6 {
justify-content: flex-start;
}
.sec3, .sec5 {
justify-content: flex-end;
}
img {
width: 450px;
}
.sec__img {
opacity: 0;
transform: translate(0px, 50px);
transition: all .6s ease-in-out;
}
/* アニメーション後のスタイル */
.sec__img.fadeIn {
opacity: 1;
transform: translate(0px, 0px);
}
CSSもシンプルですね。スクロールしやすいように高さをセクションごとに設定しております。画像の位置はフレックスボックスを使って配置してます。
アニメーションのスタイルは、opacityで透明にしておいて、transform:translateで50px下に配置して、アニメーション後は透明度を1にして、translateを0pxにして元の位置に戻すアニメーションを設置しました。
それではお待ちかねのJavaScriptを書いていきます。
JavaScriptを設定します
index.jsに書いていきます。
まずは、アニメーションをさせたい要素を取得しましょう!
'use strict';
{
// DOM取得
const targets = document.querySelectorAll('.anim');
// デバッグ
console.log(targets);
}
アニメーションするimgタグが複数なので、animクラスをquerySelectorAllで全て取得します。
console.logでデバックしたので、コンソールでちゃんと取得できてるかを確認します。
ちゃんとanimクラスを付加したimgタグを取得できてそうです。
次はintersectionObserverで初期化と、監視の開始までを書いてみましょう。intersectionObserverはJavaScriptでデフォルトで用意されているオブジェクトなので、ライブラリの読み込みなどは必要ありません。ただ書くだけです。
オブジェクトの概念がわからない人はこちらの記事を参考にしてください。サラッと概念を理解しておくだけでも後々役に立ちます。
それでは書いていきます!!
'use strict';
{
// DOM取得
const targets = document.querySelectorAll('.anim');
console.log(targets);
// 初期化
const observer = new IntersectionObserver(callback, options);
// 監視を開始
targets.forEach(target => {
observer.observe(target);
});
}
内容の解説ですが、
const observer = new IntersectionObserver(callback, options);
この部分でintersectionObserverを初期化しています。初期化とは今から使い始めますよ!!と宣言するということです。これをしないとintersectionObserverを使うことができません。
初期化の場合、下記添付画像のようにnewの後、1文字目は大文字で書くので注意してください。
初期化が完了して、それを変数observerに入れています。次はobserverを使ってHTMLの監視を開始します。
// 監視を開始
targets.forEach(target => {
observer.observe(target);
});
この部分ですね。監視を開始するには、
observer.observe()という書き方をします。
その前に、監視をするimgタグは複数なので、forEachでtargetsの中身を一つずつ取り出す処理をする必要があります。そして取り出した監視対象を変数targetに入れて、observe()の中に入れることで全てのimgタグの監視がスタートします。
もう一度前のコードに戻ります。
const observer = new IntersectionObserver(callback, options);
初期化した時に、new IntersectionObserver()の中には2つの引数をセットしないとけません。
- callback -> 処理の内容を書く
- options -> rootの設定と、処理を発火させるタイミングを設定
2つの引数はこのような役割になっております。
冒頭でintersectionObserverは非同期で動くと書いていましたが、それはcallbackという関数のことで、callback関数は処理の準備は裏で待機させておいて、処理を発火させるタイミングで動くように待機させる設定にしてあります。
非同期は処理は設定しているけど、一旦待機だよ!と裏側で待たしている状態なので必要なタイミングですぐに実行できます!!
関数の実行は関数名の後に括弧を書きますが、括弧を付けないことで、処理を実行させないで待機させておく。という意味になっています。非同期の処理は読込やスクロールの処理の邪魔をしないので、使いやすい処理です。
次はオプションの設定をしていきます。第二引数でoptionsと書いたものですね。
書き方はこんな感じで書きます。
'use strict';
{
// DOM取得
const targets = document.querySelectorAll('.anim');
console.log(targets);
// オプション
const options = {
root: null,
rootMargin: "0px 0px -250px 0px",
threshold: [0]
}
// 初期化
const observer = new IntersectionObserver(callback, options);
// 監視を開始
targets.forEach(target => {
observer.observe(target);
});
}
optionsは波括弧で設定するのでオブジェクトで設定します。設定する値は、root、rootMargin、thresholdの3つになります。必ずこの3つの項目を配置しておきます。
rootの設定
今回はrootにnullの値を入れていきます。nullとは空ということです。nullを設定するとビューポートがrootになります。
ビューポートがrootになる?どゆこと?てなりますよねww
intersectionObserverAPIは、2つの要素が重なったタイミングで発動します。
2つの要素とは、
- アニメーションさせる要素
- root
先ほど、JavaScriptで取得したHTMLがアニメーションさせる要素です。そのアニメーションさせる要素のアニメーションを動かすタイミングがrootと交差したタイミングになります。
そしてrootをnullにするとビューポートがrootになります。ビューポートとはパソコンで見えている部分がビューポートです。すなわちパソコン画面内にアニメーションさせる要素が入ったタイミング(交差)でアニメーションスタートとなります。
言葉で説明してもうまく伝わるかわからないかもなので、この後動かしてみていきましょう!
rootMarginの設定
次のオプションはrootMarginです。
rootMargin: "0px 0px -250px 0px",
rootMarginはrootにCSSのmarginみたいなものを設定できます。設定するとどうなるか?ですが、例えば、rootの下に20pxのmarginを設定すると、アニメーションのスタートが20pxぶん速くスタートします。
実際にはあまり20pxなどは使わなくて、ネガティブマージンをよく使います。ネガティブマージンとは-20pxみたいなマイナスの値を設定したmarginのことです。rootの下に-20pxを設定すると、アニメーションが20pxぶん遅れてスタートします。
あまり速くにアニメーションをスタートするとアニメーションがよく見えないので、アニメーションのスタートを遅らせることでアニメーションを目立たせます。
rootMarginの書き方は、
rootMargin: "上のmargin 右のmargin 下のmargin 左のmargin",
このような時計回りの順番でmarginを指定します。注意点としてはmarginの数値が0であってもpxをつけないとエラーになるので注意してください。
rootMarginはmarginの数値が0であってもpxをつけないとエラーになるので注意
今回の設定では、下のmarginを-250pxにして250pxぶん遅らせて発火させるように設定しております。
thresholdの設定
こちらは監視要素側(アニメーションさせる要素)の設定です。現状は配列で[0]となっています。
threshold: [0]
この設定ではどうなるかというと、0%交差すると発火するということになります。すなわち交差した瞬間にアニメーションが開始されるということです。
0.5にしてみます。
threshold: [0.5]
0.5にすると、監視要素の50%部分でrootと交差するとアニメーションが開始となるということになります。0〜1までの数値で指定できます。thresholdを指定すれば、監視要素に何%重なれば発火するという設定が可能になります。
このようにアニメーションの開始のタイミングは監視要素でもできるし、root要素でもrootMarginを使ってできるので、かなり柔軟に対応できるかと思います。
以上、オプションの設定の解説でした!!次は交差した時の処理のcallback関数を書いていきますね。
交差した時の処理を関数を使って書いていきます。
index.jsファイルに追記していきましょう。
'use strict';
{
// DOM取得
const targets = document.querySelectorAll('.anim');
// オプション
const options = {
root: null,
rootMargin: "0px 0px -300px 0px",
threshold: [0]
}
// 処理
function callback(entries) {
console.log(entries); /* entriesは配列 */
entries.forEach(entry => {
console.log(entry); /* entryを取り出す */
// entry.isIntersectingがtrueの時だけ処理を実行する
if (entry.isIntersecting) {
entry.target.classList.add('fadeIn');
}
});
}
// 初期化
const observer = new IntersectionObserver(callback, options);
// 監視を開始
targets.forEach(target => {
observer.observe(target);
});
}
「// 処理」の部分を追記しました。今回動きを確認するためにconsole.logを仕込んでおります。ややこしくなるので、「// DOM取得」の部分のconsole.logは削除しました!
まず、callback関数は引数にentriesという引数を取得できます。このentries引数には監視する要素が全て入った情報を配列で取得します。
一度この状態で、ブラウザのコンソールを開いてみましょう。
少しみにくいかもですが、index.jsの15行目のconsole.logは(5)となっていますね。配列で5個の要素を取得できているみたいです。
entriesに配列が入ってきているところがわかったら、次はforEachの中に書いたentryもcosole.logしてました。entriesからentryという変数に監視要素を取り出してます。
forEachなので繰り返し処理です。index.jsの17行目のcosole.logが5回繰り返されるのが確認できます。こちら配列から一つずつ各監視要素を取り出すことに成功しています!
この様にcallback関数ではまず配列を引数で受け取って、その配列を一つずつ取り出すという作業が必要になってきます。これは監視要素が一つでも複数でも必要な作業なので、覚えておいてください!
コンソールを開いたままで、スクロールしてみてください。
最初にブラウザをリロードすると一回実行されて、画像と交差したタイミングで一回実行されてます。つまりスクロールすると計2回実行されることになります。
ブラウザを読み込んだ瞬間にアニメーションが動いてしまうと困りますので、この実行のタイミングを一つにする必要があります。
具体的にどうするかというと、コンソールのforEachで取り出した部分を開いてみてください。
赤線でなぞったisIntersecting: falseという項目があると思います。こちらは要素の上に乗っている時はtrueになるという特性があります!要素から外れるとfalseになります。
コンソールを開いてスクロールしてみましょう。
上記動画にある通り要素に乗っかると、上記動画にある通り、監視対象の上に乗るとisIntersectingがtrueになって、戻って監視対象をビューポート外に出ると、isIntersectingがfalseになったのがわかったのではないでしょうか?
この特性を利用して、isIntersectingがtrueになったら、というif文で処理を振り分けます。それがこのコードの部分になります。
// entry.isIntersectingがtrueの時だけ処理を実行する
if (entry.isIntersecting) {
entry.target.classList.add('fadeIn');
}
isIntersectingの値を取得するには、entry.isIntersectingでtrueかfalseを取得できます。if文はtrueかfalseで処理を分けますので、entry.isIntersectingがtrueの場合だけif文の中身の処理を走らすようにします。これでintersectionObserverの処理回数を一回に絞り込むことができました!!
trueの時のif文の中の処理はこう書いています。
entry.target.classList.add('fadeIn');
entry.targetと書くことで、監視要素を取得できてます。
コンソールを開いて、entryを開いてみましょう。
entryの中にtargetという項目がありますよね?entry.tagetと繋げて書くことで監視要素を取得できます。
それにclassList.add('fadeIn')と書くことでクラスを付加しています。つまり、entry.isIntersectingがtrueになったらクラスを付加するというプログラムをこれで組むことができました!
これで動きをちゃんとみてみましょう!!
ちゃんと全ての画像に動きがつきましたね!
アニメーションのタイミングが250pxぶん画面に入ってからスタートしているのがわかっていただけましたでしょうか?もう一度、そのタイミングを意識してみてみましょう!
今は全ての画像のアニメーションは一緒ですが、CSSを工夫することで、一部だけアニメーションを変更することも可能です!
一つだけ違うアニメーションにしてみましょう。
intersectionObserverで部分的に違うアニメーションにする
アニメーションの部分的な変更は簡単です。アニメーションのスライド部分はtransformを使っているので、ここを変更するだけでOK!
style.cssに追記します。
body {
margin: 0;
}
.sec, .sec7 {
height: 800px;
}
.sec2,
.sec3,
.sec4,
.sec5,
.sec6 {
height: 350px;
display: flex;
align-items: flex-end;
}
.sec2, .sec3, .sec6 {
justify-content: flex-start;
}
.sec3, .sec5 {
justify-content: flex-end;
}
img {
width: 450px;
}
.sec__img {
opacity: 0;
transform: translate(0px, 50px);
transition: all .6s ease-in-out;
}
/* 違うバージョンのアニメーション、追記する */
.sec__img--animVer1 {
transform: translate(-50px, 0px);
}
/* 追記終わり */
/* アニメーション後のスタイル */
.sec__img.fadeIn {
opacity: 1;
transform: translate(0px, 0px);
}
この部分を追記しました!
/* 違うバージョンのアニメーション、追記する */
.sec__img--animVer1 {
transform: translate(-50px, 0px);
}
/* 追記終わり */
CSSを一つだけ追加です!めっちゃ簡単ですね。そしてindex.htmlにクラスを付加します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>intersectionObserverProject</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<section class="sec"></section>
<section class="sec2"><img src="images/blog.jpg" alt="" class="sec__img anim"></section>
<section class="sec3"><img src="images/blog2.jpg" alt="" class="sec__img anim"></section>
<!-- sec__img--animVer1クラスを付加 -->
<section class="sec4"><img src="images/blog.jpg" alt="" class="sec__img sec__img--animVer1 anim"></section>
<section class="sec5"><img src="images/blog2.jpg" alt="" class="sec__img anim"></section>
<section class="sec6"><img src="images/blog.jpg" alt="" class="sec__img anim"></section>
<section class="sec7"></section>
<script src="index.js"></script>
</body>
</html>
sec4の中にあるimgタグにsec__img--animVer1クラスを付加しました!!
これでどうなったかみてみましょう!!
3番目の画像だけ左からのフェードスライドインになりましたね!このようにCSSをちょっといじるだけでいろんなスクロールアニメーションを楽しめるので、良かったら試していろいろ遊んでみてください♪
現状はアニメーションさせる画像に重なってから250px進んだタイミングでアニメーションをスタートしています。
アニメーションをスタートするタイミングを各監視要素に対して変更したい!ということもあるかと思います。次はこのタイミングを調整できるようにintersectionObserverを設定を変更してみましょう!!
intersectionObserverで要素ごとにタイミングを変更する方法
intersectionObserverでアニメーションののスタートするタイミングを変更するのは、rootMarginとthresholdの2つの方法があると先ほど書きました。
では同じページ内で監視要素ごとにアニメーションのスタートするタイミングを変える方法はどうすればいいでしょうか?
まず方法の一つとして、もう一度、初期化してintersectionObserverをもう一つ定義するという方法があります。確かにこの方法だともう一つ定義したintersectionObserverの設定を違う設定にすれば、違うタイミングのアニメーションを設定できそうです。
しかし、この方法だと違うタイミングのアニメーションを定義する度にコードが増えていき、管理しにくいコードになってしまいそうです。できれば一度初期化したintersectionObserverを使い回ししたいですよね。
intersectionObserverを使い回しを可能にするのが関数化です。
初期化したコードを関数化して、必要なデータだけを関数実行時に引数を使って入れてやる方法です。
引数がよくわからないなら、こちらの記事を参考にしてみてください!!
それでは関数化して違うタイミングのintersectionObserverを作っていきましょう!!
まずはDOM取得部分以外のコードをfunctionの中に入れていきます!!
'use strict';
{
// DOM取得
const targets = document.querySelectorAll('.anim');
// 引数を2つ設定する
function animObserver(elements, timimg) {
// オプション
const options = {
root: null,
rootMargin: "0px 0px -250px 0px",
threshold: [0]
}
// 処理
function callback(entries) {
console.log(entries); /* entriesは配列 */
entries.forEach(entry => {
console.log(entry); /* entryを取り出す */
// entry.isIntersectingがtrueの時だけ処理を実行する
if (entry.isIntersecting) {
entry.target.classList.add('fadeIn');
}
});
}
// 初期化
const observer = new IntersectionObserver(callback, options);
// 監視を開始
targets.forEach(target => {
observer.observe(target);
});
}
// 関数を実行
animObserver();
}
DOM取得以外のコードをfunction(関数)の中に入れました!関数の名前はanimObserverとでもしておきます。関数を作ったら忘れずに実行部分も書いておきましょう。
実行部分はこの部分ですね。
// 関数を実行
animObserver();
このanimObserver関数の引数は2つ設定します。こちらの箇所のことです。
// 引数を2つ設定する
function animObserver(elements, timimg) {
引数とは関数で外部からデータを取り込めるようにする機能のことです。elementsとtimingの2つの引数を設定しておきます。
この引数をどう使うかですが、
- elements → 監視する要素を指定する
- timing → rootMarginの値を指定する
監視する要素と、rootMarginの値をanimObserver関数でで取り込む設定にしておきます。
次はこの引数を関数内で設定します!
'use strict';
{
// DOM取得
const targets = document.querySelectorAll('.anim');
// 引数を2つ設定する
function animObserver(elements, timimg) {
// オプション
const options = {
root: null,
// timingをセット
rootMargin: `0px 0px ${timimg}px 0px`,
threshold: [0]
}
// 処理
function callback(entries) {
console.log(entries); /* entriesは配列 */
entries.forEach(entry => {
console.log(entry); /* entryを取り出す */
// entry.isIntersectingがtrueの時だけ処理を実行する
if (entry.isIntersecting) {
entry.target.classList.add('fadeIn');
}
});
}
// 初期化
const observer = new IntersectionObserver(callback, options);
// 監視を開始
// elementsをセットする
elements.forEach(target => {
observer.observe(target);
});
}
// 関数を実行
animObserver();
}
timing引数はrootMarginに指定します。
// timingをセット
rootMargin: `0px 0px ${timimg}px 0px`,
文字列の中に変数をセットするにはテンプレートリテラルという手法を使い、ダブルクオテーションだったところをバッククオテーションで囲うと、文字列の中に${}を使って変数を入れることができます!!これで引数を渡って、数値を指定することができたハズです!
次は監視を開始のところに、elementsを設定します!
// 監視を開始
// elementsをセットする
elements.forEach(target => {
observer.observe(target);
});
もともとtargetsと書いていたところをelementsに変更しましょう!これで監視したい要素を関数外からとりこんた要素をこの監視部分にセットできました!!
引数の設定は完了しましたので、値をセットしてみましょう!!
'use strict';
{
// 引数を2つ設定する
function animObserver(elements, timimg) {
// オプション
const options = {
root: null,
// timingをセット
rootMargin: `0px 0px ${timimg}px 0px`,
threshold: [0]
}
// 処理
function callback(entries) {
console.log(entries); /* entriesは配列 */
entries.forEach(entry => {
console.log(entry); /* entryを取り出す */
// entry.isIntersectingがtrueの時だけ処理を実行する
if (entry.isIntersecting) {
entry.target.classList.add('fadeIn');
}
});
}
// 初期化
const observer = new IntersectionObserver(callback, options);
// 監視を開始
// elementsをセットする
elements.forEach(target => {
observer.observe(target);
});
}
// DOM取得
const targets = document.querySelectorAll('.anim');
// 関数を実行
// 値をセットして実行
animObserver(targets, -250);
}
関数を実行部分だけを書き換えました!!DOM取得部分はみやすいように上から持ってきました!
DOM習得で作った関数targetsと、rootMarginでセットしていた-250を関数実行時に括弧内に2つセットして実行します。
引数でセットした順に設定しないとエラーになるので注意してください!!
ようは何をしているかというと、関数を利用して、変更したい部分を引数にしておくことで、関数実行時に値を入れ替えることでアニメーションのタイミングを柔軟に変更できるようにしました。
これで一度、ちゃんと動くかみてみましょう。
ちゃんと動きましたね!!これでしっかりと引数で設定できていることがわかりました!!
下準備がやっと完了しました。すいません、、
次は一つだけアニメーションのタイミングを変えていきます。
index.htmlでクラスを一つ付けてください。
〜
以下省略
<!-- sec__img--animVer1にクラスを付加 -->
<section class="sec4"><img src="images/blog.jpg" alt="" class="sec__img sec__img--animVer1 anim"></section>
<!-- animクラスをanim2に変更する -->
<section class="sec5"><img src="images/blog2.jpg" alt="" class="sec__img anim2"></section>
<section class="sec6"><img src="images/blog.jpg" alt="" class="sec__img anim"></section>
<section class="sec7"></section>
以下省略
〜
sec5の中にあるimgタグのanimクラスをanim2クラスに変更します!
animクラスは監視要素に設定するためのクラスでしたよね?そこを変更しておきます。
そして、もう一度関数実行していきます!!index.jsに追記します。
'use strict';
{
// 引数を2つ設定する
function animObserver(elements, timimg) {
// オプション
const options = {
root: null,
// timingをセット
rootMargin: `0px 0px ${timimg}px 0px`,
threshold: [0]
}
// 処理
function callback(entries) {
console.log(entries); /* entriesは配列 */
entries.forEach(entry => {
console.log(entry); /* entryを取り出す */
// entry.isIntersectingがtrueの時だけ処理を実行する
if (entry.isIntersecting) {
entry.target.classList.add('fadeIn');
}
});
}
// 初期化
const observer = new IntersectionObserver(callback, options);
// 監視を開始
// elementsをセットする
elements.forEach(target => {
observer.observe(target);
});
}
// DOM取得
const targets = document.querySelectorAll('.anim');
// anim2を取得
const targets2 = document.querySelectorAll('.anim2');
// 関数を実行
// 値をセットして実行
animObserver(targets, -250);
// target2をセット
animObserver(targets2, -500);
}
さきほど変更したanim2を取得します!!取得したtargets2変数をもう一度、animObserver()を書いてそこにセットしましょう!!rootMarginは、-500をセットします!
もう一度、初期化することを考えると、めちゃくちゃコードの短縮ができていると思います。一度関数をセットしておけば、後から値を入れ変えて何度も実行できるのでめっちゃ便利です!
では、これでもう一度スクロールしてみましょう!!
4つ目画像だけ、アニメーションのタイミングをだいぶ後にずらすことができました!!
このようにタイミングの違うパターンを作りたいときは、関数実行時に値を入れ替えることができるので、また機会がありましたらこの方法も試してみてください。
今回のコードもこちらからダウンロードできるようにしておきます。
もし実装などで上手くいかないことありましたら、下記ボタンからご相談いただけましたらありがたいです。