ターミナルに雪を降らせる

[LIFULLその2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/lifull2)の13日目です。
去年も13日目だった気がする。しかも今年は13日の金曜日。

すっかり冬になりました。
冬といえば雪
どうせなら雪を見ながら作業したいなーなんて思いました。
というわけで

# 雪を降らせる（ターミナルに)
## 使うもの
- Hyper
    - ターミナルです、JSでカスタマイズできます
    - https://hyper.is/

## 雪を降らせるための作戦
HyperはUIコンポーネントが以下のようになっています。
[公式より、コンポーネントについて](https://hyper.is/#decorating-components)
Termのコンポーネントに雪を降らせるための要素を重ねて表示させることにします

## とりあえず、ローカルのプラグインを読み込ませる
Macの場合は、`~/.hyper_plugins/local/`以下にプラグインを自作して、読み込ませます

```bash
mkdir ~/.hyper_plugins/local/snow
touch ~/.hyper_plugins/local/snow/index.js
```

設定ファイルを編集して、ローカルのプラグインを読み込むようにする

```js:~/.hyper.js
localPlugins: [
  "snow"
],
```

ローカルプラグインの準備ができたので、あとは雪を降らせるためにいじるだけです

## まずは雪を降らせる場所作り
作成した`index.js`で雪を降らせる場所(div)を追加します

```js:index.js
exports.decorateTerm = (Term, { React, notify }) => {
  return class extends React.Component {
    render () {
      return React.createElement('div',{style: { width: '100%', height: '100%', position: 'relative' }}, [
        React.createElement('div', { class: 'snowfall' }), //雪を降らせるためのdiv要素
        React.createElement(Term, this.props), //元々のターミナル
      ]);
    }
  }
}
```

細かい説明は省きますが、元々のTermコンポーネントに雪を表示するための要素を追加しています。
詳細については、[このあたり](https://hyper.is/#decorating-components)が参考になります。

`index.js`を変更したら、`⇧⌘R`でリロードしてください。

## 実際に降らせる雪づくり
雪を降らせる場所に、雪の要素を追加します。

```js:index.js
exports.decorateTerm = (Term, { React, notify }) => {
  return class extends React.Component {
    render () {
      return React.createElement('div',{style: { width: '100%', height: '100%', position: 'relative' }}, [
        React.createElement('div', { class: 'snowfall' },
          React.createElement('div', { class: 'snow' }, '雪')
        ),
        React.createElement(Term, this.props),
      ]);
    }
  }
}
```
誰がどう見ても、雪が追加されました。
<img width="827" alt="スクリーンショット 2019-12-12 2.25.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126870/6b5bfbf3-7a2e-d78b-6c7b-3d87537a53cb.png">

## 雪を降らせる
スタイルを追加して、雪を降らせるアニメーションやら表示位置やらを調整します。
特に凝ったことはしないので、アニメーションもcssで完結させます。

```js:index.js
exports.decorateTerm = (Term, { React, notify }) => {
  return class extends React.Component {
    render () {
      return React.createElement('div',{style: { width: '100%', height: '100%', position: 'relative' }}, [
        React.createElement('div', { class: 'snowfall' },
          React.createElement('div', { class: 'snow' }, '雪')
        ),
        React.createElement('style', {}, `
          .snowfall {
            position: absolute;
            height: 100%;
            width: 100%;
            pointer-events: none;
            z-index: 100;
          }

          div.snow {
            animation: snowfall 3s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 10%;
          }

          @keyframes snowfall {
            0% {
              transform: translateY(0) rotateY(0);
            }
            50% {
              transform: translateY(50%) rotateY(180deg);
            }
            100% {
              transform: translateY(100%) rotateY(360deg);
              opacity: 0;
            }
          }
        `),
        React.createElement(Term, this.props),
      ]);
    }
  }
}
```

`<style>`の要素を直接追加して、cssを当てるようにしています。
単純なのでとりあえずこれで良いかなと

自分なりのポイントを解説すると、`z-index: 100;`の部分がポイントで、
開発者コンソールで確認すると、元々のTerm要素に`z-index`が設定されているようで、
問答無用で最前面に出すために100とかの数値を設定してます。
最前面に出すためなので、値は100でも1000でも10000でも問題ないです。

とりあえずここまでで雪が降り始めました。
![snowfall#1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126870/25dab55f-0ffe-4a73-29e4-e4e94f90dd1b.gif)

## 雪をふやす
あとは適当に雪を追加して、アニメーションの時間などをいじって雪っぽさを演出します。

<details><summary>長いので省略</summary><div>

```js:index.js
exports.decorateTerm = (Term, { React, notify }) => {
  return class extends React.Component {
    render () {
      return React.createElement('div',{style: { width: '100%', height: '100%', position: 'relative' }}, [
        React.createElement('div', { class: 'snowfall' },
          React.createElement('div', { class: 'snow'}, '雪'),
          React.createElement('div', { class: 'snow2'}, '雪'),
          React.createElement('div', { class: 'snow3'}, '雪'),
          React.createElement('div', { class: 'snow4'}, '雪'),
          React.createElement('div', { class: 'snow5'}, '雪'),
          React.createElement('div', { class: 'snow6'}, '雪'),
          React.createElement('div', { class: 'snow7'}, '雪'),
          React.createElement('div', { class: 'snow8'}, '雪'),
          React.createElement('div', { class: 'snow9'}, '雪'),
        ),
        React.createElement('style', {}, `
          .snowfall {
            position: absolute;
            height: 100%;
            width: 100%;
            pointer-events: none;
            z-index: 100;
          }

          div.snow {
            animation: snowfall 3s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 10%;
          }

          div.snow2 {
            animation: snowfall 8s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 20%;
          }

          div.snow3 {
            animation: snowfall 2s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 30%;
          }

          div.snow4 {
            animation: snowfall 5s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 40%;
          }

          div.snow5 {
            animation: snowfall 10s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 50%;
          }

          div.snow6 {
            animation: snowfall 7s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 60%;
          }

          div.snow7 {
            animation: snowfall 9s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 70%;
          }

          div.snow8 {
            animation: snowfall 4s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 80%;
          }

          div.snow9 {
            animation: snowfall 2s linear 0s infinite normal;
            position: absolute;
            height: 100%;
            left: 90%;
          }

          @keyframes snowfall {
            0% {
              transform: translateY(0) rotateY(0);
            }
            50% {
              transform: translateY(50%) rotateY(180deg);
            }
            100% {
              transform: translateY(100%) rotateY(360deg);
              opacity: 0;
            }
          }
        `),
        React.createElement(Term, this.props),
      ]);
    }
  }
}
```
</div></details>

## 完成したやつ && 作業風景
![snowfall#2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126870/cf376824-0404-8e4d-7fe6-e9ed19a4f165.gif)

# まとめ
ターミナルに雪を降らせたけど、予想以上に鬱陶しいので、
雪が見たければ本物の雪を見にいこう

# 参考
- [Hyperターミナルのプラグインを作ってみよう!](https://qiita.com/vimyum/items/569280135ee5d4da33bd)
    - 自作プラグインについて参考にさせていただきました
- [そろそろ雪が降る寒さになってきたので雪を降らせるエフェクト3選＋おまけ](https://qiita.com/masanarih0ri/items/efa4b09e1bc6b0b05983)
    - 雪のアニメーションについて参考にさせていただきました