素のJavaScriptでDOM要素をフェードイン/アウトさせる

# はじめに
この記事ではjQueryのメソッドの一つである `fadeIn()` と `fadeOut()` をVanillaJS(素のJS)で実現するソリューションを紹介します。

## 背景
- 筆者はここ数年jQueryからVanillaJSへの移行をおこなっている。
- 筆者はjQueryのメソッドである `fadeIn()` と `fadeOut()` を多用している。

## 課題
- jQueryの `fadeIn()` と `fadeOut()` に該当するメソッドがVanillaJSには存在しない。
- 移行にあたり使いまわせるコードを残しておきたい。

# 仕様
できるだけ　jQueryの `fadeIn()` & `fadeOut()` に近い仕様で実装したいと考えた。

1. フェードイン/アウトをさせるDOM要素を指定する。
1. フェードイン/アウトの時間をミリ秒で指定する。
1. 非同期処理を実現する。
    - 例えば、フェードイン/アウトのアニメーションの終了を待ってから次の処理を行うことができる。
    - jQueryではコールバック関数を用いていたが、ここでは非同期処理（Promise or async & awaitを採用した。）

## fadeIn()
```
fadeIn(element, duration?, display?);
```

| 引数 | 型 | 説明 |
| --- | -- | - |
| `element`　　 | `HTMLElement` | フェードインしたいDOM要素を指定 |
| `duration` | `number`      | アニメーション時間をミリ秒で指定 |
| `display`  | `String`      | 表示時の`display`プロパティを指定[^1]|

[^1]:CSSのプロパティである `display`については公式のドキュメントを参考しにしてください。[https://developer.mozilla.org/ja/docs/Web/CSS/display](https://developer.mozilla.org/ja/docs/Web/CSS/display)



## fadeOut()
```
fadeOut(element, duration?);
```
| 引数 | 型 | 説明 |
| --- | -- | - |
| `element`　　 | `HTMLElement` | フェードアウトしたいDOM要素を指定 |
| `duration` | `number`      | アニメーション時間をミリ秒で指定 |


# コード
```js
const delay = 40; // fps=25

function fadeIn() {
    return new Promise(async (resolve, reject) => {
        if (arguments.length > 0) {
            const elem = arguments[0];
            if (elem.style.display === 'none' || elem.style.display === '') {
                const duration = arguments.length === 2 ? arguments[1] : 500;
                const display = arguments.length === 3 ? arguments[2] : 'block';
                elem.style.display = display;

                const coef = 1 / (duration / delay);
                elem.style.opacity = 0;
                var opacity = Number(elem.style.opacity);
                var animation = setInterval(() => {
                    opacity += coef;
                    elem.style.opacity = opacity;
                    if (opacity >= 1) {
                        clearInterval(animation);
                        resolve(true);
                    }
                }, delay);
            }
        }
    });
}

function fadeOut() {
    return new Promise(async (resolve, reject) => {
        if (arguments.length > 0) {
            const elem = arguments[0];
            if (elem.style.display !== 'none') {
                const duration = arguments.length === 2 ? arguments[1] : 500;

                const coef = 1 / (duration / delay);
                elem.style.opacity = 1.0;
                var opacity = Number(elem.style.opacity);
                var animation = setInterval(() => {
                    opacity -= coef;
                    elem.style.opacity = opacity;
                    if (opacity <= 0) {
                        elem.style.display = 'none';
                        clearInterval(animation);
                        resolve(true);
                    }
                }, delay);
            }
        }
    });
}
```

## 非同期処理として実装する
```js
return new Promise(async (resolve, reject) => {
    /* ここに処理を書く */
});
```

## 引数のチェック
```js
// 引数は必ず一つ以上
if (arguments.length > 0) {
    // 第1引数はDOM要素
    const elem = arguments[0];
    // 第2引数はデュレーション時間
    const duration = arguments.length === 2 ? arguments[1] : 500;
    // 第3引数はdisplayプロパティ (フェードインのみ)
    const display = arguments.length === 3 ? arguments[2] : 'block';
}
```

## フェード処理
### フェードイン
```js
// 現在表示されていないことを確認
if (elem.style.display === 'none' || elem.style.display === '') {

    // 表示モードにしたうえで、不透明度を0にする。
    elem.style.display = display;
    elem.style.opacity = 0;

    // 不透明度
    // elem.style.opacityはString型なので、数値として扱うためにnumber型にキャストする必要がある。
    // 初期値は0
    var opacity = Number(elem.style.opacity);

    // setIntervalが実行されるたびに不透明度に加算する定数
    const coef = 1 / (duration / delay);

    // setIntervalによって徐々に不透明度を1に近づける。
    var animation = setInterval(() => {
        opacity += coef;
        elem.style.opacity = opacity;

        // 終了条件 : 不透明度が1以上
        if (opacity >= 1) {
            clearInterval(animation);
            resolve(true);
        }
    }, delay);
}
```

### フェードアウト
```js
// 現在表示されていることを確認
if (elem.style.display !== 'none') {

    // 不透明度を1にする
    elem.style.opacity = 1.0;
    var opacity = Number(elem.style.opacity);

    // setIntervalが実行されるたびに不透明度に減算する定数
    const coef = 1 / (duration / delay);

    // setIntervalによって徐々に不透明度を0に近づける。
    var animation = setInterval(() => {
        opacity -= coef;
        elem.style.opacity = opacity;

        // 終了条件 : 不透明度が0以下
        if (opacity <= 0) {
            elem.style.display = 'none';
            clearInterval(animation);
            resolve(true);
        }
    }, delay);
}
```

# 使い方
```js
const elem = document.querySelector("#elem");
const duration = 600;
fadeIn(elem, duration, 'block');
fadeOut(elem, duration);
console.log("done");
```

### 非同期処理 (promise)
```js
const elem = document.querySelector("#elem");
const duration = 600;
fadeIn(elem, duration, 'block')
    .then(() => {
        fadeOut(elem, duration)
            .then(() => {
                console.log("done");
            });      
    });
```

### 非同期処理 (async/await)
```js
const elem = document.querySelector("#elem");
const duration = 600;
await fadeIn(elem, duration, 'block');
await fadeOut(elem, duration);
console.log("done");
```

# ライブラリ化
以上の処理をライブラリ化しました。（TypeScript対応済み）
- リポジトリ : https://github.com/tomoya-onuki/fadeIOnu.js

### インストール
- JavaScript : [ここ](https://github.com/tomoya-onuki/fadeIOnu.js/blob/master/fadeIOnu.js)から `fadeIOnu.js` をダウンロード 

- TypeScript
  ```sh
  npm i https://github.com/tomoya-onuki/fadeIOnu.js/
  ```