こんにちは。UX&フロントエンジニアしながら絵描きして遊んでいる**ゆき(@yuneco)**です。この記事ではVue.jsを使ってCSSアニメーションを使った表現を自在に行うための基本的な部分をステップバイステップで解説します。目標は↓以下のようなアニメーションをJavaScriptで自在に構築できるようになることです。
ソースコードはこちら: https://github.com/yuneco/css-anime-tutorial
目次
この記事では最初にSVGを単純に表示するところから始め、Vueのコンポーネントを利用してそのSVGを自由に配置・変形させる方法を説明します。その上でCSS transitionを用いたアニメーションを取り入れます。最後に、複雑なアニメーションを抽象化・構造化してより複雑なシーンを構成するための方法を解説します。
- SVGを作る
- Vueプロジェクトを作る
- SVGを表示する
- 好きな場所に配置する
- 大きさ・角度を自由に変える
- アニメーションさせる
- 連続して動かす(キーフレームアニメーション)
- アニメーションの抽象化と構造化
おことわり
- この記事で解説する方法は必ずしもアニメーションを構築する際のスタンダードな方法ではありません
- 楽に複雑なアニメーションを作りたい場合、animejsやPixiJS等、高度なライブラリの導入・学習を検討してください
- この記事ではアニメーション専用のライブラリを用いずに自力でアニメーション構築までたどり着くことでVue.jsやJavaScript、CSSアニメーション等に関するより深い理解を得ることを狙っています
...若干言い訳っぽくもありますが、単純に「自分で仕組みを理解してアニメーションを作れる」スキルを身に付けるのは間違いなく楽しいことです。少々長い記事になりますがお付き合いいただければ幸いです
SVGを作る
まず最初にこのチュートリアルで使うSVGを作ります。Illustratorで好きなキャラクターを作成し、メニューから[ファイル]>[書き出し]>[スクリーン用に書き出し]を選択します。「形式」をSVGに変更し、右側の歯車アイコンから設定を表示します。

...ちょっと小難しい設定が出てきました。でも今回は「SVGを使って〜」と言いながらもSVGタグ自体をVue.jsでゴリゴリするわけではないのでここの設定はさして気にしなくて大丈夫です。右下の「レスポンシブ」チェックだけ外しておいてください。1
設定ができたら「設定を保存」>「アートボードを書き出し」でSVGファイルを出力します。Illustratorがない方は別なファイルでももちろんOKです。面倒な場合はひとまずgithubにファイルを置いたので使ってください。
ブラウザにドロップすると、こんな感じで表示されると思います。名前は**「タマさん」**です。今決めました。(今回はわかりやすくするため、1pxの青線でボーダーを入れています)
Vueプロジェクトを作る
なにはともあれVueのプロジェクトを作らないと始まりません。vue create プロジェクト名
でプロジェクトを作成します。この記事では下記の構成で進めますが、もちろん好みに応じて変えていただいても大丈夫です。2
? 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
ディレクトリに新しくファイルを作成し、以下を記述します:
<template>
<img src="/img/tama.svg" alt="タマさん">
</template>
このチュートリアルでは一番簡単な方法...ということでimgタグで読み込みをします。
他にもSVGを表示する方法として、
-
<svg>
タグで直接記述する -
cssのbackground-image
で画像として読み込む - vue-svg-loader等を利用してVueのコンポーネントとして読み込む
といった方法があります。特に1はより自由度の高い表現(SVGの中の一部分の色や形をVueで制御する、等)ができるので、より複雑な表現を追求したい方はぜひチャレンジしてみてください。
シンプルですがTama
コンポーネントができたのでこれを表示します。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の背景にグリッド画像を表示しました。
好きな場所に配置する
とりあえず画面に絵はでましたが、好きな場所に自由に表示できないと絵作りができませんよね。次はこのタマさんを思った場所に配置できるようにしていきます。
指定の座標に配置する
ひとまず、x=200px, y=100px
の位置に置いてみましょう。
<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)
の位置にタマさんがいらっしゃいました!

・・・いや、ちょっと待って (200px, 100px)
の位置にタマさんを立たせたかったのに、基準が左上になってしまっています。もちろん絵にもよりますが、このタマさんのようなキャラクターであれば足元を基準に配置したいですよね。
やりかたはいくつかありそうですが、今回は単純にmargin
で調整します。
.tama-root {
// ...
margin: -300px auto auto -90px;
}

