背景
Vue.jsは世界中で使われる、パワフルなフロントエンドフレームワークです。直近ではVue 3もリリースされました。軽量化やTypeScriptのサポート強化、Composition APIの導入などが行われ、さらに手軽に、効率的なアプリケーションを構築できるようになりました。
そんなVue.jsのアプリケーションのユーザー体験や印象を向上してくれるのが、アニメーションです。Green Sock Animation Platform(通称GSAP)は、高性能にも関わらず、手軽に使えるJavaScriptのアニメーションツールです。本稿では、Vue 3とGSAPを使って、アニメーションを作成する方法をご紹介します。
目標
このチュートリアルを最後まで進めると、Vue 3のComposition APIを使ったアプリケーションにGSAPでアニメーションを適用する方法を学べます。チュートリアルを通して、以下のアニメーションを作成します。
適用するアニメーションは以下のとおりです。
- ヘッダーが画面上から中央に向かって下がってきて、時間差でリンクがフェードインする。
- パネルが画面下から中央へ上がってくる。
- 「Vue Meets GSAP」テキストが画面上から中央に向かって、1行ずつ下がってくる。
- ボールが画面の上から下がってきて、画面右に跳ねた後、テキストに向かってスライドする。
GSAPとは?
GSAPは、ウェブ上でアニメーションを作成するためのJavaScriptツールセットです。ウェブブラウザ上で表示できる要素は、すべてGSAPでアニメーション化できます。数行のJavaScriptコードを記述するだけで、要素がどのように、どのタイミングで動くのかを定義できます。CSSのみだと数十、数百行のコードが必要なアニメーションを、GSAPでは数行で実現できてしまいます。
特にVue.jsとGSAPは相性が良いと言われています。Vue.jsにはデフォルトでアニメーション専用のJavaScriptフックが装備されています。このフックにGSAPを組み合わせることで、アニメーションが適用されるタイミングを確実にコントロールできます。
ページの構造
本稿で作成するページの構造をご紹介します。
Header.vue
: ページのヘッダー部分。ページタイトルとリンクを含む。Banner.vue
: ページの中央部分。テキストと画像を含む。Panels.vue
: ページの下部に表示するパネル部分。
必要事項
- バージョン12以降のNode.js。
- Vue 3の基礎知識。
開発の準備
このチュートリアルでは、アニメーションの適用方法に集中できるよう、以下のVue.jsのスターターキットを準備しています。
このスターターキットのmain
ブランチには、アニメーションを追加していない、CSSのみでスタイリングをしたVue.jsプロジェクトが格納されています。このチュートリアルでは、main
ブランチから開発を進めます。
complete-app
ブランチには、アニメーション追加後の、完成形のVue.jsプロジェクトが格納されています。チュートリアルの途中でコードを確認したい場合は、complete-app
ブランチを参照してください。
スターターキットをクローンします。ターミナルで、任意のディレクトリで、以下のコマンドを実行してください。
git clone https://github.com/smwilk/vue-gsap.git
次に、以下のコマンドでプロジェクトディレクトリに移動し、必要な依存パッケージをインストールします。
cd vue-gsap
npm install
これで、開発の準備ができました。以下のコマンドで、開発用サーバーを立ち上げます。
npm run serve
ブラウザでhttp://localhost:8080/にアクセスすると、以下のような画面が表示されます。この時点では、アニメーションはなく、すべての要素が静止状態です。
これで、アニメーションを追加する準備が出来ました。一旦ターミナルでプロセスを終了させます。
GSAPをインストールする
次に、GSAPをインストールします。ターミナルで、以下を実行します。
npm install gsap
単一要素にアニメーションを追加する
ヘッダー部分のアニメーション
ヘッダーが画面上から中央に向かって下がってきて、時間差でリンクがフェードインするアニメーションを追加します。
エディタでsrc/componentsフォルダのHeader.vueコンポーネントを開きます。
HTMLテンプレートの、<!--ここにtransitionを追加-->
から<!--ここでtransitionをクローズ-->
コメントまでを、以下のように置き換えます。
<div class="header">
<!--ここからtransitionコードを開始-->
<!--ヘッダーのアニメーション-->
<transition
appear
@before-enter="headerBeforeEnter"
@enter="headerEnter"
>
<div class="header-container">
<div class="header-title">
<span class="header-title-text">Vue x GSAP</span>
</div>
<!--リンクのアニメーション-->
<transition
appear
@before-enter="linksBeforeEnter"
@enter="linksEnter"
>
<div class="links-container">
<div class="links">
<a href="#">Link 1</a>
<a href="#">Link 2</a>
<a href="#">Link 3</a>
</div>
</div>
</transition>
</div>
</transition>
<!--ここでtransitionをクローズ-->
</div>
Vue.jsのアニメーションは、ビルトインの<transition>
コンポーネントを使って導入します。この<transition>
コンポーネントにアニメーションの設定を属性として追加できます。
Header
コンポーネントには、2つの<transition>
を追加しています。
1つ目の<transition>
は、Webページのヘッダーが、画面上から不透明度を高めながら下がってくるアニメーションです。<transition>
に、appear
、@before-enter
、@enter
属性を追加しています。
appear
属性は、初期レンダリング時にトランジションを適用するために追加しています。@before-enter
と@enter
は、アニメーションのサイクルに応じて定義したメソッドを呼び出すフックです。@before-enter
は、要素が追加され、アニメーションが実行される直前にheaderBeforeEnter
メソッドを呼び出します。@enter
は、要素が追加され、アニメーションが実行される時headerEnter
にメソッドを呼び出します。各メソッドは、後ほど定義します。
Vue.jsのトランジションで使用できるその他のフックについて詳しくは、Vue.js公式ドキュメントのJavaScriptフックを参照してください。
2つ目のtransition
は、ヘッダーが下がってきた後に、リンクが次々とフェードインし、表示されるアニメーションです。1つ目のアニメーションと同様、appear
、@before-enter
と@enter
を属性として追加します。
次に、<script>
の内容を、以下のように置き換えます。
import gsap from "gsap"
export default {
setup() {
// ヘッダーが上から下がってくるアニメーション
const headerBeforeEnter = (el) => {
gsap.set(el, {
y: "-100%",
opacity: 0
})
}
const headerEnter = (el, done) => {
gsap.to(el, {
opacity: 1,
duration: 1,
y: "0",
ease: "Power0.easeOut",
onComplete: done
})
}
// リンクがフェイドインするアニメーション
const linksBeforeEnter = (el) => {
el.style.opacity = 0
}
const linksEnter = (el, done) => {
gsap.to(el, {
duration: 1,
opacity: 1,
delay: 1,
onComplete: done
})
}
return { headerBeforeEnter, linksBeforeEnter, headerEnter, linksEnter }
}
}
上記のコードを上から順に解説します。
まず、インストールしたGSAPを、import gsap from “gsap”
でコンポーネントにインポートします。
次に、Vue 3のComposition APIで使用される、コンポーネントのロジックを1つの関数にまとめることのできるsetup()
関数に、それぞれのトランジションのフックで呼び出すメソッドを定義します。
headerBeforeEnter
メソッドでは、el
引数を通してアニメーションを設定する要素を受け取ります。headerBeforeEnter
は、ヘッダー要素が追加され、アニメーションが実行される直前の設定を追加します。
GSAPでは、「Tween」と呼ばれる、アニメーションのプロパティを設定するインスタンスを作成します。Tweenにアニメーションを設定するターゲットを紐付け、アニメーションの適用期間やCSSのプロパティを定義します。
gsap.set()
メソッドを使用して、アニメーションが開始する直前のヘッダーの表示を設定します。translateY
のショートカットであるy
プロパティで、ヘッダーのy軸の位置を、ヘッダーが画面から完全に見えなくなる位置(-100%
)に配置します。ヘッダーが下がってくるタイミングで、突然ヘッダーが出現するのではなく、不透明度が徐々に増加するよう、ヘッダーのopacity
プロパティを0
に設定します。
headerEnter
メソッドで、ヘッダーが下がってくるアニメーションを設定します。gsap.to()
メソッドで、アニメーションのゴールを設定します。
headerEnter
には、el
だけでなく、アニメーションが完了した際に呼び出すdone
コールバックを渡します。
opacity
を1
に設定し、不透明度が徐々に増加し、ふわっとヘッダーが表示される様にします。
duration
で、アニメーションの継続時間を1秒間に設定します。y
は0
に設定し、ヘッダーが画面上部から元の位置まで下がってくるように設定します。ease
で、イージングの指定をPower0.easeOut
に設定し、ヘッダーの動き方を設定します。設定できるイージングは、GSAP公式ドキュメントの「GreenSock Ease Visualizer」で視覚的に確認できます。
最後に、onComplete
アニメーションが完了した時に、done
コールバックが呼び出される様に指定します。
linksBeforeEnter
とlinksEnter
で、ヘッダーに含まれるリンクが、ヘッダーが下がってきた後に、画面にふわっと表示されるアニメーションを追加しています。
ヘッダー部分と設定しているプロパティはほとんど同じですが、大きく異なる点は、delay
を設定しているところです。ヘッダーが下がってきてからリンクを表示するために、ヘッダーのduration
である1秒間、アニメーションの開始を遅らせています。
最後に、return
で作成したメソッドをオブジェクトとして返します。
これで、ヘッダー部分のアニメーションの準備が出来ました。
この時点で、Header.vue
のコードの全貌を確認したい場合は、Githubを参照してください。
ファイルを保存し、ターミナルで、npm run serve
を実行し、ブラウザでhttp://localhost:8080/にアクセスします。
以下のように、ヘッダーのアニメーションが適用されています。
ここで、再度プロセスを終了しておきます。
リストにアニメーションを追加する
パネル部分のアニメーション
次に、パネル部分のアニメーションを追加します。
エディタでsrc/componentsフォルダのPanels.vueコンポーネントを開きます。
HTMLテンプレートの、<!--ここにtransitionを追加-->
から<!--ここでtransitionをクローズ-->
コメントまでを、以下のように置き換えます。
<div class="panels">
<!--ここからtransitionコードを開始-->
<transition-group
tag="div"
class="panels-grid"
appear
@before-enter="panelsBeforeEnter"
@enter="panelsEnter"
>
<div
v-for="(panel, index) in panels"
:key="panel.name"
class="panel"
:data-index="index"
>
<div>{{ panel.name }}</div>
</div>
</transition-group>
<!--ここでtransitionをクローズ-->
</div>
ここでは、画面下部から1つずつパネルが出現するアニメーションを適用します。パネルは、v-for
を使って、リストとして表示させています。Vue.jsでは、リスト全体にアニメーションを適用する時は、<transition-group>
コンポーネントを使用します。
<transition-group>
は、要素としてレンダリングされません。その代わりに、tag
属性にレンダリング要素を指定できます。ここでは、div
を要素として指定します。
また、<transition-group>
の内部には、固有のkey
属性を指定する必要があります。ここでは、パネルのname
プロパティを指定しています。
ヘッダー同様、@before-enter
にpanelsBeforeEnter
メソッド、@enter
にpanelsEnter
メソッドを紐付けています。
次に、<script>
の内容を、次のように置き換えます。
import { ref } from "vue"
import gsap from "gsap"
export default {
setup() {
// 表示するパネルの設定
const panels = ref([
{ name: "Panel 1" },
{ name: "Panel 2" },
{ name: "Panel 3" },
{ name: "Panel 4" }
])
// パネルのスタート設定
const panelsBeforeEnter = (el) => {
gsap.set(el, {
y: 100,
opacity: 0
})
}
// パネルのアニメーション設定
const panelsEnter = (el, done) => {
gsap.to(el, {
opacity: 1,
y: 0,
duration: 0.8,
delay: 1 + el.dataset.index * 0.2,
onComplete: done
})
}
return { panels, panelsBeforeEnter, panelsEnter }
}
}
上記のコードを上から順に解説します。
panelsBeforeEnter
メソッドで、パネルのアニメーション開始前の設定を行います。 パネルは画面下から出現させるため、panelsBeforeEnter
でy
の値を100
にし、パネルの最終位置から100px下に設定します。opacity
は0
に設定します。
panelsEnter
は、v-for
で生成される要素ごとに1回ずつ実行されます。アニメーションの継続時間をduration
で0.8
に設定します。
パネルは全て同時に表示させるのではなく、1つずつ表示させるために、delay
を設定します。delay
の値は、<template>
で設定した:data-index=“index”
を通して、要素のdataset
に含まれるindex
を取得して設定します。
パネルのdelay
の計算方法は、以下のとおりです。
1(ヘッダーのアニメーションの継続時間) + el.dataset.index(パネルのindex
) * 0.2(各パネルのdelay
)
ヘッダーが画面に表示されてからパネルのアニメーションが開始するように、ヘッダーのアニメーションの継続時間である1秒を追加します。加えて、パネルのindex
に0.2秒を乗算し、index
が小さいパネルから表示されるようにします。
これで、パネル部分のアニメーションの準備が出来ました。
この時点で、Panels.vue
のコードの全貌を確認する場合は、Githubを参照してください。
ファイルを保存し、ターミナルで、npm run serve
を実行し、ブラウザでhttp://localhost:8080/にアクセスします。
以下のように、パネルのアニメーションが適用されています。
再度、プロセスを終了させておきます。
複雑なアニメーションを追加する
バナー部分のアニメーション
次に、バナー部分に、テキストが上から降ってきて、ボールが上から画面右に飛び跳ねるアニメーションを追加します。
エディタでsrc/componentsフォルダのBanner.vueコンポーネントを開きます。
HTMLテンプレートの、<!--ここにtransitionを追加-->
から<!--ここでtransitionをクローズ-->
コメントまでを、以下のように置き換えます。
<div class="banner">
<!--ここにtransitionを追加-->
<transition-group
tag="div"
appear
@before-enter="textBeforeEnter"
@enter="textEnter"
class="text-container"
>
<div
v-for="(line, index) in lines"
:key="line.phrase"
class="text"
:data-index="index"
>
<div>
{{ line.phrase }}
</div>
</div>
</transition-group>
<div class="shape-container">
<transition
appear
@before-enter="ballBeforeEnter"
@enter="ballEnter"
>
<img class="circle" src="../assets/circle.svg" alt="Circle"/>
</transition>
</div>
<!--ここでtransitionをクローズ-->
</div>
ここでは、テキスト部分に<transition-group>
を、ボール部分に<transition>
を使用しています。
次に、<script>
の内容を、次のように置き換えます。
import { ref } from "vue"
import gsap from "gsap"
export default {
setup() {
// 表示するテキストを設定
const lines = ref([
{ phrase: "Vue" },
{ phrase: "Meets" },
{ phrase: "GSAP" },
])
// テキストのスタート設定
const textBeforeEnter = (el) => {
gsap.set(el, {
y: "-100%",
opacity: 0
})
}
// テキストが上から下へ移動するアニメーション
const textEnter = (el, done) => {
gsap.to(el, {
opacity: 1,
duration: 1.2,
y: "0",
ease: "bounce.out",
// テキストを0.4秒ずつずらして上から下へ移動
delay: 2 + 0.4 * (lines.value.length - el.dataset.index),
onComplete: done,
})
}
// ボールが上から下へ移動するアニメーションのスタート位置設定
const ballBeforeEnter = (el) => {
gsap.set(el, {
y: "-150%"
})
}
// ボールが上から下がって跳ねるアニメーション
const ballEnter = (el, done) => {
const tl = gsap.timeline( { delay: 5, onComplete: done } ) // アニメーションのタイムライン
const screenWidth = window.innerWidth // 画面の横幅
const elementWidth = document.querySelector(".circle").getBoundingClientRect().right // ボールの横幅
// ボールのアニメーションの設定
tl
.to(el, { y: 350 } ) // y軸350pxの位置に落ちる
.to(el, { y: 0, duration: 0.5 } ) // 0.5秒間でy軸0pxの位置まで戻る
.to(el, { y: 350, duration: 1.25, ease: "bounce.out" }) // 1.25秒間でy軸350pxの位置に跳ねながら戻る
.to(el, { x: screenWidth - elementWidth - 10, duration: 2.5 }, "-=1.75") // 前のアニメーションと1.75秒重複しながら、2.5秒間でx軸の画面の端まで跳ねながら移動する
.to(el, { x: 0, duration: 1 }, "+=1") // 前のアニメーション完了から1秒後に、1秒間でx軸0pxまで戻る
}
return { lines, textEnter, textBeforeEnter, ballBeforeEnter, ballEnter }
}
}
上記のコードを上から順に解説します。
まず、テキストが上から1行ずつ下がってくるアニメーションを設定しています。
textBeforeEnter
メソッドで、gsap.set
を使って、テキストのアニメーションが開始する前のスタイルを定義します。テキストが画面上から完全に隠れるように、y
を-100%
に、不透明度を徐々に上げるためにopacity
を0
に設定します。
textEnter
メソッドで、テキストが降りてくるアニメーションを設定します。duration
は、テキストを1行につき0.4秒ずらして表示させるため、0.4 x 3行で1.2
を設定しています。y
を0
に設定し、テキストがHTMLとCSSで設定している元の位置まで下がってくるようにします。ease
は、bounce.out
を設定し、テキストが着地時点で飛び跳ねるようにします。
テキストが1行ずつ下がってくるように、delay
を設定します。
delay
の計算方法は、以下のとおりです。
2(ヘッダーとパネルの継続時間 + 0.2秒の猶予)+ 0.4(各行のdelay
) * (lines.value.length(lines
配列に含まれる要素の数) - el.dataset.index(各行の要素のindex
))
ボールのアニメーションは、ballBeforeEnter
とballEnter
メソッドで定義します。
ballBeforeEnter
で、アニメーションが開始する前のボールの位置を-150%
に設定します。
ballEnter
で、ボールが画面の上から下がってきて、画面右側に跳ね、テキスト側まで戻ってくるアニメーションの設定をします。
ボールのアニメーションは、gsap.timeline()
で作成します。gsap.timeline()
は、長く複雑なアニメーションを作成したいときに便利な、アニメーションの順序を設定するツールです。
const tl = gsap.timeline( { delay: 5, onComplete: done } )
で、タイムラインを作成し、タイムラインインスタンスを作成します。delay: 5
でアニメーションの開始を5秒ずらし、ヘッダーなど他の要素のアニメーションが終わった後にアニメーションを開始するようにします。onComplete: done
でアニメーションが完了時に呼び出すdone
コールバックを設定します。
tl
インスタンスでto
メソッドを設定して、ボールの動きを細かく指定します。設定方法はgsap.to
と同じです。タイムラインに含まれる各アニメーションの詳細は、上記のコードにコメントとして記載しています。
最後に、<style>
の.circle
クラスの /** アニメーション適用後に-1.2remに編集 */
コメントの行のtop
の値を以下のように20rem
から-1.2rem
に変更し、静止用に固定していたボールの位置を変更します。
.circle {
position: absolute;
width: 9rem;
height: 9rem;
top: -1.2rem;
}
この時点で、Banner.vue
のコードの全貌を確認したい場合は、Githubを参照してください。
これで、すべてのアニメーションの準備が出来ました。ファイルを保存し、ターミナルで、npm run serve
を実行し、ブラウザでhttp://localhost:8080/にアクセスします。
以下のように、すべてのアニメーションが適用されています。
これで、アニメーションが完成しました。
最後に
GSAPは、CSSだけでは実現が難しかったり、複雑化してしまうようなアニメーションを数行のコードで実装を可能にする便利なツールです。GSAPはVue.jsのトランジションフックと相性が良く、簡単に、そして確実にアニメーションを実装できます。さらに簡潔化が進み便利になったVue 3と組み合わせれば、コード量を小さく抑えながら、ユーザーの目を惹きつけるアニメーションを気軽に導入できます。
このチュートリアルを基に、ぜひあなたのプロジェクトにもアニメーションを追加してみてください。
フィードバック、登壇、勉強会のお誘いなど気軽にsnakajima[at]twilio.comまでご連絡ください。開発中のプロジェクトに関してはGithub(smwilk)を覗いてみて下さい。