Vuetifyでsvgとvue-property-decorator使って分目盛（？）付き時計知育アプリ作ってみた。

# この記事に関して

特定の技術に関して深掘りするものではないです。フロントサイドのみの軽いアプリを作成する時の一通りの事を紹介し、部分部分で読んでくれる人の参考になればと思います。

# 背景

ちょっと娘が時計の読み方で苦戦してる。見た所、長針と短針で目盛が違う（短針は一周12、長針は一周60）部分がこんがらがっているように感じる。探してみたが、世の中の知育アプリとかは短針の目盛ばっかり。
年末年始で勉強を兼ねて簡単なアプリを作っておきたい（そしてQiita記事投稿しておきたい）と思っていた所なので、分目盛もある時計の時間の進み方を視覚的に見せられるブラウザアプリを作ってみる事にした。

# 成果物

[Githubソース](https://github.com/silverbox/clockstudy)
[アプリページ](https://silverbox.github.io/clockstudyapp/index.html) ※時と場合によって閉じる可能性アリ。
時間数や分数を指定して、その分だけ進めたり戻したりする事が出来るようにした。今回は針の動き方などを見る為に特化したので、クイズなどはつけていない。

# 前提

- Dockerコンテナを作ってその中で開発。ただし、ソースはホスト側でVSCodeで編集。そしてコンテナ内部で編集を検知してホットデプロイされる形。
- Vue＋Vuetify＋vue-property-decoratorを使う。フロントサイドだけで構成する。
- アナログ時計部分はコンポーネント化する。
- 自分のデザイン力を考慮し、デザインにかける力は最低限に。※アプリページをご覧になった方で、もし改善案とか頂けると有り難いです。

# 環境構築

## vueプロジェクト作成

こんな[Dockerfile](https://github.com/silverbox/clockstudy/blob/main/vue/Dockerfile-vueini)でコンテナ作る。※Dockerの準備などが必要。これは環境構築の一手法なので、環境に合わせて他の記事などを参考にするのがよいかと。

```console
vue create clockstudy
```
この時点ではVue3はまだPreviewとか出てきていたので「Manually select features」を選択。
その次では、「TypeScript」をデフォルトから追加選択。

## vuetifyプラグイン追加

```console
cd clockstudy
vue add vuetify
```

## vue-property-decorator 導入

App.vue がその時点ではvue-property-decorator 対応した形でないので、[vue-property-decorator](https://github.com/kaorun343/vue-property-decorator) を参考に修正する。当時のソースを保持していないので間違えてるかもしれないけど、以下のような感じ。

```vue:Script部分
<script lang="ts">
import { Vue, Component } from "vue-property-decorator"

@Component()
export default class App extends Vue {
  name = 'App'
}
</script>
```

## 謎のエラー解消

開発モード（```npm run serve```）で動かすと以下のエラーがコンソールに出てくる。

> Try `npm install @types/vuetify` if it exists or add a new declaration (.d.ts) file containing `declare module 'vuetify/lib/framework';

ネット上ではtsconfig.json の compilerOptions.types に "vuetify" を追加というソリューションは見つかったが、自分の場合は解決せず。さらに探して[Vuetifyのissue](https://github.com/vuetifyjs/vuetify/issues/5944#issuecomment-520027576)にあった「src/plugins/vuetify.ts」を編集する事で解消。
その情報によると ```vue add vuetify``` による導入での問題（？）らしい。issueにあったぐらいなのでそのうち直ると思う（issue投稿は2019-08-10だったが・・・）。

ここまでで初期ページが表示されるまで。

# 実装

## svg をvueで表示する

丁度初期サンプルページのVueロゴはsvgで作成されている。svgファイルを別に作ってそれを読み込んでいる形。これを真似する。

```vue:HelloWorld.vueでのsvg使用例
<v-img
  :src="require('../assets/logo.svg')"
  class="my-3"
  contain
  height="200"
/>
```

## svg で文字盤作成

以下のポイントがある。

- 短針目盛は３時間毎、長針目盛は５分毎に大きくしておきたい
- 文字列も表示したい。
- 長針目盛を短針目盛の外側に配置したい

最初は長針と短針部分を作成。svgの回転機能（transform で rotate）を使用。それでも目盛数は多いので手作業ですべて行うのは死ぬのでLibreOfficeを使ってコピペできる様にした。スケールや位置などの修正時にこの表計算シートが役立った。※そこのsvg作成もプログラムでやるとカッコよかったかも？
その後、文字部分の作成。線では回転機能が使えたが、文字でそれを使うと文字自体も回転してしまうので使えない。なので、sinとcosの三角関数を駆使して起点を算出し、文字大きさなどの補正をする形に。この計算も表計算シートで行う。結果以下の形に。関数の中身などは [ワーク用ods](https://github.com/silverbox/clockstudy/blob/main/work.ods) を参照してもらえると。

[svgイメージ](https://github.com/silverbox/clockstudy/blob/main/vue/clockstudy/src/assets/component/analogclock/analogpanel.svg) と [svgソース](https://github.com/silverbox/clockstudy/blob/main/vue/clockstudy/src/assets/component/analogclock/analogpanel.svg?short_path=b3fa0b8)

これを同じようにVue側で読み込んでおく（[Vueソース](https://github.com/silverbox/clockstudy/blob/main/vue/clockstudy/src/components/AnalogClock.vue#L6-L11
)）。後で長針や短針を重ねるのでpositionはabsoluteにしてある。

```vue:読み込み部分
    <v-img
      style="position:absolute;"
      :src="require('../assets/component/analogclock/analogpanel.svg')"
      :height="height"
      :width="width"
    />
```

## svgで短針、長針作成

針を作成するだけなら文字盤のsvgと一緒にしてもよかったが、imgのオブジェクトを回転させる事で時刻を表現するので別のimgオブジェクトにしておく。説明するほどでないのでsvgの中身詳細は省略。針なのでもっとカッコよいsvgを作れれば良いが、今回は単なる太目の直線で表現。

## 針を回転させる

針のv-imgのstyleを可変にしておく。

```vue:html部分
    <v-img
      :style="getHourHandStyle"
      :src="require('../assets/component/analogclock/hourHand.svg')"
      :height="height"
      :width="width"
    />
```

それを時刻によって変更する形に。cssでは tranform:rotateを使用して回転させる。
後ろについているTransitionCssは後述のアニメーション用。

```typescript:TypeScript部分
  get getHourHandStyle (): string {
    const hourDeg = this.minutes / 2
    return "position:absolute; transform:rotate(" + hourDeg + "deg);" + this.getTransitionCss()
  }
```

## 針の回転をアニメーションする

cssのtransition機能を使用する。１秒かけて遷移する様にしておく。設定値の詳細は[公式ページ](https://developer.mozilla.org/ja/docs/Web/CSS/transition)で。後述の繰り上がり下がりケアの為に分岐がついている。これで、rotateの値変化による角度変化がアニメーションされ、針が動いているように見える。

```typescript:css指定部分生成
  getTransitionCss(): string {
    if (this.animate) {
      return "transition: all 1000ms 0s ease;"
    } else {
      return ""
    }
  }
```

## 時間の繰り上がり下がりをケアする

内部では分として現在の表示時刻を保持している。時間を進めすぎたりすると、内部の値が大きすぎたりする。使用シーンからするとコンピューター的にオーバーフローする事はほぼ無いが、気持ち悪いのでちゃんとケアしておきたい。

### マイナス値や720分（12時間）越したら補正する

現在の値に、720足して、720で割った余りを新しい値にする事で対応できる。
ただ、今回はアニメーションも重要。単に値を戻すだけだと、その時に針が逆走してしまう。

### 補正時にはアニメーションをオフにする

繰り上がり下がりの動きはつける。ただその後に補正する。その時だけアニメーションをオフにする事で、見た目は変化しない様にする。あまり使いたくないが、setTimeoutを使用する。その処理の中で補正が発生する時のみアニメーションをオフする事に。

#### setTimeout使用時のthis

いくつかポイントがあったので、これから同じ事をする人の為に記載。

- thisの型指定（anyだけど）をする事でエラー解消
- bind(this)を付ける事で参照可能にする

```typescript:
    this.timeoutId = setTimeout(function(this: any){ // <- ここ
      const wkMins = (this.curMins + 720) % 720
      if (wkMins != this.curMins) {
        this.useAnimation = false // <- ここがオフのフラグ
        this.curMins = wkMins
      }
      this.timeoutId = -1
    }.bind(this), 2000) // <- ここ
```

#### vueにおける処理フローでのポイント

最初は、フラグのオン／オフを処理の前後に挟んでいた。しかしどうもそのフラグがうまく動いていない感じだった。どうやら処理スレッドが全て終わった後にレンダリング処理が走るっぽい動作をしている事に気づく。
メインの更新時と、補正（setTiimeout内）の処理それぞれの前にフラグをセットする事で対応。

### 連続実行時の問題対応

setTiimeoutはセットした分だけ実行される。しかし今回のケースでは補正処理は操作していない時にのみ実行したい。実際に何もケアしないと連続実行すると、アニメーション中にsetTiimeout処理が走ってアニメーションが途切れてしまうような事が発生する。
その為、setTiimeout時にはそのIDをちゃんと保持しておき、処理が実行されたら保持IDをクリア（-1に設定）しておく。その上で、setTiimeoutの前にclearTimeoutを使う事で、待機中のsetTiimeout処理をキャンセルしておく。

```typescript:クリア部分とID取得部分
    if (this.timeoutId != -1) {
      clearTimeout(this.timeoutId) // キャンセル部分
    }
    this.timeoutId = setTimeout(function(this: any){
      // .. 中略 ..
      this.timeoutId = -1 // setTiimeout処理終了時には保持IDクリア
    }.bind(this), 2000)
```

## その他部分

ボタン配置やイベントは特筆すべき部分は無い。普通にvuetifyのgridレイアウト使ってボタンおいてイベント付ける感じ。

# 配信

~~静的webリソースだけで済むのでAWSのCloudFront＋S3で配信する。~~
GitHub Pagesでの公開に切り替えました。

## 静的webリソース作成

```console
npm run build
```

でdistフォルダに生成される。

## ~~S3~~ githubにアップ

~~新しくバケット作成して生成された静的webリソースをアップロード~~
新しくgithubにリポジトリ作成して静的webリソースをrootにpush
※GitHub PagesではURLが ```https://{ ユーザーID }.github.io/{ リポジトリ名 }/``` となるので、vueでビルドする時には vue.config.js に設定追加してビルドする。

```json:publicPath部分を追加
module.exports = {
  "transpileDependencies": [
    "vuetify"
  ],
  publicPath: '/{ リポジトリ名 }'
}
```

## ~~CloudFront整備~~ GitHub Pagesによる公開設定

~~S3Originタイプのオリジンを指定。バケットとパスは前述の静的webリソースをアップしたものを指定。ドメインがCloudFrontのランダム生成文字列になっちゃうけど、自分しか使わないのでこのままで。もちろん個人ドメインなどを持ってる人はそちらを設定してよいと思います。
※実際にはもっと指定するパラメーターあるけどボリュームがそっちメインになっちゃうのですいませんがそれをメインに説明している記事などを参照お願いします。~~
新しく作成した前述リポジトリで、「settings」の「Options」タブの下の方にGitHub Pagesの設定エリアがある。

###「Source」でブランチを選択

今回は「main」を選択。もちろん好きなブランチ利用可能。
リソースのパスも選択する。「/」rootか、「/docs」が選択可能。こちらは二択っぽい。今回はrootを使用。そして「保存」。

# 感想

今回svgを初めて使ってみた。拡大縮小もしやすいので幾何学模様ならば画像よりもsvgの方が良いと思った。
vue-property-decoratorを使って、vueのコンポーネント化なども体験でき、振り返ってソース見ると結構少ないソース量ながらも自分的には結構達成感あり。
この手の静的webリソースをだけのサービスを激安で配信できるCloudFrontはやっぱりすごい。
~~あ、github.io 使った方が良かったか。後でgithub.io化してみよう。~~
※GitHub Pages に切り替えました。