マジミラ2020プロコンで使用したものまとめ3（文字の上昇と削除）

#概要
[相生葵の Advent Calendar 2020](https://adventar.org/calendars/5480) 9日目の記事です。

[初音ミク「マジカルミライ 2020」プログラミング・コンテスト](https://magicalmirai.com/2020/procon/)に参加したので使用したもののまとめ記事です。
再生は[こちら](https://aoaoaoaoaoaoaoi.github.io/magicalmirai-procon-2020-3/)でできます。
コードは[こちら](https://github.com/aoaoaoaoaoaoaoi/magicalmirai-procon-2020-3)にあります。

#目次
1. 作成したかった動き
2. 構造
3. 文字が上に消えていく動き

##1. 作成したかった動き
「文字を上昇させながら、ふわっと消えていく」
＋ 画面外に出たdiv要素は削除する

これを要素に分けると、以下のようになります。

* 文字が上に上昇する
* だんだん文字が薄くなる
* 画面外に出たdiv要素は削除する

##2. 実現方法
###文字が上に上昇する
cssアニメーションでしようと思いましたが、「画面外に出たdiv要素を削除」するためにdiv要素の高さ調整が必要だったのでjsの```interval```で実装しました。

###だんだん文字が薄くなる
cssアニメーションで作成しました。

###画面外に出たdiv要素は削除する
単純に消してしまうと、次のフレーズの歌詞が不自然に上に上がるので、上に上げるフレーズの高さを上に上昇させるとともにどんどん小さくしていくことにしました。

##1. 文字が上に消えていく動き

```javascript:index.js

//現在の再生位置
let currentPosition = 0;

//フレーズ用の配列
let phraseArray = [];

//1秒で50フレーム「setDeletePhrase」を呼ぶ登録をする関数
const update = () =>{
  let delay = 1000 / 50;
  timer = setInterval(function() {
    setDeletePhrase();
  }, delay)
};

//フレーズの削除
const setDeletePhrase = () => {
  let move = 8;
  let deleteArray = [];
  phraseArray.forEach((item, index) => {
    if(item.phrase.endTime < currentPosition + 100){
      let phraseDiv = item.phraseDiv;

      if(item.moveCount == 0){
        let selecter = "#" + item.id;
        let currentHeight = parseInt($(selecter).height());
        phraseDiv.classList.add('fadeLyric');
        item.setHeightChangeSize =  (currentHeight / 10);
      }

      let currentPos = parseInt(phraseDiv.style.top);
      if(item.moveCount < 10){
        currentPos -= item.getHeightChangeSize;
        let selecter = "#" + item.id;
        let currentHeight = parseInt($(selecter).height());
        let newHeight = currentHeight - item.getHeightChangeSize;
        if(newHeight < 0){
          newHeight = 0;
        }
        phraseDiv.style.height = newHeight + "px";
      }
      let newPos = (currentPos - move);
      phraseDiv.style.top = newPos + "px";
 
      item.incrementMoveCount();
      if(parseInt(phraseDiv.style.top) < -1000){
        deleteArray.push(index);
        phraseDiv.remove();
      }
    }
  });
  deleteArray.forEach((index) => {
    phraseArray.splice(index, 1);
  });
};

//フレーズクラス
class Phrase{
  constructor (phrase, id, phraseDiv)
  {
    this.phrase = phrase; //Iphrase
    this.id = id; //phraseのdiv要素のid
    this.phraseDiv = phraseDiv; //div要素
    this.moveCount = 0; //動いた回数
    this.heightChangeSize = 0; //一度の上昇で変更する縦サイズ
  }

  incrementMoveCount (){
    ++this.moveCount; 
  }

  get getHeightChangeSize() {
    return this.heightChangeSize;
  }
  set setHeightChangeSize(size) {
    this.heightChangeSize = size;
  }
}
```

```css:index.css
.fadeLyric{
    animation: fadeOut 200ms linear forwards;
}

  @keyframes fadeOut {
    0% {
        opacity: 1;
    }
    100% {
        opacity: 0;
    }
  }
```

###setDeletePhraseの呼び出し
以下のupdate関数を1度呼び出すことで、1秒に50回「setDeletePhrase」を定期的に呼びだすようになります。

```javascript

//1秒で50回「setDeletePhrase」を呼ぶ関数
const update = () =>{
  let delay = 1000 / 50;
  timer = setInterval(function() {
    setDeletePhrase();
  }, delay)
};
```
setIntervalは以下の形で使用します。

```javascript

setInterval(function() { /*定期的に呼び出す処理*/ }, /*呼び出す間隔（ms）*/)
```
今回は1秒に50回「setDeletePhrase」を実行したかったので、「1000ms（1秒）/ 50（回数） = 20ms」に1回「setDeletePhrase」を呼び出すようにしました。

###setDeletePhraseの処理

上からいきます。

```javascript

let move = 8; //1度に上に動かす値
let deleteArray = []; //配列から削除するphrase
phraseArray.forEach((item, index) => {
  //処理
}
```
moveの「8」は動きを見て決めた値です。
「deleteArray」は一定以上画面外に消えたフレーズを削除するために作成しています。
「phraseArray.forEach」で作成したフレーズを一つずつ上に移動させています。

```javascript

    if(item.phrase.endTime < currentPosition + 100){
```
```item.phrase.endTime```（終了時間）が ```currentPosition```（現在の再生位置）+100ms以下であればフレーズのdiv要素を上昇させる処理を行います。

```javascript

      if(item.moveCount == 0){
        let selecter = "#" + item.id;
        let currentHeight = parseInt($(selecter).height());
        phraseDiv.classList.add('fadeLyric');
        item.setHeightChangeSize =  (currentHeight / 10);
      }
```
moveCountが0ということはまだ一度も上に移動していないので、動かす前の準備をこのif文のかっこの中で行っています。

「fadeLyric」クラスをフレーズのdiv要素に追加して、文字が消えていく演出を付与しています。
phraseDivの高さを取得して、一度で変更する高さのサイズを計算しています。
10回で高さを0にしたかったので、10で割って一回の移動で変更する高さを出しています。

```javascript

      let currentPos = parseInt(phraseDiv.style.top);
      if(item.moveCount < 10){
        currentPos -= item.getHeightChangeSize;
        let selecter = "#" + item.id;
        let currentHeight = parseInt($(selecter).height());
        let newHeight = currentHeight - item.getHeightChangeSize;
        if(newHeight < 0){
          newHeight = 0;
        }
        phraseDiv.style.height = newHeight + "px";
      }
      let newPos = (currentPos - move);
      phraseDiv.style.top = newPos + "px";
```
移動した回数が10回未満であれば、まだ高さが0になり切っていないので高さを小さくします。
現在のtopの値から減らす高さ分の大きさを引いているのは、高さを小さくすると要素が下がってしまうためです。
高さを変えても上昇する大きさは変わらないようにするために、高さを削った分topを小さくしています。

```javascript

      if(parseInt(phraseDiv.style.top) < -1000){
        deleteArray.push(index);
        phraseDiv.remove();
      }
```
topが-1000以下になったらフレーズのdiv要素を削除しています。
このときheightは既に0pxになっているので、消えても表示されている歌詞は上に移動しません。