Vue.js(Nuxt.js)でアニメーションやってみたら最高だった話。

Nuxt.jsで自己紹介サイトを作りました。
https://nitta.studio/

見ていただくと分かる通りアニメーションをしまくったのですが、、

VuexとVue.jsのウォッチャをつかって、

  1. イベントハンドリング
  2. ステート変更
  3. ウォッチャで検知
  4. 複数のコンポーネントでアニメーション発火🔥

のような書き方をしたら最高だったので、ご紹介です。

アニメーションって、どこにどの処理書けばいいのか困りませんか?
凝ったものを実装するとめちゃめちゃなコードになりがちですよね...

しかーし!Vue.jsのデータ駆動とよばれる設計のおかげで、アニメーションの制御がとっても綺麗にできます。
(※データ駆動とは、データを中心にDOM描画をおこなったり、アクションを起こしたり、振る舞いを変えることができる設計のこと)

インタラクションが得意なデザイナ・フロントエンドさんには超おすすめですよ!

必要となる前提知識

  • Nuxt.js
  • Vue.js
  • Vuex
  • TweenMax

筆者はいつもPugとStylusを使いますが、
今回はみんな大好きHTMLSCSSでやってみますね!
てかどう考えてもPugStylusが一番いいだろ〜何でみんな嫌がるの全然意味がわからない😩

やること

Nitta.Studioのアニメーションは、解説の題材にしてはちょっと複雑なので、
簡単なサンプルを用意してみました。まずは完成形から。

2018-04-29 17.34.56.gif

  1. ボタンをクリック(イベントハンドリング)
  2. Vuexでストアのステートを変更
  3. TheLeft, TheCenter, TheRight, TheBackgroundコンポーネントのウォッチャで検知
  4. 各コンポーネントでアニメーション🔥

と、このような流れになっています。今回は順にポイントを確認していこうと思います。
サンプルコードは以下から。
https://github.com/soichiro-nitta/qiita-animation

各自cloneするなどして、

$ npm install # Or yarn install
$ npm run dev # Or yarn dev

して開発環境を立ち上げてみましょう。
それでは解説していきまーす! Qiita初投稿でテンションあがってるぜフォォォ!

イベントハンドリング

まず、components/TheButton.vueを見ていきましょう。
ここでは、クリックイベントをトリガーに、ステートの変更をおこないます。
(Vuexのストアの状態を変更するのでミューテーションをコミットしています。)

components/TheButton.vue
<template>
  <div class='TheButton'>
    <div
      class='TheButton_Start'
      @click='click' <!-- メソッドを指定 -->
    >

clickイベントをバインディング。

components/TheButton.vue
<script>
import {mapGetters, mapMutations} from 'vuex'

export default {
  // ...
  methods: {
    // ...
    ...mapMutations({
      click: 'click' // `this.click()`にマッピングされます
    })
  }
}
</script>

mapMutationsヘルパーでミューテーションをマッピング。

これでクリック時にストアのミューテーションが実行されますね。

ここではclickイベントミューテーションをバインドするだけです。
ここにアニメーションのコードは書きません。

ストア

ストアはstore/index.jsで定義しています。

store/index.js
export const state = () => ({
  entered: false // このステートが変更されるとアニメーションが🔥
})

export const getters = {
  entered: state => state.entered // 各コンポーネントのウォッチャで監視するので
}

export const mutations = {
  click (state) {
    state.entered = !state.entered // クリックされたらステートを切り替えます
  }
}

↑ タイプclickのミューテーションは、state.enteredをトグルする形になっています。
これでクリックされたら、state.enteredが切り替わるようになりますね。

あとで説明しますが、各コンポーネントのウォッチャでは、
state.enteredが「trueになったとき」と「falseになったとき」で書き分けをしていきます。

state.enteredはウォッチャで監視するので、ゲッターも定義しておきます。

ウォッチャ

ここからが本番!
各コンポーネントのウォッチャでステートの変更を検知して、アニメーションを発火します🔥
ここではTheLeftコンポーネントのウォッチャ(watchオプション)を見てみましょう。

components/TheLeft.vue
<script>
import {mapGetters} from 'vuex'
import {TweenMax, Expo, Elastic} from 'gsap'

export default {
  // ...
  watch: {
    entered (val) { // ステートの`entered`が切り替わるたび、この処理が実行される
      this.flash() // アニメーション🔥
      val ? this.enter() : this.leave() // `entered`の値によってアニメーションを書き分け🔥
    }
  },
  methods: { // アニメーションの宣言はここ
    flash () {
      requestAnimationFrame(() => {
        TweenMax.to(this.$refs.title, 0.05, { // `this.$refs`でDOMにアクセス
          color: 'red',
          scale: 1.3,
          ease: Expo.easeIn,
          repeat: 19,
          yoyo: true
        })
      })
    },
    enter () { // `entered`が`true`になったとき発火
      requestAnimationFrame(() => {
        TweenMax.to(this.$refs.background, 1, {
          scaleX: 1,
          ease: Expo.easeOut
        })
      })
    },
    leave () { // `entered`が`false`になったとき発火
      requestAnimationFrame(() => {
        TweenMax.to(this.$refs.background, 1, {
          scaleX: 0,
          ease: Expo.easeOut
        })
      })
    }
  }
}
</script>

