【javascript】observerで絵本の表紙をリッチにしてみた

# はじめに
前回、フィルターについてcodepen challengeを行った記事を書いたのですが、今回は新しくスクロールについてcodepen challengeに調整してみたので、そちらを記事にしていきます。
詳しくは前回の記事を参照してください。[被写界深度再現してみた](https://qiita.com/hayuse/items/2beb7db0bd2a63741f94)

## 今回作ったもの
![コンポ 1_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3640746/a33cdd10-e4be-e9e2-25e4-c32b8e6dba86.gif)


今回作ったものは自分のお気に入りの絵本をスクロールアニメーションで紹介していくものです。
**0.25倍でみて** もらえるとちゃんと見えるかと思います。。。（レスポンシブ対応できてない。。。）
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="RwmJbeq" data-pen-title="Scroll story book" data-user="dtweqbgd-the-vuer" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/dtweqbgd-the-vuer/pen/RwmJbeq">
  Scroll story book</a> by Hayuse (<a href="https://codepen.io/dtweqbgd-the-vuer">@dtweqbgd-the-vuer</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

今日はこのプロジェクトをどのように作ったのかを説明していけたらなと思います。（画像も含めて）

ロードマップはこんな感じ。

![ロードマップ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3640746/09955a32-8660-7919-c301-bc26c90baef8.png)

この記事ではコーディングの箇所について詳しく解説していきます。
**スクロールによって特定の要素が映った時点で、実行させる**ってよくやりますよね。
今回は、jsのAPIの***IntersectionObserver***で簡単にそれが実装できたのでそれを紹介してます。

### IntersectionObserverとは？
今回scrollの発火に使ったのはこの[IntersectionObeserver](https://developer.mozilla.org/ja/docs/Web/API/IntersectionObserver)です。
こいつなんと、**画面内に特定のエレメントがあるかどうかを監視**してくれるAPIなんです。

具体的な使い方は
```typescript

const scrollIntersect = () => {
    console.log("画面に映った！！");
}

// IntersectionObserverの設定
const options = {
    root: null, // root nullで表示画面全体に交錯したか（映ったか）を設定。（elementでもできるぽい）
    rootMargin: '0px', // elementの判定の大きさ（大きくしたら映った判定がガバガバになる）
    threshold: 0.1 // 何%映ったら、発火させるか。0.1 = 10%
};

// IntersectionObserverを設定(上記のoptionの設定でscrollIntersect関数を発火させる)
const scrollObserver = new IntersectionObserver(scrollIntersect, options);

// triggerクラスを発火するトリガーとしてobserver開始の開始。
const pageTriggers = document.querySelectorAll('.trigger');
pageTriggers.forEach(trigger => scrollObserver.observe(trigger));
```
こんな感じ。
コードの説明としては、`trigger`のクラス名のついたオブジェクトが、画面に映った場合、`scrollIntersect`関数を発火させています。

## 実装手順

### 概要
今回特に行いたかった実装は、

**1.特定のエレメントが見えたら、ページのスクロールを自動調整**
**2.自動調整が終わったらアニメーションの開始**

この二つを実装するために、今回はスクロールを自動調整するobserverとアニメーションを実行するobserverの２つを使って実装しています。
イメージとしてはこんな感じ
![スクロールロジック.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3640746/85cda905-8d7d-341d-0a82-f0a643d6397e.png)

### 手順１：枠組み作成と発火させるエレメントの配置

用意した画像を調整して配置するのと、発火のトリガーとなる要素を自分の好きなところに表示していきます。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="zYQQoPo" data-pen-title="Untitled" data-user="dtweqbgd-the-vuer" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/dtweqbgd-the-vuer/pen/zYQQoPo">
  Untitled</a> by Hayuse (<a href="https://codepen.io/dtweqbgd-the-vuer">@dtweqbgd-the-vuer</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

赤いのがスクロール調整関数を発火させる用のエレメント
黄色いのがアニメーション関数を発火させる用のエレメント

### 手順２：jsでスクロール用のobserverの配置
ここで出てきます`IntersectionObeserver`
赤いエレメントをトリガーにしてobserverを開始していきます。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="ZENNLze" data-pen-title="Untitled" data-user="dtweqbgd-the-vuer" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/dtweqbgd-the-vuer/pen/ZENNLze">
  Untitled</a> by Hayuse (<a href="https://codepen.io/dtweqbgd-the-vuer">@dtweqbgd-the-vuer</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

この関数で特定のクラスを`querySelectorAll`したNodeListを渡します。
そうすると要素が映った瞬間にscrollFunctionの関数が発火します。
```typescript
const observeStart = (triggerElements: NodeListOf<HTMLElement>) => {
    // IntersectionObserverの設定
    const options = {
        root: null, // root nullで表示画面全体に交錯したか（映ったか）を設定。（elementでもできるぽい）
        rootMargin: '0px', // elementの判定の大きさ（大きくしたら映った判定がガバガバになる）
        threshold: 0.1 // 何%映ったら、発火させるか。0.1 = 10%
    };

    // IntersectionObserverを設定(上記のoptionの設定でscrollFunction関数を発火させる)
    const scrollObserver = new IntersectionObserver(scrollFunction, options);

    // triggerクラスを発火するトリガーとしてobserver開始の開始。
    for (const trigger of triggerElements) {
        scrollObserver.observe(trigger)
    }
}
```


scrollFunction関数
```typescript
const scrollFunction = (entries: IntersectionObserverEntry[], _observer: IntersectionObserver) => {
    for (const entry of entries) {
        const triggerElement = entry.target;
        if (entry.isIntersecting) { // トリガーとなる要素が画面に映ったらここがtrueになる。
            triggerElement.innerHTML = "<span>画面に映った！！→</span>";
            console.log("画面に映った！！");
        } else {
            triggerElement.innerHTML = "<span>画面に映ってない！！→</span>";
            console.log("画面に映ってない！！");
        }
    }
}
```
関数の引数に自動的に要素のデータやその他諸々のデータが入ってくるので、実行したい条件をそれらから使うといいかもしれません。
ちなみに映ってるか映ってないかをtrue,falseで返してくれるのはisIntersectingみたいです。

### 手順３：自動でスクロール量を調整

要素が映ったタイミングで自動で１ページ分スクロールするようにscrollFunctionに手を加えます。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="zYQQNzW" data-pen-title="observer_step2" data-user="dtweqbgd-the-vuer" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/dtweqbgd-the-vuer/pen/zYQQNzW">
  observer_step2</a> by Hayuse (<a href="https://codepen.io/dtweqbgd-the-vuer">@dtweqbgd-the-vuer</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

```typescript
const scrollFunction = (entries: IntersectionObserverEntry[], _observer: IntersectionObserver) => {
    for (const entry of entries) {
        const triggerElement = entry.target;
        if (entry.isIntersecting) { // トリガーとなる要素が画面に映ったらここがtrueになる。
            triggerElement.innerHTML = "<span>画面に映った！！→</span>";
            console.log("画面に映った！！");
            // １ページの要素を取得
            const pageElm = triggerElement.closest<HTMLElement>(".frame-page");
            if (pageElm === null) return;
            // 要素が左から何pxのとこにあるのか取得
            const pageX = pageElm.offsetLeft;
            console.log(pageElm.getBoundingClientRect());
            // スクロールさせる
            window.scrollTo({
                top: window.scrollY,  
                left: pageX,              
                behavior: 'smooth'    
              });

        } else {
            triggerElement.innerHTML = "<span>画面に映ってない！！→</span>";
            console.log("画面に映ってない！！");
        }
    }
}
```

### 手順４:アニメーションの発火

スクロールの発火と同じようにして今度は、黄色のトリガーが映ったタイミングでアニメーションを発火させます。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="gONONyd" data-pen-title="observer_step3" data-user="dtweqbgd-the-vuer" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/dtweqbgd-the-vuer/pen/gONONyd">
  observer_step3</a> by Hayuse (<a href="https://codepen.io/dtweqbgd-the-vuer">@dtweqbgd-the-vuer</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

```typescript
const animtionFunction = (entries: IntersectionObserverEntry[], _observer: IntersectionObserver) => {
    for (const entry of entries) {
        const triggerElement = entry.target;
        if (entry.isIntersecting) { // トリガーとなる要素が画面に映ったらここがtrueになる。
            console.log("アニメーションスタート");
            const currentPage = triggerElement.closest('.frame-page');
            if (currentPage === null) return;
            currentPage.classList.add('is-active')
        } else {
            console.log("アニメーションストップ");
            const currentPage = triggerElement.closest('.frame-page');
            if (currentPage === null) return;
            currentPage.classList.remove('is-active')
        }
    }
}
```

ここはほぼ同じロジックですね。前回のコードを少し変えて、observerの関数にトリガーと発火させたい関数を渡すようにしています。

```typescript
type observeFunction = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void;

const observeStart = (triggerElements: NodeListOf<HTMLElement>, triggerFunction: observeFunction) => { // triggerFunctionに発火させたい関数を渡す。
    // IntersectionObserverの設定
    const options = {
        root: null, // root nullで表示画面全体に交錯したか（映ったか）を設定。（elementでもできるぽい）
        rootMargin: '0px', // elementの判定の大きさ（大きくしたら映った判定がガバガバになる）
        threshold: 0.1 // 何%映ったら、発火させるか。0.1 = 10%
    };

    // IntersectionObserverを設定(上記のoptionの設定でscrollIntersect関数を発火させる)
    const scrollObserver = new IntersectionObserver(triggerFunction, options);

    // triggerクラスを発火するトリガーとしてobserver開始の開始。
    for (const trigger of triggerElements) {
        scrollObserver.observe(trigger)
    }
}
```

あとはロードのタイミングで発火させるトリガーのNodeListと関数を渡してあげればスッキリコードの完成！

```typescript
document.addEventListener("DOMContentLoaded", () => {
    const scrollTrigger = document.querySelectorAll<HTMLElement>(".scroll-trigger");
    observeStart(scrollTrigger, scrollFunction);
    const animationTrigger = document.querySelectorAll<HTMLElement>(".animation-trigger");
    observeStart(animationTrigger, animtionFunction);
});
```

### 完成に向けて
あとはHTMLに切りはりした画像を別々に乗っけたり,cssを動くように書き直せば動くはずです。
今回使用した画像は全部AIに生成してもらったので別記事でAI生成のさせ方と、コツなどをまとめていけたらなと思います。