フェードインとか。

おまけもあるよ💖

◆おことわり

古いブラウザは知らない。帰れ。

…いや、帰れよ。何だよその目は。

…かーえーれっ!かーえーれっ!

ウェヒヒヒwww

あ!泣いてやんの!だっせ!だっせ!

◆Intersection Observer

まえに、画像の遅延読み込みについて書いたんです。そこで登場したIntersection Observerくんが活躍します。応援してあげてください。

▼Intersection Observerとは!

説明しよう!

これはな、IntersectionをObserveするものだよ。

Intersection Observer API | MDN

和訳すると交差監視API。すなわち、ある要素Aがある要素Bと交差した時にコールバックを発火できる。

要素って言ってんのはpタグでもいいしdivタグでもいいし。重要なのは、ビューポートと要素の交差を監視できるということ。今見えている画面の範囲に要素が重なったタイミングでメソッドを蹴ることができるんすね。

勝ったな。風呂食ってメシ入ってくるわ。

◆JavaScript

すること。

  • コールバック関数を書く。
  • 親要素を確定する。
  • 子要素を確定する
  • アタッチする。

▼コールバック関数を書く

コールバック関数って何ってお前…。刺されても文句は言えんぞ。
とりあえず…単にFunctionのことだと思っておけばいいです。交差したというイベントが発生した時に呼びたいイベントを書く。

じゃあまぁ、window.alertでいいじゃん。

const c = (entries) => window.alert('ちばけんま!')

書いてあることわかるだろうか。

Q:なにこれ?
A:cはfunctionを格納している定数です。functionの、アローな書き方です。

Q:entriesってなに?
AIntersectionObserverEntryのリストなんだけど、とりあえず使ってないよね。まずは交差した時に関数が発火されるかどうかを確認しようじゃん?
おまえ:うん。

▼親要素の特定と設定

AとBの交差をハンドリングしてぇワケですので、Aを指定してあげる必要ある。以下の定数を作ってみる。

const options = {
  root: null,
  rootMargin: '0px',
  threshold: 0,
};

Q:あれ、要素指定してる感じないけど。
A:「見えている範囲」を指定したいときはnullで良いんです。

ちゃんと指定したいときはrootってプロパティに、Elementを指定してあげれば良い。

Q:他のプロパティはなに?
A:知らんでもいいよ。

と言ってしまうのはアレなので簡単に説明すると

  • rootMargin:その要素のまわりに感知マージン(造語)を張れる。要素が見える前からイベント発火できる。
  • threshold:どれくらい要素が見えたらイベント発火するか。要素が見えたあとのタイミングでイベント発火できる。

ってことです。

▼子要素の特定

<div id="piyo">piyo</div>

こんな要素があった時に

const e = document.querySelector('#piyo');

こんな感じで書いてあげればよいですよね。

▼アタッチ

アタッチって呼び方が適切なのかわからんが。

「IntersectionObserver」というクラスを使って、どうこうする。

const c = (entries) => window.alert('ちばけんま!')

const options = {
  root: null,
  rootMargin: '0px',
  threshold: 0,
};

const e = document.querySelector('#piyo');

const observer = new IntersectionObserver(c, options);
observer.observe(e);

これでOK。要素が画面に表示された時にイベントが発火されまする。

◆お前「バグってんじゃん」

えーうそー。どこどこー。

▼最初に発火されちゃってる

penを実行した瞬間にwindow.alert動いちゃってんよ。なんぞこれぇ!

あと、なんか交差外れたときも発火されちゃってる。なぁんぞぉぉおおおお!!😭

というときは、isIntersectingを参照しなっせ。「交差してるなう」を表現してくれます。

cの中身だけちょっと変えました。見てみろ。

何をしたのかって、filterでisIntersectingがtrueであるIntersectionObserverEntryだけフィルタしたわけです。

補足:forEach使ってるけど、可読性がすごく上がるってときだけ使うにとどめろ。あんまり使わないほうが良いよ。読みにくいし理解しづらいし副作用があるかどうか不明瞭だから。メリットないっす。素直にforof使え。

▼画像が絡む場合

この例はdivだし大丈夫なんだけど、例えば画像を縦に並べた時に変な動きするかもね。

というのも、読み込まれるまでheightが不明であるimgタグは、初期表示時に高さ0pxである。だもんで、その一瞬で交差した判定になるよね。

その画像の上にある要素だとか画像自体にheightを設定して差し上げろ。

▼交差するたびに何回もイベント発火するんですけど

しゃあないやろ。交差を検知しているだけなのだから。ガタガタぬかすな。

一回だけにしたいんなら自分で表示を管理するための変数持つしかない。

あるいはobserverのunobserveメソッドを叩いて監視をやめる。

◆TypeScriptしたい

いまどきJavaScriptなんてマゾ言語は使っていられるわけないから、TypeScriptで書くことになるよね

以下、コードはts。SCSSで書きたい気持ちもあるけど取り敢えずプレーンなCSSでやっておく。