↑ どうでしょう。綺麗じゃないですか?
ストアのenteredが切り替わるたびに、watchプロパティで宣言した処理が実行されます。

筆者のおすすめは、
watchオプションにはロジックだけ書くようにして、
実際のアニメーションのコードはmethodsオプションに定義する形です。

ウォッチャに書くのはシンプルなロジックだけ!と決めておくと、汚れなくてよいと思います。

他のTheCenter, TheRight, TheBackgroundコンポーネントも同様の流れです。
要は、動かしたい対象のDOMがあるコンポーネントに、そのアニメーションも書くという設計です。
どのアニメーションがどこにあるのか、わかりやすくてイイですね!

これで今回のように、イベントハンドリングするDOMと、アニメーションしたいDOMが別コンポーネントでも、
Vuexとウォッチャのおかげで役割をハッキリさせることができました。

おまけ

async/awaitを使ったsetTimeout()の筆者オレオレmixinをご紹介。

まずは通常のsetTimeout()、こんな感じですよね?

<script>
export default {
  // ...
  watch: {
    isHell () {
      setTimeout(() => {
        this.hell1()
        setTimeout(() => {
          this.hell2()
          setTimeout(() => {
            this.hell3()
            setTimeout(() => {
              this.hell4()
              setTimeout(() => {
                this.hell5()
              }, 500)
            }, 400)
          }, 300)
        }, 200)
      }, 100)
    }
  },
  // ...
}
</script>

ここは地獄か😇?

次に、mixinを使用したコードを見てみましょう。

<script>
export default {
  // ...
  watch: {
    async isHeaven () {
      await this.$delay(100)
      this.heaven1()
      await this.$delay(200)
      this.heaven2()
      await this.$delay(300)
      this.heaven3()
      await this.$delay(400)
      this.heaven4()
      await this.$delay(500)
      this.heaven5()
    }
  },
  // ...
}
</script>

神じゃないですか?😻

plugins/mixin.jsにメソッドの詳細がありますので、見てみましょう。

plugins/mixin.js
import Vue from 'vue'

Vue.mixin({
  methods: {
    $delay (ms) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }
  }
})

どうですか? これでコールバック地獄卒業しましょう。
(今回紹介しませんでしたが、TheCenter, TheRight, TheBackgroundコンポーネントでも使用しています。)

おわりに

いかがでしたでしょうか?

筆者は他のJSフレームワークも経験がありますが、
イベントハンドリング → アニメーション発火の設計には悩む場面が多く、
アニメーションどこに定義すべきか、DOMへのアクセスどうしよう、と試行錯誤の日々でした。
結局、自分ルールを作っても、できたアニメーションのコードはひどく読みづらいものでした。

しかしVue.jsを試してみると、Vuexとウォッチャのおかげで、
ロジックと、実際のアニメーションのコードを、スッキリと分けることができました!
当面はこの形に落ち着きそうだなと思っています。

また、今回はアニメーション制御の解説でしたが、
Vue初心者には、イベントハンドリングやウォッチャの使い方、Vuex、メソッド定義の設計など、いい感じに学べて、かつ視覚的にも理解がすすみそうなので、入門によい題材なのではと思います。

Vue.jsはインタラクションが得意なデザイナ・フロントエンドさんには本当にオススメなので、
ぜひ題材にしていただいて、Vue使いデザイナー・フロントエンドが増えてくれれば筆者は嬉しいです〜😸

(またTheButtonコンポーネントでは、今回トランジションを使用しましたが解説を省略しています。Vue.jsのアニメーションの真髄は、まだまだいっぱいあります。もしご興味のある方は公式ガイドでわかりやすく記されていますので、ご覧になられてはいかがでしょうか。)

1contribution

新田さんのサイト、アニメーションがとても綺麗だったのでどのように実装されているかすごく興味がありました。ノウハウのご共有ありがとうございます。参考になります!

184contribution

@isheik オオッ、そういっていただけて嬉しいです。まとめた甲斐がありました!笑

なんだこりゃ最高か:relaxed:
Vue.jsとnuxt.jsを勉強したくなりましたありがとうございます:angel:

184contribution

@kosuke_takahara ヤッター嬉しいです! ご一緒にVue沼へハマりましょう〜😸

15contribution

最高にcoolですね!自分もVuejsに挑戦しようと思っていたところだったので、非常にココロオドリます!参考にさせていただきます!

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.