これで無事、(200px, 100px)
の位置にタマさんがたちました!(見切れてるけど!)
配置する座標をパラメータで制御できるようにする
ひとまず決め打ちの場所に配置することはできましたが、実際に使うときはこの座標を動的に変更したりアニメーションさせたりしたいですよね?よってCSSの中にハードコードするわけにはいきません。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
を削除し....
.tama-root {
// ...
margin: -300px auto auto -90px;
// 削除: transform: translate(200px, 100px);
}
代わりにテンプレート内でプロパティを使用してスタイルを指定します:
<template>
<img class="tama-root" src="/img/tama.svg" alt="タマさん"
:style="{
transform: `translate(${x}px, ${y}px)`
}"
>
</template>
これで呼び出し側(App.vue
)から好きな位置を指定してタマさんを立たせることができるようになりした。
<div id="app">
<tama :x="300" :y="400"></tama>
</div>

大きさ・角度を変える
同じようにして、大きさ(スケール)と角度も変えられるようにしましょう。位置・大きさ・角度を自由にコントロールできるようになれば、複数のパーツを組み合わせて好きな画面を作れるようになりますね。
プロパティで大きさ・角度を制御する
まずは位置と同様にプロパティの追加からです。スケールは縦と横を別々に設定できるように、scaleX
とscaleY
の2つのプロパティにしておきます。
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
には角度の単位が必要なので忘れずに。
<img class="tama-root" src="/img/tama.svg" alt="タマさん"
:style="{
transform: `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`
}"
>
最後にこれも呼び出し側からプロパティを設定しましょう。
<div id="app">
<tama :x="300" :y="400" :scale-x="1.5" :scale-y="1.5" :rotate="45"></tama>
</div>
確かに大きさと角度は変わりましたが、またしても基準がちょっと変です。このモヤモヤが伝わりますでしょうか...
この問題を解決するには、専用のCSSプロパティtransform-originを使用します。
.tama-root {
// ...
transform-origin: 90px 100%;
}
transform-origin
は%でもpxでも設定できるので、いい感じにタマさんの足元を基準に設定してあげましょう。
アニメーションさせる
やっとかよ...と言われてしまいそうですが、ようやくアニメーションする準備が整いました。まずはシンプルに「クリックすると50px上にジャンプする」アニメーションを作ります。基本的な方針は**「クリックされたらタマさんのyプロパティを-50する」**だけです。
クリックで位置を変更する
アニメーションの実装場所はTama.vue
自身の中と呼び出し側のApp.vue
の2箇所が考えられますが、「タマさん自身にジャンプする機能を持たせる」方が自然なので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づつ上に上がっていきます。
ただ困ったことに、この状態でブラウザのコンソールを見てみると警告がでてしまっています。