◆ねっとり表示フェードイン作ってみる

そんな難しい話でもない。

  • フェードインのアニメーションつくる。
  • アニメーションついたCSSクラスつくる。
  • コールバックでそのクラスをaddする。

▼アニメーション

例えばfadeinっていう名前のCSS animationつくる。

@keyframes fadein {
  from { opacity: 0; }
  to { opacity: 1; }
}

ご覧の通り、opacity(不透明度)がゼロからイチになる。

▼つくったアニメーションを使うクラスつくる

例えばウンコっていう名前のCSSクラスつくって、さっきのアニメーションを参照する。

.unko {
  animation: 1s fadein;
}

▼コールバックでそのCSSをaddする

const c = (entries: IntersectionObserverEntry[]) =>
  entries
    .filter((e) => e.isIntersecting)
    .forEach((e) => e.target.classList.add('unko'));

ウンコをaddしてあげればホラこの通り。

※Rerunってボタンでリセットできます。

シュワ~と表示されたでしょう。ステキ!

あとはまぁスキにしろって感じだけど、例えばtransformrotateとか使えばクッソくだらないアニメーションさせることもできるよね。

あ、rotateってSafariで動かないの?そう…。

(Firefoxはイケた。)

▼最初にピョっとなってからシュワ~となるんですけど

そうっすね。最初に元の状態がピョっと表示されてからanimationが開始されるからそうなる。animationじゃのうて、いつものtransitionでやればいいよね。

例えばopacity:1のCSSクラスをaddするとか、逆にopacity:0のクラスを最初からあてておいてremoveするとか。removeのパターンは以下参照。

◆Vue

▼注意点

Vueって非同期で要素が描画されるから、対象の要素が描画されるまえにお前の書いた処理が通ってしまう場合があるよね。created(setup)じゃなくてmountedに書くとかしないといけないんじゃない。なんか動かねぇなと思ったら描画と処理の順序を洗ってみること。

▼ディレクティブ

いちいちオブザーバーをシコシコすんのめんどくさくねぇ?っていうので、カスタムディレクティブ作っちゃえばいいじゃん。

というので、Vuetifyにはすでにv-intersectっていうの用意されてます。ワァオ!onceのオプションもあるから一度だけ呼べて便利!

しかしVuetify使ってない人もおるだろうし、ディレクティブも手作りしちまおうそうしよう。でもアレコレ考えるのダルいからVuetifyのソースを丸パクリしようそうしよう。

えーとどこだ。

あ り ま し た

https://github.com/vuetifyjs/vuetify/blob/21331625b9707f19cc2004f2e0957b288a2ccad3/packages/vuetify/src/directives/intersect/index.ts

あとはまぁmain.tsでコイツをVue.directiveに…あれ。「_observe」ってなに。えっなんかglobalにdeclareされてる。やだっこわいっ。もう帰りゅ!

別に_observeをglobal declareで混ぜてもいいし、いらねぇとこ削ったディレクティブにしてもいいとは思うわ。ディレクティブに固めたんなら例えばこういう書き方できる。

<div v-intersect="(e) => e[0].target.classList.add('fade')"
  >piyo<
/div>

Elementに直接記述できちゃうので、idとか考える必要なくて便利だ。

けどまだなんか冗長というかコレジャナイ感あるっていうか。isIntersectingであるtargetを直で触れるだけで良いわけですから。っつかtargetってundefinedになり得るし。

作るか…

◆Vue用Directive作ったよ

Vue3を使ってる人は、insertedとunbindの名前がBreaking Changeなので下記参照して書き換えろ。

Custom Directives | 3.x Syntax

▼使い方

codepenが言う事きいてくれないからデモはキャンセルだ。

・意図

  • intersectingしたときだけコールバック発火。
  • IntersectionObserverEntryのArrayじゃのうて対象要素が取れてくる。
  • 「一度だけ発火」に対応。

・登録

main.tsでimportして登録。ファイルの置き場は各自いい感じにしろ。俺はsrc/directiveに入れておく。

import intersecting from '@/directive/intersecting';
Vue.directive('intersecting', intersecting);

・使う①

    <div id="app">
      <div class="hoge">hoge</div>
      <div v-intersecting.once="(e) => e.classList.remove('invisible')" class="fuga invisible">
        piyo
      </div>
    </div>

あぁースッキリ!!😡

コレだよコレ!!😡

・もぢふぁい

Vuetifyのコードを丸パクリしたためにmodifier持ってます。.onceを使えば一度だけ発火できまする。.quietを使えば、初期表示で表示範囲に入っちゃってる要素のイベントを発火しないで済みます。一回範囲から外してもっかい入れると発火できます。

Vuetifyの方のquietディレクティブがイマイチ何をしたいのかわからんかったからそういうハナシなんじゃねぇかと想定してみた。

◆余談

IntersectionObserver V2ってのもある。なんか気持ちいいとの噂。

◆結論

後は好きにしてください。あと、動き変とか言ってること変とかあったらコメント欄に書いていけ。こっそり直す。