GSAPとVue 3でアニメーションを作成する方法

March 03, 2022
執筆者
レビュー担当者

GSAPとVue 3でアニメーションを作成する方法

背景

Vue.jsは世界中で使われる、パワフルなフロントエンドフレームワークです。直近ではVue 3もリリースされました。軽量化やTypeScriptのサポート強化、Composition APIの導入などが行われ、さらに手軽に、効率的なアプリケーションを構築できるようになりました。

そんなVue.jsのアプリケーションのユーザー体験や印象を向上してくれるのが、アニメーションです。Green Sock Animation Platform(通称GSAP)は、高性能にも関わらず、手軽に使えるJavaScriptのアニメーションツールです。本稿では、Vue 3とGSAPを使って、アニメーションを作成する方法をご紹介します。

目標

このチュートリアルを最後まで進めると、Vue 3のComposition APIを使ったアプリケーションにGSAPでアニメーションを適用する方法を学べます。チュートリアルを通して、以下のアニメーションを作成します。

Vue 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のスターターキットを準備しています。

Vue x GSAPスターターキット

このスターターキットの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コールバックを渡します。

opacity1に設定し、不透明度が徐々に増加し、ふわっとヘッダーが表示される様にします。

durationで、アニメーションの継続時間を1秒間に設定します。y0に設定し、ヘッダーが画面上部から元の位置まで下がってくるように設定します。easeで、イージングの指定をPower0.easeOutに設定し、ヘッダーの動き方を設定します。設定できるイージングは、GSAP公式ドキュメントの「GreenSock Ease Visualizer」で視覚的に確認できます。

最後に、onCompleteアニメーションが完了した時に、doneコールバックが呼び出される様に指定します。

linksBeforeEnterlinksEnterで、ヘッダーに含まれるリンクが、ヘッダーが下がってきた後に、画面にふわっと表示されるアニメーションを追加しています。

ヘッダー部分と設定しているプロパティはほとんど同じですが、大きく異なる点は、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-enterpanelsBeforeEnterメソッド、@enterpanelsEnterメソッドを紐付けています。

次に、<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メソッドで、パネルのアニメーション開始前の設定を行います。 パネルは画面下から出現させるため、panelsBeforeEnteryの値を100にし、パネルの最終位置から100px下に設定します。opacity0に設定します。

panelsEnterは、v-forで生成される要素ごとに1回ずつ実行されます。アニメーションの継続時間をduration0.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%に、不透明度を徐々に上げるためにopacity0に設定します。

textEnterメソッドで、テキストが降りてくるアニメーションを設定します。durationは、テキストを1行につき0.4秒ずらして表示させるため、0.4 x 3行で1.2を設定しています。y0に設定し、テキストがHTMLとCSSで設定している元の位置まで下がってくるようにします。easeは、bounce.outを設定し、テキストが着地時点で飛び跳ねるようにします。

テキストが1行ずつ下がってくるように、delayを設定します。

delayの計算方法は、以下のとおりです。

2(ヘッダーとパネルの継続時間 + 0.2秒の猶予)+ 0.4(各行のdelay) * (lines.value.length(lines配列に含まれる要素の数) - el.dataset.index(各行の要素のindex))

ボールのアニメーションは、ballBeforeEnterballEnterメソッドで定義します。

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)を覗いてみて下さい。