オールSVGで音楽の五度圏アプリを作ってみた

この記事は [NTTコムウェア Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nttcomware) の25日目（最終日！）の記事となります。

## はじめに
私は建設や工事などのいわゆる”現場”で働く方々が Happy になれるソリューション提案やサービス提供を行なっています。

直近では、図面上の作業空間を確認するWebサービスを、SVGを利用して検討することになりました。

SVGは未経験でしたので、今回はその勉強で学んだことと、試しに作ってみたアプリの紹介となります。

## SVGとはなんぞや
SVG (Scalable Vector Graphics) はWebで画像を表示するための技術です。

https://ja.wikipedia.org/wiki/Scalable_Vector_Graphics

以下のような特徴から、従来のjpegやpngからの置き換えが進み、今後ますます普及することが見込まれています。

* [ベクター形式](https://ja.wikipedia.org/wiki/%E3%83%99%E3%82%AF%E3%82%BF%E3%83%BC%E7%94%BB%E5%83%8F)のため、どんな画面サイズでも画質が荒くなることがなく表示できる
* HTML5で採用されており、[W3Cによって仕様が標準化](https://www.w3.org/TR/SVG2/)されている
* 拡張性の高いテキストベースの記法により、javascriptやflashを用いずともインタラクティブな画像変化が可能

<a title="Yug, modifications by Cfaerber et al., CC BY-SA 2.5 &lt;https://creativecommons.org/licenses/by-sa/2.5&gt;, via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File:Bitmap_VS_SVG.svg"><img width="512" alt="Bitmap VS SVG" src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Bitmap_VS_SVG.svg/512px-Bitmap_VS_SVG.svg.png"></a>

## 五度圏とはなんぞや
五度圏 (Circle of Fifths) とは音楽のキーの関係性がひと目で理解できる表のことです。

キーというのは、CメジャーとかAマイナーとか、その音を特徴つける調性のことです。
（カラオケの「キーを上げる／下げる」と言った方が伝わりやすいかもですね）

https://watanabejunya.com/circle-of-fifth/

視覚的かつ円の回転が伴うのでSVGの題材にはピッタリかと思います。五度圏を題材にアプリを作ってみます。

## 作ったアプリの紹介
いろいろ説明するよりみてもらった方が早い気もするので、公開します。（↓こちらのリンクより↓）

https://7010oncajon.github.io/apps/circle-of-fifths/index.html

![IMG_4997_resized.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2385327/b74ba5e1-21bc-7eae-1b84-97cf1a3b3541.png)

世の中には五度圏の画像やアプリなどはありますが、それに比べて以下のような特徴を持っています。

* フラット（♭）が増える方向が時計まわりになっている
    * シャープ（♯）が増える方向が時計まわりになっていることが多い
    * しかし、ドミナント進行と呼ばれる超重要コード進行を念頭におくと、フラットが時計回りの方が使い勝手が良い（はず）。
* ダイアトニック・コードの役割を表示できる
    * トニック: 落ち着いた音色（APでは 青色の丸 で表示）
    * サブドミナント: トニックほどではないが緊張感あり（緑色）
    * ドミナント: 緊張感と不安定な音色。トニックに進行する（オレンジ）

五度圏の素晴らしさや、このアプリの利用シーンをとうとうと語れるのですが、ここはQiitaなのでここまでにしておきます 笑

## 技術的な特徴 および 苦労した点
アプリの構成は非常にシンプルです。ここからは技術的な話となり、ある程度Webフロント(htmlやjs)の理解があることが前提となります。

```
circle-of-fifths
 - index.html             : メインの画面
 - app.js                 : 描画および動作の処理を記述
 - snap.svg-min.js        : ライブラリ（後述）
 - style.css              : スタイルシート（レスポンシブ対応のみ記述）
 - apple-touch-icon.png   : faviconアイコン（iOS用）
 - android-touch-icon.png : faviconアイコン（Android用）
```

### オールSVGで作ってみた
ベースとなるhtmlのbody部にはSVGタグしか用意していません。

```html:index.html
<svg id="svg" viewbox="0 0 360 640" width="360" height="640"></svg>
```

この中にSVGの要素であるCircleタグやRectタグ、Textタグが記述されることで、GUIを構成しています。

画面の下部にあるボタンもSVGです。アニメーションやカラーでボタンのようなUIを実現してみました

### javascriptライブラリ(Snap.svg)を導入してみた
前述の通りSVGはHTML5の標準タグのため、特段のライブラリは不要です。

しかし、生産性の効率（と、個人的な興味から）SVGタグを操作するライブラリを導入して実装しました。

http://snapsvg.io/

使い勝手はよかったですが、本質的にはタグ操作のラッパであることには変わりません。なので無理に導入しなくても、jQueryなどで使い慣れたものでも十分かと思います。

導入してよかった点は、ライブラリ側でアニメーションを用意してくれていたところです。円盤の回転やボタン押下時のアニメーションを簡単に実現することができました。

```js:sample.js
var s = Snap("#svg");
var object = s.rect(100, 100, 80, 60);  // 変形対象のオブジェクト
var matrix = Snap.matrix().rotate(45);  // 変換行列

// 通常の変形
// object.transform(matrix);

// アニメーション付きの変形
// 第一引数：変換行列、第二引数：アニメーションの時間
object.animate({ transform: matrix }, 1000)
```

### スマホ対応もばっちりにしてみた
Webベースの実装ですので、スマホでも動きます。

PWAほどしっかりとは作り込んでいませんが、①ファビコンの導入と②レスポンシブデザインの設定で、あたかもアプリのように動きます。

* iOSの場合
    * ブラウザで開く
    * 下の"共有"アイコンから「ホーム画面に追加」を選択
    * ホーム画面からすぐに起動

![IMG_4996_resized.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2385327/ff5cad41-7d5f-2825-be08-38f11cdc0cec.png)

* Androidの場合
    * 持ってないので確認できなかったけど、同じようにできるはず！


#### ①ファビコンの導入
適当にアイコンを作って、それなりの大きさにリサイズして、htmlのhead部にリンクを貼ればOKです。

![apple-touch-icon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2385327/8430bebc-ca10-db68-3a5e-68c524b69f29.png)

```html:index.html
<!-- fabicon設定-->
<link rel="apple-touch-icon" href="apple-touch-icon.png" sizes="180x180">
<link rel="icon" type="image/png" href="android-touch-icon.png" sizes="192x192">
```

#### ②レスポンシブデザインの設定
スタイルシートにレイアウトの設定をして上げて、svgタグを`div`でラップすればOKです。（YouTubeの埋め込みをするときと同じ考えですね）

```css:style.css
.contents {
    max-width: 360px;
    margin: auto;
}
.svg-wrapper {
    position: relative;
    width: 100%;
    padding-top: 177.77777%; /* SVG高さ / SVG幅 * 100 */
}
.svg-wrapper svg {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}
```

```html:index.html
<div class="contents">
  <div class="svg-wrapper">
    <svg id="svg" viewbox="0 0 360 640" width="360" height="640"></svg>
  </div>
</div>
```

### グループと座標系の適切な理解を
HTMLの画面構成は部品ごとの相対的な位置で制御します。
一方でSVGはピクセル座標(x軸, y軸)での指定となります。

なので部品を配置後に全体的にレイアウトを調整しようとすると、全ての座標の修正が入ってしまい大変です。

SVGには部品をまとめる`group`というタグがありますので、各部品はローカル座標で調整し、そのグループを最後にグローバル座標で調整することが大事です。

![local_global.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2385327/5e2e4ada-2fd3-68bf-65c7-dd8f450162a4.png)

ちなみに上の図もSVGで書きました。（SVGのSVGによるSVG記事の画像 笑）


### 360°以上回転させる場合にはご注意を
円盤をぐるぐる回転させると270°のところで回転アニメーションがバグりました。
これは内部的には 270° が -90° と認識されたためです。

```shell:Webコンソール
// 連続で右方向に回転させた時の回転角度
app.js:294 29.999999993824627
app.js:294 60.00000000617539
app.js:294 90
app.js:294 119.99999999382463
app.js:294 150.0000000061754
app.js:294 180
app.js:294 209.9999999938246
app.js:294 240.00000000617536
app.js:294 -90                 // NG 期待した数値は 270
app.js:294 -60.00000000617539
app.js:294 -29.999999993824627
app.js:294 0
```

こればっかりはどうしようもないので、条件文を追記して対応しました。

* ソースコードが美しくなくなって、すごく嫌。。。
* 一部、直し忘れたので、ある条件の時にだけバグります
    * アニメーションが崩れるだけなので実害はないです
    * シャープとフラットボタンをいろいろ押して、ぜひ発生条件を見つけてください

それにしても、360°(= 0°) で、何かは起こるだろうとは思っていましたが、なぜ 270° なのだろう。Snap.svgのバグかな。

### Apple社のブラウザにはご注意を
PCのChromeでの動作確認を終えて、iPhoneで確認したところ、綺麗に配置した文字が少しづれていることに気づきました

老眼かな？とも思いましたが、PCのSafariでも、づれることに気づきました。

調べてみると`dominant-baseline`属性はsafariおよびiOSのChromeでは有効にならないようです。

**標準化とは何か？**
（私は声を大にして問いたい。。。）

仕方がないので、スタックオーバーフロー先生の教えに従い、`dy`属性で対応しました。

https://stackoverflow.com/questions/35565063/svg-dominant-baseline-not-working-in-safari

```js:app.js
let k = s.text(CENTER_X, CENTER_Y, key.split(''));
k.attr({
    textAnchor: "middle",
    // dominantBaseline: "middle", // 有効にならない！
    dy: "0.5em",                   // 代わりに dy属性 を利用
    letterSpacing: 0,
    fill: color,
    fontSize: size
});
```

## おわりに
2021年は私たちの生活が大きく様変わりをした１年でした。（それは望むと望まざるに関わらずです）
不確実性がます世の中で、不安なこと、悲しいこと、不自由なことがまだまだ続くかもしれません。

しかし、そんな中でもGoodな変化もあったのかな、と私は思います。

私ごとにはなりますが、会社がリモート勤務を基本とする方針としたことで、家族との時間が大幅に増えました。また空き時間を利用して、ピアノを弾いたり、小説を書いたり、それこそ今回のアプリのような勉強の時間に充てることも出来ました。そういった意味でも決して忘れることのできない１年だったと思います。

来年も私と、あなたにとって、よきことが増える１年であってほしいと、心から思います。

最後になりましたが、Qiita運営の皆様、NTTコムウェアAdventCalendar事務局の皆様、ありがとうございます！とっても楽しかったです！！

**Merry Christmas**
*and*
**Happy New Year.**

<iframe width="560" height="315" src="https://www.youtube.com/embed/Or8yUrhnVI0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>