Vue.js+SVGで自由にCSSアニメーションしたい人のための完全解説（ソース付き）

こんにちは。UX＆フロントエンジニアしながら絵描きして遊んでいる**[ゆき(@yuneco)](https://twitter.com/yuneco)**です。この記事ではVue.jsを使ってCSSアニメーションを使った表現を自在に行うための基本的な部分をステップバイステップで解説します。目標は↓以下のようなアニメーションをJavaScriptで自在に構築できるようになることです。 

![0_goal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/b20a196e-fd39-fed3-b4dc-c6630ac0f754.gif)
ソースコードはこちら： https://github.com/yuneco/css-anime-tutorial

## 目次

この記事では最初にSVGを単純に表示するところから始め、Vueのコンポーネントを利用してそのSVGを自由に配置・変形させる方法を説明します。その上でCSS transitionを用いたアニメーションを取り入れます。最後に、複雑なアニメーションを抽象化・構造化してより複雑なシーンを構成するための方法を解説します。

1. [SVGを作る](#SVGを作る)
2. [Vueプロジェクトを作る](#Vueプロジェクトを作る)
3. [SVGを表示する](#SVGを表示する)
4. [好きな場所に配置する](#好きな場所に配置する)
5. [大きさ・角度を自由に変える](#大きさ・角度を変える)
6. [アニメーションさせる](#アニメーションさせる)
7. [連続して動かす（キーフレームアニメーション）](#連続して動かす（キーフレームアニメーション）)
8. [アニメーションの抽象化と構造化](#アニメーションの抽象化と構造化)

### おことわり

- この記事で解説する方法は必ずしもアニメーションを構築する際のスタンダードな方法ではありません
- 楽に複雑なアニメーションを作りたい場合、[animejs](https://animejs.com/)や[PixiJS](https://www.pixijs.com/)等、高度なライブラリの導入・学習を検討してください
- この記事ではアニメーション専用のライブラリを用いずに自力でアニメーション構築までたどり着くことでVue.jsやJavaScript、CSSアニメーション等に関するより深い理解を得ることを狙っています

...若干言い訳っぽくもありますが、単純に「自分で仕組みを理解してアニメーションを作れる」スキルを身に付けるのは間違いなく楽しいことです。少々長い記事になりますがお付き合いいただければ幸いです :blush: 

## SVGを作る

まず最初にこのチュートリアルで使うSVGを作ります。Illustratorで好きなキャラクターを作成し、メニューから[ファイル]＞[書き出し]＞[スクリーン用に書き出し]を選択します。「形式」をSVGに変更し、右側の歯車アイコンから設定を表示します。
￼![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/8bf01b09-6b74-9131-a58c-2356be74a0d5.png)

...ちょっと小難しい設定が出てきました:worried:。でも今回は「SVGを使って〜」と言いながらもSVGタグ自体をVue.jsでゴリゴリするわけではないのでここの設定はさして気にしなくて大丈夫です。右下の「レスポンシブ」チェックだけ外しておいてください。[^svgsetting]

[^svgsetting]: 入れたままでも良いのですが、初期状態でサイズが決まっていた方が扱いやすいのです

設定ができたら「設定を保存」＞「アートボードを書き出し」でSVGファイルを出力します。Illustratorがない方は別なファイルでももちろんOKです。面倒な場合はひとまず[githubにファイルを置いたので](https://github.com/yuneco/css-anime-tutorial/blob/master/public/img/tama.svg)使ってください。

ブラウザにドロップすると、こんな感じで表示されると思います。名前は**「タマさん」**です。今決めました。（今回はわかりやすくするため、1pxの青線でボーダーを入れています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/843428d9-160e-8e67-8341-8df7d8d04a37.png)
## Vueプロジェクトを作る

なにはともあれVueのプロジェクトを作らないと始まりません。`vue create プロジェクト名`でプロジェクトを作成します。この記事では下記の構成で進めますが、もちろん好みに応じて変えていただいても大丈夫です。[^vuebasic]

[^vuebasic]: Vue.jsの基本については説明しないので、不明な場合には適時Vueの入門記事等を参照してください。

```
? Please pick a preset: 
  default (babel, eslint) 
❯ Manually select features 

? Check the features needed for your project: 
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
❯◉ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported 
by default): (Use arrow keys)
❯ Sass/SCSS (with dart-sass) 
  Sass/SCSS (with node-sass) 
  Less 
  Stylus 

? Pick a linter / formatter config: 
  ESLint with error prevention only 
  ESLint + Airbnb config 
❯ ESLint + Standard config 
  ESLint + Prettier 

以後すべてデフォルト
```

プロジェクトができたら、余計な`HelloWorld`コンポーネントを削除し、空っぽのプロジェクトが`npm run serve`で立ち上がることまで確認してください。

## SVGを表示する

先ほど作成したSVG(tama.svg)をプロジェクトの`/public/img/`に配置します（imgディレクトリは作成してください）。まずはこのSVGをVueで表示するところから始めましょう。

普通にSVGを出すだけであればVueはあんまり関係ないのですが、後々のことを考えてこの時点でVueのコンポーネントにしておきます。`components`ディレクトリに新しくファイルを作成し、以下を記述します：

```vue:src/components/Tama.vue
<template>
  <img src="/img/tama.svg" alt="タマさん">
</template>
```

このチュートリアルでは一番簡単な方法...ということでimgタグで読み込みをします。
他にもSVGを表示する方法として、

1. `<svg>`タグで直接記述する
1. `cssのbackground-image`で画像として読み込む
1. [vue-svg-loader](https://www.npmjs.com/package/vue-svg-loader)等を利用してVueのコンポーネントとして読み込む

といった方法があります。特に1はより自由度の高い表現（SVGの中の一部分の色や形をVueで制御する、等）ができるので、より複雑な表現を追求したい方はぜひチャレンジしてみてください。

シンプルですが`Tama`コンポーネントができたのでこれを表示します。`App.vue`から読み込んで表示しましょう。

```vue:App.vue
<template>
  <div id="app">
    <tama></tama>
  </div>
</template>

<script>
import Tama from './components/Tama.vue'
export default {
  name: 'app',
  components: {
    Tama
  }
}
</script>

<style lang="scss">
html, body {
  margin: 0;
  padding: 0;
}
body {
  position: relative;
  height: 100%;
  background: url('/img/grid.svg') repeat;
}
#app {
  margin: 0;
}
</style>
```

`Tama`コンポーネントをそのまま読み込んで配置しているだけです。
スタイル部分は余計なマージン等をリセットして、わかりやすくするためにbodyの背景に[グリッド画像](https://github.com/yuneco/css-anime-tutorial/blob/master/public/img/grid.svg)を表示しました。

こんな感じで表示されればまずはクリアです！
￼![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/2385baef-3f0f-07cb-dfe4-4ee1c23b4f6d.png)

## 好きな場所に配置する

とりあえず画面に絵はでましたが、好きな場所に自由に表示できないと絵作りができませんよね。次はこのタマさんを思った場所に配置できるようにしていきます。

### 指定の座標に配置する

ひとまず、`x=200px, y=100px`の位置に置いてみましょう。

```vue:Tama.vue
<template>
  <img
    class="tama-root"
    src="/img/tama.svg"
    alt="タマさん"
  >
</template>

<style lang="scss" scoped>
.tama-root {
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(200px, 100px);
}
</style>
```

`position: absolute`で絶対配置にして`transform`で座標を指定します。位置の指定には`top`と`left`も使用できますが、CSSでアニメーションをするときにはできる限りtransformを使いましょう。独立した要素の表示位置を指定するだけの用途ではこちらの方が軽く、（いくつか要件もありますが）GPUレンダリングによってよりぬるぬるのアニメーションを実現できます。

できました！ `(200px, 100px)`の位置にタマさんがいらっしゃいました！
￼![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/6d1fd12e-f47c-c0e2-10cb-736f301e2cb1.png)

・・・いや、ちょっと待って:upside_down: `(200px, 100px)`の位置にタマさんを立たせたかったのに、基準が左上になってしまっています。もちろん絵にもよりますが、このタマさんのようなキャラクターであれば足元を基準に配置したいですよね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/84c4d7d8-a0d3-8d5a-c5ad-685a7ac359c8.png)

やりかたはいくつかありそうですが、今回は単純に`margin`で調整します。

```scss:Tama.vue
.tama-root {
  // ...
  margin: -300px auto auto -90px;
}
```
￼
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/2e232d52-0754-3054-54e5-949c8a2cf267.png)
これで無事、`(200px, 100px)`の位置にタマさんがたちました！（見切れてるけど！）

### 配置する座標をパラメータで制御できるようにする

ひとまず決め打ちの場所に配置することはできましたが、実際に使うときはこの座標を動的に変更したりアニメーションさせたりしたいですよね？よってCSSの中にハードコードするわけにはいきません。Vueのプロパティを使ってこの座標を制御できるようにしましょう。

```vue:Tama.vue
<script>
export default {
  name: 'Tama',
  props: {
    x: { type: Number, default: 200 },
    y: { type: Number, default: 100 }
  }
}
</script>
```

`Tama.vue`にscriptを追加し、`x`,`y`2つのプロパティを作成します。`type`や`default`も設定しておきましょう。
このプロパティを使って動的にスタイルを変えるので、`<style>`から`transform`を削除し....

```scss:Tama.vue
.tama-root {
  // ...
  margin: -300px auto auto -90px;
  // 削除: transform: translate(200px, 100px);
}
```

代わりにテンプレート内でプロパティを使用してスタイルを指定します：

```vue:Tama.vue
<template>
  <img class="tama-root" src="/img/tama.svg" alt="タマさん"
    :style="{
      transform: `translate(${x}px, ${y}px)`
    }"
  >
</template>
```

これで呼び出し側(`App.vue`)から好きな位置を指定してタマさんを立たせることができるようになりした。

```vue:App.vue
  <div id="app">
    <tama :x="300" :y="400"></tama>
  </div>
```
￼
## 大きさ・角度を変える

同じようにして、大きさ（スケール）と角度も変えられるようにしましょう。位置・大きさ・角度を自由にコントロールできるようになれば、複数のパーツを組み合わせて好きな画面を作れるようになりますね。

### プロパティで大きさ・角度を制御する

まずは位置と同様にプロパティの追加からです。スケールは縦と横を別々に設定できるように、`scaleX`と`scaleY`の2つのプロパティにしておきます。

```js:Tama.vue
  props: {
    x: { type: Number, default: 200 },
    y: { type: Number, default: 100 },
    scaleX: { type: Number, default: 1.0 },
    scaleY: { type: Number, default: 1.0 },
    rotate: { type: Number, default: 0 }
  }
```

このプロパティをテンプレートに反映させます。スケールと回転も`transform`で指定できるので簡単ですね。`scale`は単位不要ですが、`rotate`には角度の単位が必要なので忘れずに。

```vue:Tama.vue
  <img class="tama-root" src="/img/tama.svg" alt="タマさん"
    :style="{
      transform: `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`
    }"
  >
```

最後にこれも呼び出し側からプロパティを設定しましょう。

```vue:App.vue
  <div id="app">
    <tama :x="300" :y="400" :scale-x="1.5" :scale-y="1.5" :rotate="45"></tama>
  </div>
```

位置の指定と同じなので簡単ですね・・・あれ？
￼![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/61024e7f-1770-d23c-336f-850942de3f3c.png)

確かに大きさと角度は変わりましたが、またしても基準がちょっと変です。このモヤモヤが伝わりますでしょうか...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/10c56536-f92e-db74-182c-1069ec22cbc1.png)

この問題を解決するには、専用のCSSプロパティ[transform-origin](https://developer.mozilla.org/ja/docs/Web/CSS/transform-origin)を使用します。

```scss:Tama.vue
.tama-root {
  // ...
  transform-origin: 90px 100%;
}
```

`transform-origin`は%でもpxでも設定できるので、いい感じにタマさんの足元を基準に設定してあげましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/59f85744-4ffc-428e-1120-de6420bc2646.png)

## アニメーションさせる

やっとかよ...と言われてしまいそうですが、ようやくアニメーションする準備が整いました。まずはシンプルに「クリックすると50px上にジャンプする」アニメーションを作ります。基本的な方針は**「クリックされたらタマさんのyプロパティを-50する」**だけです。

### クリックで位置を変更する

アニメーションの実装場所は`Tama.vue`自身の中と呼び出し側の`App.vue`の2箇所が考えられますが、「タマさん自身にジャンプする機能を持たせる」方が自然なので`Tama.vue`の中に書いていきましょう。

```vue:Tama.vue
<template>
  <img class="tama-root" src="/img/tama.svg" alt="タマさん"
    :style="{...}"
    @click="jump(50)"
  >
</template>
...
<script>
export default {
  name: 'Tama',
  props: {...},
  methods: {
    jump (height) {
      this.y -= 50
    }
  }
}
</script>
```

`@click`でクリック時にjumpメソッドを呼び出し、その中で`y`プロパティを-50しています。この状態でタマさんをクリックすると、クリックのたびにタマさんが50pxづつ上に上がっていきます。

ただ困ったことに、この状態でブラウザのコンソールを見てみると警告がでてしまっています。
￼![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/f9c31151-e2a3-b7f4-88a8-7e20d7b0955c.png)

`y`プロパティは外部（App.vue）で指定できるように公開しているのに、`Tama.vue`の内部で変えちゃダメだよ！って警告ですね。`(x, y)`はあくまでベースの立ち位置として使い、内部用の相対位置を管理する変数を追加します。

まず、内部用の変数のために`data`を作成し、変数`dx`と`dy`を追加します。`jump`メソッドで変更する対象も`dy`に変えましょう。

```vue:Tama.vue
<script>
export default {
  name: 'Tama',
  props: {...},
  data () {
    return {
      dx: 0,
      dy: 0
    }
  },
  methods: {
    jump (height) {
      this.dy -= height
    }
  }
}
</script>
```

テンプレート側は`ベースの立ち位置(x, y) + 相対位置(dx, dy)`で座標を指定します：

```vue:Tama.vue
<template>
  <img class="tama-root" src="/img/tama.svg" alt="タマさん"
    :style="{
      transform: `translate(${x + dx}px, ${y + dy}px) ...`
    }"
    @click="jump(50)"
  >
</template>
```

### 位置の変更をアニメーションにする

クリックで動くようにはなりましたが、まだアニメーションじゃないですね、これ。アニメーションを追加しましょう。アニメーションの方法としては大きく次の2つが考えられます：

1. `y`や`dy`といった座標を制御する変数をタイマー等で連続的に変化させる
2. 変数の値は一気に変えてしまって、CSSのアニメーションで補間する

1は自由度が高く複雑なアニメーションの表現や制御ができますが、毎フレーム座標の計算が走るため重くなりがちです。2であれば、変更後の値さえ指定してしまえば、間のフレームはブラウザがいい感じに描画してくれるため、基本的に高速です。

今回はもちろん、2で進めましょう。[^othercase]
実装はCSSに2行足すだけです。

[^othercase]: 過去に記事にした作例だと[VueとSVGを使ってシューティングゲーム『ネコ🐱メザシ🐟アタック🌟』を作ったのでソースと解説](https://qiita.com/yuneco/items/34daf063075d03c4eede)が1のタイマーを用いるパターンで、[VueとFirebaseの基本機能全部使ってぬるぬる動くポートフォリオサイトを作ったのでソースと解説](https://qiita.com/yuneco/items/b14a695a060c96309239)が2のCSSアニメーションのみによるもの（この記事と同じ）です。


```scss:Tama.vue
.tama-root {
  // ...
  transition: transform 1s ease;
  will-change: transform;
}
```
`will-change`はアニメーションを滑らかにするためのおまじないです。（本来は「おまじない」程度の理解で使うのは良くないプロパティなので、詳しくは別な記事：[will-changeで目指す60fpsのぬるぬるCSSアニメーション](https://qiita.com/yuneco/items/f07d60ab21dd1de9a51d)をご参照ください）

これでクリックするたびにヌルっとした動きでタマさんが上にせり上がるようになりました。ひとまず、最初のアニメーション完成です:tada:
![1_move1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/0e54776c-e6ce-7f7f-0aab-556598e72ff5.gif)

### アニメーションの時間とイージングを制御する

先ほどの例ではアニメーションの時間(1s = 1秒)やイージング(ease)をハードコードしてしまいましたが、これらの設定は当然アニメーションの内容によって変わってきますよね。これも変更可能にしておきましょう。

`data`に`duration`と`easing`変数を追加し、CSSの`transition`をこの2変数を使って組み立てるよう、テンプレートに追加します。

```vue:Tama.vue
<template>
  <img ...
    :style="{
      transform: ... ,
      transition: `transform ${duration}ms ${easing}`
    }"
    @click="jump(50)"
  >
</template>
...
<script>
export default {
  name: 'Tama',
  props: {...},
  data () {
    return {
      ...
      duration: 1000,
      easing: 'ease'
    }
  },
  ...
}
</script>

```

## 連続して動かす（キーフレームアニメーション）

ジャンプなので浮き上がったら地面に戻ってこないといけませんね。アニメーションを連続して実行する方法を考えます。

イメージとしてはこんな感じ：

```javascript:Tama.vue
jump (height) {
  this.dy = -height
  // アニメーションが終わるまで待つ
  this.dy = 0
}
```

### タイマーでアニメーションを連続実行する

アニメーションの終了を待ち合わせるには

1. 単純にタイマーで待つ
2. CSS transiotionの`transitionend`イベントを拾う

の2つが考えられます。

正攻法は2なのですが、複数のアニメーションを入れ子にした場合の制御が結構難しいので、今回はシンプルに1でいきたいと思います。

```javascript:Tama.vue
jump (height) {
  this.dy = -height
  this.easing = 'ease-out'
  window.setTimeout(() => {
    this.dy = 0
    this.easing = 'ease-in'
  }, this.duration)
}
```

イージングも設定して、いい感じにふわっとジャンプするようにしてみました。クリックするとバッチリ、ジャンプして元の位置に戻ってきますね！
![1_jump1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/ec0a9e75-44e8-8bf6-98fb-31f7cbb954fc.gif)

### async/awaitでアニメーションを連続実行する

では、この調子でもっと複雑なアニメーションを...
**・・・うん、つらい** :sob:

JavaScriptそれなりに書かれている方なら、もう先ほどのコードの時点でヤバみを感じていただいていることと思います。そうです、:imp: **コールバックヘル** :imp:です。この先3つ4つのアニメーションをこの方式で入れ子にしていくのは地獄でしかありません。

この地獄から抜け出すために、[async/await](https://qiita.com/soarflat/items/1a9613e023200bbebcb3)を使います。といっても特別なものではなく、単に標準のタイマーをPromise化するだけです。

```javascript:src/core/Time.js
export default {
  /**
  * Promiseを使い指定の時間待機します。
  * @param {Number} ms 待機時間（ミリ秒）
  * @return {Promise} 引数の指定時間経過後にresolveされます
  */
  wait (ms) {
    return new Promise(resolve => {
      window.setTimeout(resolve, ms)
    })
  }
}
```

このあたりは馴染みがないとちょっとわかりづらいと思いますが、このPromise化したタイマーを使うとこんな感じで待ち時間を簡単に挟むことができるようになります：

```javascript
console.log('このログはすぐ表示される')
await Time.wait(2000) // ここで2秒待つ
console.log('このログは2秒後に表示される')
```

この記事では　Promiseやasync/awaitの説明はしませんが、ひとまず使い方のイメージがつけば大丈夫かと思います。

この`Time.js`を使うと、先ほどのつらみコードは以下のように書き直せます：

```javascript:Tama.vue
<script>
import Time from '@/core/Time'
export default {
...

  async jump (height) {
    this.dy = -height
    this.easing = 'ease-out'
    await Time.wait(this.duration)
    this.dy = 0
    this.easing = 'ease-in'
    await Time.wait(this.duration)
  }

...
}
```

## アニメーションの抽象化と構造化

これで3つ以上のアニメーションでも連続実行できそうな仕組みが整いました。せっかくなので、もう少しだけ抽象化してアニメーション専用のライブラリのような快適な仕組みを整えましょう

### Tween風メソッドの実装

先ほどの例では、以下の2ステップをアニメーションのパート（キーフレーム）ごとに繰り返しています：

1. dataの内の変数をいろいろ変更
2. `Time.wait`でCSSのアニメーションが終わるのを待つ

この処理を別メソッドに追い出します。

```javascript:Tama.vue
methods: {
  async tween (props, duration = 1000) {
    Object.assign(this.$data, props)
    this.$data.duration = duration
    await Time.wait(duration)
  },
  async jump (height) {
    await this.tween({ dy: -height, easing: 'ease-out' }, 1000)
    await this.tween({ dy: 0, easing: 'ease-in' }, 1000)
  }
}
```

追加した`tween`メソッドは[Object.assign](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)を使って、引数で渡された`prop`で`this.$data`の変数を上書きします。その上で`Time.wait`を使用して`duration`ミリ秒待機します。
呼び出し側の`jump`はこれを使ってとってもシンプルにアニメーションをかけるようになりましたね。

### 複雑な動きの実装

`tween`メソッドの実装で複数のアニメーション（キーフレーム）を容易につなげることができるようになりました。これを使ってジャンプの動きをもっと洗練させてみます。

```js:Tama.vue
async jump (height = 200, duration = 2500) {
  await this.tween({ dScaleY: 0.8, easing: 'ease' }, duration * 0.1)
  await this.tween({ dy: -height, dScaleY: 1.1, easing: 'ease-out' }, duration * 0.35)
  await this.tween({ dy: 0, dScaleY: 1.2, easing: 'ease-in' }, duration * 0.35)
  await this.tween({ dScaleY: 0.7, easing: 'ease' }, duration * 0.1)
  await this.tween({ dScaleY: 1.0, easing: 'ease' }, duration * 0.1)
}
```

ジャンプの前に「溜め」を作ったり、ジャンプ中に体を縦に引き伸ばしたりすることで一枚絵でもぽよんとした可愛らしい動きを作ることができます :relaxed:
![3_jump2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/6f14c862-f7ec-2ded-400c-4f2a5c26a22c.gif)

ここまでのまとめとして、コンポーネント全体を一度貼っておきます。
ついでに`jump`に加えて`walk`メソッドも追加してみました。

```vue:Tama.vue
<template>
  <img class="tama-root" src="/img/tama.svg" alt="タマさん"
    :style="{
      transform: `translate(${x + dx}px, ${y + dy}px) scale(${scaleX * dScaleX}, ${scaleY * dScaleY}) rotate(${rotate + dRotate}deg)`,
      transition: `transform ${duration}ms ${easing}`
    }"
    @click="jump(200)"
  >
</template>

<style lang="scss" scoped>
.tama-root {
  position: absolute;
  left: 0;
  top: 0;
  margin: -300px auto auto -90px;
  transform-origin: 90px 100%;
  will-change: transform;
}
</style>

<script>
import Time from '@/core/Time'
export default {
  name: 'Tama',
  props: {
    x: { type: Number, default: 200 },
    y: { type: Number, default: 100 },
    scaleX: { type: Number, default: 1.0 },
    scaleY: { type: Number, default: 1.0 },
    rotate: { type: Number, default: 0 }
  },
  data () {
    return {
      dx: 0,
      dy: 0,
      dScaleX: 1.0,
      dScaleY: 1.0,
      dRotate: 0,
      duration: 1000,
      easing: 'ease'
    }
  },
  methods: {
    async tween (props = {}, duration = 1000) {
      Object.assign(this.$data, props)
      this.$data.duration = duration
      await Time.wait(duration)
    },
    async jump (height = 200, duration = 2500) {
      await this.tween({ dScaleY: 0.8, easing: 'ease' }, duration * 0.1)
      await this.tween({ dy: -height, dScaleY: 1.1, easing: 'ease-out' }, duration * 0.35)
      await this.tween({ dy: 0, dScale: 1.2, easing: 'ease-in' }, duration * 0.35)
      await this.tween({ dScaleY: 0.7, easing: 'ease' }, duration * 0.1)
      await this.tween({ dScaleY: 1.0, easing: 'ease' }, duration * 0.1)
    },
    async walk (step = 100, duration = 500) {
      await this.tween({ dRotate: 10, dScaleY: 0.8, easing: 'ease' }, duration * 0.2)
      await this.tween({ dx: this.dx + step, dy: -step * 0.2, dRotate: -5, dScaleY: 1.1, easing: 'cubic-bezier(.04,.67,.52,1)' }, duration * 0.7)
      await this.tween({ dy: 0, dRotate: 0, dScaleY: 1, easing: 'ease' }, duration * 0.1)
    }
  }
}
</script>
```

### アニメーションを構造化する

ここまでで、複数のキーフレームで構成される複雑なアニメーションを`jump`や`walk`といった抽象化されたメソットで呼び出せるようになりました。最後に、これらのアニメーションを組み合わせてさらに複雑な動きを作ります。

`App.vue`にボタンを追加します。このボタンクリックでタマさんに`jump`と`walk`を組み合わせた一連のアニメーションを演じてもらいましょう。

```vue:App.vue
<template>
  <div id="app">
    <button @click="play">Play</button>
    <tama ref="tama" :x="100" :y="300" :scaleX="0.5" :scaleY="0.5"></tama>
  </div>
</template>
```

ボタンクリック時の`play`メソッドの中身はこんな感じです：

```js:App.vue
async play () {
  const tama = this.$refs.tama
  await tama.jump(100, 1500)
  await tama.walk(100, 1200)
  await tama.walk(60, 600)
  await tama.walk(40, 400)
  await tama.jump(200, 2500)
}
```

小さくジャンプして→3歩歩いて→最後に大きくジャンプする、という一連の動きをとてもシンプルに表現できているのがわかると思います。今回はタマさん一人だけですが、複数のコンポーネントを組み合わせたり入れ子にしたりすることで、ゲームのような複雑な動きも構造化していくことができるはずです。

## まとめ

* CSS transformの座標をVueコンポーネントのプロパティと連動させることで、画像（SVG）を自由な位置・大きさ・角度で簡単に配置できるよ
* CSS transitionを使えば位置・大きさ・角度の変更をアニメーションにできるよ
* async/awaitを使えばまるでTweenライブラリを使っているかのように、複雑なアニメーションを自然に書けるよ