Vue.jsでインタラクティブなTAB譜を作ってみた


この記事は[ドワンゴ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/dwango)の13日目です。

12日目は berlysia さんの [「RxJS v6 の進化した TestScheduler を使う」](https://qiita.com/berlysia/items/dad98488c4bdde938ef3) でした。 さいきん自分も少しフロントエンドを触るようになったのですが、テストや非同期処理など使うライブラリがいっぱいで難しいですね。。バージョンアップに伴う新しい機能を解説できるくらい詳しくなりたい…！

# はじめに
[@namaozi](https://twitter.com/namaozi)といいます。ふだんはギターを弾いていますが、プログラマを仕事にしています。

仕事では主にニコニコ動画の投稿機能の開発をしています。自分は動画投稿が趣味なので、自分が使っているサービスの開発できてハッピーです＼(^o^)／

# つくったもの

さて、「Vue.jsでインタラクティブなTAB譜を作ってみた」という記事のタイトルですが、TAB譜というのはギターなどのフレット楽器向けの楽譜のことです。

普段僕は全くTAB譜を作らないので、手軽にTAB譜を作って公開できたらいいな〜と思っており、今回はVue.js (Nuxt.js) でTAB譜を作ってみました。

![スクリーンショット 2018-12-13 23.43.52.png](https://qiita-image-store.s3.amazonaws.com/0/103754/cab22f2b-b953-1c5a-6822-6065b2e33077.png)
[こんなコメントをもらっても](https://nico.ms/sm28409638)、気軽にTAB譜が作れるようになったのでもう大丈夫！ :grin: 

リポジトリはこちら: https://github.com/namaozi/namaozi-tabs

まず、こんな感じで独自記法を書いていくとTAB譜が生成されます。

![gqkxm-xrehy.gif](https://qiita-image-store.s3.amazonaws.com/0/103754/c899ee17-401b-13ee-31fa-221eb7cf86b3.gif)

それから、譜面のチューニングの変更、ズーム、スクロールができます。
（※カポの移動は実装中です）

![sxgb0-elack.gif](https://qiita-image-store.s3.amazonaws.com/0/103754/03fa3653-dd8f-9787-970a-46590b4bb12e.gif)

# よかったこと

## 1.インタラクティブなページができた🙆
Vueのバインディングが便利。
Vueはテンプレート内で `<Component :hoge="fuga" />` のように渡してあげるだけでバインディングされるのが本当に便利です。
TAB譜上部のinputやスライダーで得た値をバインディングして描画しており、かなりヌルっと動きます。

## 2.チューニングやカポ、相対音の概念まで取り入れられた🙆
これはわりとこだわりたかったところでした。
特に相対音で書けるようにしたのは自分の外せなかったポイントで、普段カポを使って曲を弾くし、シャープフラットが少ない調で記譜したほうがスムーズです。

## 3.かんたんにSPA作れた🙆
今回Nuxt.jsを使ってみました。ベストプラクティスの結集のようなフレームワークで、本当に気軽に使うことができて素晴らしいフレームワークだなと思いました。今後も使っていきたい。


# だめだったこと

## 1.直前にデプロイしようとして失敗した😇
この記事はアドベントカレンダー13日目の記事なんですが、全体的に作り終わって「よっしゃデプロイするぞ〜」って言いながら (nuxtの) build & generateすると以下のようなTypeErrorが出てしまい、記事の公開にWebページ公開が間に合わなそうだったので一旦諦めました:cry:
やはり継続的デプロイは大事ですね、最初のうちからデプロイしておくべきでした・・・
近いうちに問題解決してページ公開したいとおもいます:bow:

![スクリーンショット 2018-12-13 22.40.39.png](https://qiita-image-store.s3.amazonaws.com/0/103754/5c75255e-1d35-d91e-64cd-cfe7023476ed.png)
デプロイ直前でTypeErrorが発覚するの図。あとで調査します。

## 2.TAB譜は情報量が多すぎ😇
普通の五線譜は時間情報（音価）と音高の2種の情報しか持ちませんが、TAB譜の場合、音高の代わりに弦番号とフレット番号を指定する必要があり、時間情報と加えて3種の情報を持つ必要があります。テキストで左から右に書いていくとふつう1次元になってしまうので、どこかで変な工夫をする必要があり、シンプルな記法にしずらいのです、、、

## 3.譜面パーサーが既に崩壊寸前😇
独自記法を考えながらパーサーを実装していたらいともたやすく崩壊寸前になってしまいました。一体何度同じ過ちを繰り返してきたことか。人間は愚か。

結局TypeErrorもこのへんで出てしまっていたので、まずはここのリファクタから始めないとなぁ… すぐ仕様も変えそうなので、戒めのためと保存のためにヤバイ感じのソースを貼っておきます。。。

https://github.com/namaozi/namaozi-tabs/blob/67212b58b52e74a27bea60fa83f995486c17e7bb/components/ScoreTab.vue#L46-L87

```.js
    methods: {
      /**
       * スコアをパースして表示させやすくオブジェクトにする
       * @param rawScore
       */
      parse(rawScore) {
        // TODO: ここのTypeErrorを調査する
        // const splittedScore = rawScore.split('\n');
        // const rawBars = splittedScore.map(raw => raw.slice(1, -1).split('|'));
        const rawBars = rawScore.split('\n').map(raw => raw.slice(1, -1).split('|')).flat();
        const bars = rawBars.map((rawBar, index) => {
          let bar = {index: index + 1};
          const notes = rawBar.split(',')
            .map(fragment => {
              // 数字から始まる時,fragmentは単音
              // しかし 5X のような場合はミュート
              if (fragment.match(/^[1-6]/) !== null) {
                const key = fragment[0];
                const value = fragment.slice(1);
                if (value === "X") {
                  return {};
                }
                return {[key]: value};
              }
              // [E,X,X,G,B,E] みたいなfragmentは6弦から書いた和音
              // {6: "E", 3: "G", 2: "B", 1: "E"} の形に変換する
              if (fragment[0] === '[' /*&& note.substr(note.length) === ']'*/) {
                const fragmentNotes = fragment.slice(1, -1).split(';');
                let chordComponents = {};
                fragmentNotes.forEach((note, i) => {
                  if (note !== 'x') {
                    return chordComponents[6 - i] = note;
                  }
                });
                return chordComponents;
              }
              // 空白は休符
              // {} 空オブジェクトにする
              return {};
            });
          bar['notes'] = notes;
          return bar;
        });
        return bars;
      }
    }

```

## 4.アニメーションできなかった😇
このアプリは譜面をパースして子コンポーネントに渡しているだけなので、描画の管理などが全くできていません。
途中まで作って「さぁアニメーションさせるぞ〜！」と思ったらコンポーネントの動的生成・時間管理が必要なことに気づき、いまの設計だと無理そうだったので一旦諦めました:cry:

そのかわりにスクロール位置などをバインディングさせることでインタラクションは安く実装することができました。今後似たシチュエーションで役に立つかも…！

## 5.音出せなかった😇
せっかくインタラクティブな感じになってるので音も出したかったんですができませんでした:cry:
これもアニメーションできなかったのと同じ問題で、いまどのコンポーネントを見ているのか、的な管理が必要になり、高コストだったので諦めました。
今後はコンポーネントの動的生成を念頭に置いて設計から見直す必要があるかもしれません。

# おわりに

明日はRuたんさんこと [@ru_shalm](https://twitter.com/ru_shalm) さんの記事です。Ruたんさんの作られるものはいつも面白くて尊敬してやみません。お楽しみに！！

それから今回TAB譜にした拙作「shallowness」は↓から聴けます、よければ是非に！ :raised_hands:

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">タイトル: shallowness / namaozi<br>演奏者: namaozi<br>コメント: その昔作った、流れるようなハイテンポでの8分の5 + 4分の5拍子の曲。かなりのお気に入り！<a href="https://t.co/Bgx1pxwclW">https://t.co/Bgx1pxwclW</a> <a href="https://twitter.com/hashtag/namaoziguitar?src=hash&amp;ref_src=twsrc%5Etfw">#namaoziguitar</a><a href="https://twitter.com/hashtag/acoustic_tone?src=hash&amp;ref_src=twsrc%5Etfw">#acoustic_tone</a> <a href="https://twitter.com/PlayAcoGuitar?ref_src=twsrc%5Etfw">@PlayAcoGuitar</a> <a href="https://t.co/HDxVVyylag">pic.twitter.com/HDxVVyylag</a></p>&mdash; なまおじ (@namaozi) <a href="https://twitter.com/namaozi/status/1051020296575045632?ref_src=twsrc%5Etfw">2018年10月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>