y
プロパティは外部(App.vue)で指定できるように公開しているのに、Tama.vue
の内部で変えちゃダメだよ!って警告ですね。(x, y)
はあくまでベースの立ち位置として使い、内部用の相対位置を管理する変数を追加します。
まず、内部用の変数のためにdata
を作成し、変数dx
とdy
を追加します。jump
メソッドで変更する対象もdy
に変えましょう。
<script>
export default {
name: 'Tama',
props: {...},
data () {
return {
dx: 0,
dy: 0
}
},
methods: {
jump (height) {
this.dy -= height
}
}
}
</script>
テンプレート側はベースの立ち位置(x, y) + 相対位置(dx, dy)
で座標を指定します:
<template>
<img class="tama-root" src="/img/tama.svg" alt="タマさん"
:style="{
transform: `translate(${x + dx}px, ${y + dy}px) ...`
}"
@click="jump(50)"
>
</template>
位置の変更をアニメーションにする
クリックで動くようにはなりましたが、まだアニメーションじゃないですね、これ。アニメーションを追加しましょう。アニメーションの方法としては大きく次の2つが考えられます:
-
y
やdy
といった座標を制御する変数をタイマー等で連続的に変化させる - 変数の値は一気に変えてしまって、CSSのアニメーションで補間する
1は自由度が高く複雑なアニメーションの表現や制御ができますが、毎フレーム座標の計算が走るため重くなりがちです。2であれば、変更後の値さえ指定してしまえば、間のフレームはブラウザがいい感じに描画してくれるため、基本的に高速です。
今回はもちろん、2で進めましょう。3
実装はCSSに2行足すだけです。
.tama-root {
// ...
transition: transform 1s ease;
will-change: transform;
}
will-change
はアニメーションを滑らかにするためのおまじないです。(本来は「おまじない」程度の理解で使うのは良くないプロパティなので、詳しくは別な記事:will-changeで目指す60fpsのぬるぬるCSSアニメーションをご参照ください)
これでクリックするたびにヌルっとした動きでタマさんが上にせり上がるようになりました。ひとまず、最初のアニメーション完成です
アニメーションの時間とイージングを制御する
先ほどの例ではアニメーションの時間(1s = 1秒)やイージング(ease)をハードコードしてしまいましたが、これらの設定は当然アニメーションの内容によって変わってきますよね。これも変更可能にしておきましょう。
data
にduration
とeasing
変数を追加し、CSSのtransition
をこの2変数を使って組み立てるよう、テンプレートに追加します。
<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>
連続して動かす(キーフレームアニメーション)
ジャンプなので浮き上がったら地面に戻ってこないといけませんね。アニメーションを連続して実行する方法を考えます。
イメージとしてはこんな感じ:
jump (height) {
this.dy = -height
// アニメーションが終わるまで待つ
this.dy = 0
}
タイマーでアニメーションを連続実行する
アニメーションの終了を待ち合わせるには
- 単純にタイマーで待つ
- CSS transiotionの
transitionend
イベントを拾う
の2つが考えられます。
正攻法は2なのですが、複数のアニメーションを入れ子にした場合の制御が結構難しいので、今回はシンプルに1でいきたいと思います。
jump (height) {
this.dy = -height
this.easing = 'ease-out'
window.setTimeout(() => {
this.dy = 0
this.easing = 'ease-in'
}, this.duration)
}
イージングも設定して、いい感じにふわっとジャンプするようにしてみました。クリックするとバッチリ、ジャンプして元の位置に戻ってきますね!
async/awaitでアニメーションを連続実行する
では、この調子でもっと複雑なアニメーションを...
・・・うん、つらい
JavaScriptそれなりに書かれている方なら、もう先ほどのコードの時点でヤバみを感じていただいていることと思います。そうです、 コールバックヘル です。この先3つ4つのアニメーションをこの方式で入れ子にしていくのは地獄でしかありません。
この地獄から抜け出すために、async/awaitを使います。といっても特別なものではなく、単に標準のタイマーをPromise化するだけです。
export default {
/**
* Promiseを使い指定の時間待機します。
* @param {Number} ms 待機時間(ミリ秒)
* @return {Promise} 引数の指定時間経過後にresolveされます
*/
wait (ms) {
return new Promise(resolve => {
window.setTimeout(resolve, ms)
})
}
}
このあたりは馴染みがないとちょっとわかりづらいと思いますが、このPromise化したタイマーを使うとこんな感じで待ち時間を簡単に挟むことができるようになります:
console.log('このログはすぐ表示される')
await Time.wait(2000) // ここで2秒待つ
console.log('このログは2秒後に表示される')
この記事では Promiseやasync/awaitの説明はしませんが、ひとまず使い方のイメージがつけば大丈夫かと思います。
このTime.js
を使うと、先ほどのつらみコードは以下のように書き直せます:
<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ステップをアニメーションのパート(キーフレーム)ごとに繰り返しています:
- dataの内の変数をいろいろ変更
-
Time.wait
でCSSのアニメーションが終わるのを待つ
この処理を別メソッドに追い出します。
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を使って、引数で渡されたprop
でthis.$data
の変数を上書きします。その上でTime.wait
を使用してduration
ミリ秒待機します。
呼び出し側のjump
はこれを使ってとってもシンプルにアニメーションをかけるようになりましたね。
複雑な動きの実装
tween
メソッドの実装で複数のアニメーション(キーフレーム)を容易につなげることができるようになりました。これを使ってジャンプの動きをもっと洗練させてみます。
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)
}
ジャンプの前に「溜め」を作ったり、ジャンプ中に体を縦に引き伸ばしたりすることで一枚絵でもぽよんとした可愛らしい動きを作ることができます
ここまでのまとめとして、コンポーネント全体を一度貼っておきます。
ついでにjump
に加えてwalk
メソッドも追加してみました。
<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
を組み合わせた一連のアニメーションを演じてもらいましょう。
<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
メソッドの中身はこんな感じです:
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ライブラリを使っているかのように、複雑なアニメーションを自然に書けるよ
-
入れたままでも良いのですが、初期状態でサイズが決まっていた方が扱いやすいのです ↩
-
Vue.jsの基本については説明しないので、不明な場合には適時Vueの入門記事等を参照してください。 ↩
-
過去に記事にした作例だとVueとSVGを使ってシューティングゲーム『ネコ🐱メザシ🐟アタック🌟』を作ったのでソースと解説が1のタイマーを用いるパターンで、VueとFirebaseの基本機能全部使ってぬるぬる動くポートフォリオサイトを作ったのでソースと解説が2のCSSアニメーションのみによるもの(この記事と同じ)です。 ↩