Analog Studio

Intersection Observer API を使った簡単なパララックスの実装方法

概要

パララックスと呼ばれる視覚効果をご存知でしょうか?
スクロールに応じて要素がスクロールとは少し違う動き (左右だったり上下逆だったり、速度が違ったり) をして印象的にする効果です。
遅延読込させた画像のロード時の効果として使われる場合もあり、採用しているサイトは多いです。

しかし、従来は scroll イベントかタイマーで要素を監視して必要になったら動作させるものが主流でした。
scroll イベントは非常に短い間隔で発火され、位置を把握する getBoundingClientRect() 関数による強制レンダリングにより往々にして処理が重くなりがちでユーザ体験を悪化させる一因になりかねないものです。

そんな悩みの種を解消する方法に Intersection Observer API が提供されました。
この API を利用すると従来の高負荷な処理が無くなり、必要なタイミングでのみ処理を行えば良く、非常に効率的になります。

パララックスってどういうものなの?

ご存知の方も多いと思いますが、パララックスがどういったものか確認してみましょう。

以下のボタンをクリックすると本文に対してパララックスを適応します。
クリックしたら記事を適当にスクロールして下さい。コンテンツがふわっと表示されることに気付くでしょう。

Intersection Observer API

では、早速 Intersection Observer API を使ってみましょう。
MDN のドキュメントも併せてご確認下さい。

Intersection Observerオブジェクトをインスタンス化

まずは Intersection Observer がターゲットを見付けた時に呼び出される callback 関数と監視の元となる要素 (ルート要素) を含むオプションを定義して、オブジェクトをインスタンス化します。

オプションは任意となっているので、必要なら設定します。
ルート要素にはデフォルトのビューポート (画面に映っている領域) 以外では任意の要素 (典型的にはスクロールボックスなど) を指定できます。

