Vue.js を使用したドラムロールの実装

こんにちは、クラウドワークスの @t0yohei です。普段は Rails を書くことが多いですが、最近フロントエンドにも興味が出てきたので、 Vue.js とかも書いたりしています。
この記事は クラウドワークス Advent Calendar 2019 の8日目の記事です。

昨日は、@bugfire さんによる [多脚戦車に乗ろう](https://qiita.com/bugfire/items/0bc7f7874e0f9e346a91) でした。多脚戦車のカメラをスマホで自在に制御できるとか、すんごいですね...。かっこいい...。

それはさておき、自分の担当回では Vue.js でのドラムロールの実装サンプルについて書きたいと思います。

## きっかけ
お仕事で Vue.js を使ってドラムロールを実現したいということがあったのですが、 [Vue.js](https://jp.vuejs.org/v2/guide/transitioning-state.html#%E3%82%A6%E3%82%A9%E3%83%83%E3%83%81%E3%83%A3%E3%81%AB%E3%82%88%E3%82%8B%E7%8A%B6%E6%85%8B%E3%81%AE%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3) 公式のドラムロールの実装(数値変化のアニメーション)が、 [GreenSock](https://greensock.com/) を使っていたりでそのまま持って来れなかったので、独自で実装するということがありました。せっかく独自で実装したし世の中にサンプルがそんなになさそうなので、 その時の実装内容に少し手を加えて記事にしたいと思います。

## ドラムロールって？
[Wikipediaさんによると](https://en.wikipedia.org/wiki/Drum_roll) 元々の意味は、
> A drum roll (or roll for short) is a technique the percussionist employs to produce, on a percussion instrument, a sustained sound, "over the value of the written note."

翻訳してみると、「ドラムロール（または略してロール）とは、打楽器奏者が、「書かれた音符の価値を超えて」持続的な音を出すために使用する手法です。」
とのことで、ドラムをバチで連打して出すあの音のことを言っているようです。
そこからどう転じてか、 web アプリでのドラムロール風のカウントアップアニメーションなどで使用されるようになったみたいです(他にもスマホのドラムロール式セレクトボックスなんてのもあるとかないとか)。
この記事では、呼び方を簡略化するために、ドラムロール風のカウントアップアニメーションのことをドラムロールと呼ぶことにします。
要は↓のようなアニメーションのこと

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/5ca26cb8-4d5c-4aa5-6f93-93017b42fe08.gif" width="400">

クルクルって回りながら、数値が変化していくあれです。

## 実装
いいから早く実装見せろという声が聞こえてきそうなので、実装全体を先に載せてしまいます。
個人の趣味で TypeScript を使いながら書いています。

```html:Parent.vue
<template>
  <input v-model.number="inputCount" type="number">
  <NumberDrumRoll :count="inputCount" /> 
</template>

<script lang="ts">
import Vue from 'vue';
import NumberDrumRoll from './NumberDrumRoll.vue';

export default Vue.extend({
  components: {
    NumberDrumRoll,
  },

  data: function() {
    return {
      inputCount: 0;
    };
  },
});
</script>
```

```html:NumberDrumRoll.vue
<template>
  <span>{{ rollingCount }}</span>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  props: {
    count: {
      type: Number,
      default: () => null,
    },
  },

  data: function () {
    return {
      rollingCount: this.count,
      intervalId: 0,
    };
  },

  watch: {
    // 親から送られてくる数値の変更を監視して、変更があればドラムロールを実行
    count(newVal: number, oldVal: number) {
      this.performDrumRolling(newVal, oldVal);
    },
  },

  methods: {
    // 0.5秒間で数値のドラムロールを実行するメソッド
    performDrumRolling(newVal: number, oldVal: number): void {
      // ドラムローリング中に再度値の変更があった場合を考えて、予め変更前の値でドラムローリングを終えたことにしておく
      this.terminateDrumRolling(oldVal);

      // 今回は0.5秒以内にドラムロールが完了するようにしているため、　その間にどれくらいずつ値を変化させていくかの数値。
      const countInterval = Math.abs(Math.floor((newVal - oldVal) / 50)) || 1;

      // setInterval メソッドを使用して、0.01秒間隔で数値の変更処理を実行する
      if (oldVal < newVal) {
        this.intervalId = window.setInterval((): void => {
          this.countUp(newVal, countInterval);
        }, 10);
      } else {
        this.intervalId = window.setInterval((): void => {
          this.countDown(newVal, countInterval);
        }, 10);
      },
    },
    // ドラムロール処理を最終値で完了して、タイマーを止める
    terminateDrumRolling(endVal: number): void {
      this.rollingCount = endVal;
      clearInterval(this.intervalId);
    },
    countUp(newVal: number, countInterval: number): void {
      if (newVal <= this.rollingCount) {
        this.terminateDrumRolling(newVal)
        return;
      }
      this.rollingCount = this.rollingCount + countInterval;
    },
    countDown(newVal: number, countInterval: number): void {
      if (newVal >= this.rollingCount) {
        this.terminateDrumRolling(newVal)
        return;
      }
      this.rollingCount = this.rollingCount - countInterval;
    },
  },
});
</script>
```

実際の実装とは少し違ってきますが、どんな動きをするのかが見えるように CodePen を↓に載せています。
<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="js,result" data-user="t0yohei" data-slug-hash="vYEOyPx" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="VueNumberDrumRoll">
  <span>See the Pen <a href="https://codepen.io/t0yohei/pen/vYEOyPx">
  VueNumberDrumRoll</a> by t0yohei (<a href="https://codepen.io/t0yohei">@t0yohei</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## ちょっぴり解説
必要なことはだいたいコメントに書いているので、簡単な補足を書きます。
今回は実際に使用されるユースケースを考えて、親コンポーネントから数値を渡して、子コンポーネントでドラムロールを実行する形式で実装しています。
非同期通信で取ってきた値を元にドラムロールを実行する場合は、親コンポーネント内の `<input v-model.number="inputCount" type="number">` を消して、 `inputCount` を非同期処理で取得する値に置き換える感じになるかと思います。

ドラムロールを実装する際に考えないといけないのは、 `何秒間で処理を完了させるのか` ってことと、 `その間にいくつずつ数値を変化させるか` ってことなんですが、今回は `0.5秒間で処理を完了させて` 、 `変化前後の数値の差分を50で割った数ずつ変化させる` という形式で実装しました。(なんでそうしたかというと、その設定値だと見え方が綺麗だったからです！)
具体的にはこの部分で実現しています。

```javascript:NumberDrumRoll.vue
// 今回は0.5秒以内にドラムロールが完了するようにしているため、　その間にどれくらいずつ値を変化させていくかの数値。
const countInterval = Math.abs(Math.floor((newVal - oldVal) / 50)) || 1;

// setInterval メソッドを使用して、0.01秒間隔で数値の変更処理を実行する
if (oldVal < newVal) {
  this.intervalId = window.setInterval((): void => {
    this.countUp(newVal, countInterval);
  }, 10);
} else {
  this.intervalId = window.setInterval((): void => {
    this.countDown(newVal, countInterval);
  }, 10);
},
```

この変の数値をいじることで、どういう見え方をさせるかを調整することができると思います。

## 終わりに
思い出しながら書いていたら、意外と時間がかかりました。 Vue.js でドラムロールを実装する際は、よかったら参考にしてみたください。