const options = { root : null, // デフォルトは null で、ビューポートを表す rootMargin : "0px", // ルート要素を上下左右にどれだけ広げるかを CSS の margin プロパティと同じ指定方法で入力する (デフォルトは "0px") threshold : 0.25, // ルート要素にマージンを加えた領域内にターゲット要素がどれだけ入った時に発火するかを0.0~1.0で指定する (デフォルトは1.0) }; const observer = new IntersectionObserver(callback, options); // コールバック関数 callback は後述

これで子孫要素を監視する Intersection Observer オブジェクトが用意できました。
監視を行う子孫要素を指定すれば条件が整った段階で callback 関数が呼ばれます。

監視する子孫要素をセットする

続いて、ルート要素内の位置を監視する子孫要素を先ほど作った Intersection Observer オブジェクトに追加します。
observe メソッドに監視したい要素のセレクタを渡すだけなので簡単です。
また、一つの Intersection Observer オブジェクトで複数の要素を監視できます。

// id="main_content" 以下の画像全て const imgs = document.getElementById("main_content").getElementsByTagName("img"); // 監視する要素を登録する for(let i = 0, n = imgs.length; i < n; i++){ observer.observe(imgs[i]); }

上記ではメインコンテンツ内の画像全てを監視対象にしてみました。
例えば、遅延読込させる画像などがこういった指定方法になります。

callback関数で処理を行う

最後に、API がルート要素内に先ほどの子孫要素が入ったことを検出した場合の処理を行います。 コールバック関数には検出した子孫要素のオブジェクト (IntersectionObserverEntry Object) 配列と監視を行う Intersection Observer のオブジェクトが渡されます。

渡される要素のオブジェクトには以下が含まれます。

const callback = (entries, observer) => { entries.forEach(entry => { // entriesは複数のターゲット要素が同時にコールバックされてくることがあるので配列 if(!entry.isIntersecting) return; // ルート要素内にターゲット要素が表示されているか (初回はなぜか全てのターゲット要素がコールバックされてくる) // 以下やりたい処理を実行する entry.target.src = entry.target.datase.src; // ここまで observer.unobserve(entry.target); // 以降、ターゲット要素の監視が不要なら監視を解除する }); };

Intersection Observer API の使い方コードまとめ

以上のコードをまとめると以下になります。
const で関数を定義している場合は、巻き上げが起こらないので呼び出すより前で定義します。(var の場合はどこでもいい)

const options = { root : null, // デフォルトは null で、ビューポートを表す rootMargin : "0px", // ルート要素を上下左右にどれだけ広げるかを CSS の margin プロパティと同じ指定方法で入力する (デフォルトは "0px") threshold : 0.25, // ルート要素にマージンを加えた領域内にターゲット要素がどれだけ入った時に発火するかを0.0~1.0で指定する (デフォルトは1.0) }; // コールバック関数は先に定義 const callback = (entries, observer) => { entries.forEach(entry => { // entriesは複数のターゲット要素が同時にコールバックされてくることがあるので配列 if(!entry.isIntersecting) return; // ルート要素内にターゲット要素が表示されているか (初回はなぜか全てのターゲット要素がコールバックされてくる) // 以下やりたい処理を実行する entry.target.src = entry.target.datase.src; // ここまで observer.unobserve(entry.target); // 以降、ターゲット要素の監視が不要なら監視を解除する }); }; // インスタンス化 const observer = new IntersectionObserver(callback, options); // id="main_content" 以下の画像全て const imgs = document.getElementById("main_content").getElementsByTagName("img"); // 監視する要素を登録する for(let i = 0, n = imgs.length; i < n; i++){ observer.observe(imgs[i]); }

パララックスを行うコードサンプル

これまでのコードを参考に以下のようにすれば非常に簡単なコードでパララックスが実装できます。

(function(){ "use strict"; const options = { root : null, rootMargin : "0px", threshold : 0.35, }; const callback = (entries, observer) => { entries.forEach(entry => { if(!entry.isIntersecting) return; entry.target.style.opacity = ""; setTimeout(() => { entry.target.style.transition = ""; }, 1200); observer.unobserve(entry.target); }); }; const observer = new IntersectionObserver(callback, options); window.addEventListener("DOMContentLoaded", () => { const targets = document.querySelector("#contents").children; for(let i = 0, n = targets.length; i < n; i++){ observer.observe(targets[i]); targets[i].style.opacity = 0; targets[i].style.transition = "1.2s"; } }, false); })();

このコードでは #contents 要素直下の全ての要素を一旦、透明 (opacity=0) にしておいて Intersection Observer によってビューポートに 35% が表示された段階で元の透明度に戻すようにしています。

透明度以外にも位置も動かしたい場合は以下のように拡張できます。(荒いやり方ですが)

(function(){ "use strict"; const options = { root : null, rootMargin : "0px", threshold : 0.25, }; const slideIn = function($target){ if($target.dataset.parallax === "off"){ // Run effects setTimeout(()=>{ $target.style.position = ""; $target.style.left = ""; }, 800); $target.style.left = "0"; delete $target.dataset.parallax; } else{ // Prepare effects console.log(window.getComputedStyle($target).position); if(window.getComputedStyle($target).position === "static"){ $target.dataset.parallax = "off"; $target.style.position = "relative"; $target.style.left = "1.5em"; } } }; const slideUp = function($target){ if($target.dataset.parallax === "off"){ // Run effects setTimeout(()=>{ $target.style.position = ""; $target.style.top = ""; }, 800); $target.style.top = "0"; delete $target.dataset.parallax; } else{ // Prepare effects if(window.getComputedStyle($target).position === "static"){ $target.dataset.parallax = "off"; $target.style.position = "relative"; $target.style.top = "1.5em"; } } }; const effects = { h2 : slideIn, h3 : slideIn, h4 : slideIn, h5 : slideIn, h6 : slideIn, p : slideUp, }; const callback = (entries, observer) => { let effect; entries.forEach(entry => { if(!entry.isIntersecting) return; entry.target.style.opacity = ""; setTimeout(() => { entry.target.style.transition = ""; }, 800); const tag = entry.target.tagName; if(tag && (effect = effects[tag.toLowerCase()])){ effect(entry.target); } observer.unobserve(entry.target); }); }; const observer = new IntersectionObserver(callback, options); const targets = document.querySelector(".post-content").children; for(let i = 0, n = targets.length, effect; i < n; i++){ observer.observe(targets[i]); targets[i].style.opacity = 0; targets[i].style.transition = "0.8s"; const tag = targets[i].tagName; if(tag && (effect = effects[tag.toLowerCase()])){ console.log(tag, effect); effect(targets[i]); } } })();

ポリフィル

Intersection Observer API に対応していないブラウザでも使用できるようにするポリフィルが提供されていますので、モダンブラウザ以外やスマホでも使えるようにしたい場合は使用可否を判断して以下を読み込んで下さい。

・GitHub - W3C - IntersectionObserver / polyfill

まとめ

以上、Intersection Observer API を用いたパララックス効果の作り方でした。

個人的にはあまり好きではないですが、効果的にユーザ体験を向上させるデザインと併せて導入できればユーザの興味をより惹くコンテンツが作れるでしょう。
ぜひ、優秀なデザイナーと一緒にコンテンツを構築して下さい!