🛵

Vueのアニメーションはカスタムディレクティブで管理しよう

2022/04/24に公開

概要

みなさんWebのスクロールアニメーションは好きですか?
私は見るのは好きですが、都度アニメーション処理を書き直すのがとても面倒っちいと感じます。

クリエイティブ系のWebサイトでよく使われている(気がする)Vue.jsですが、標準機能であるカスタムディレクティブを使ったアニメーション管理がとてもメンテしやすかったので共有します。

この記事でやること/やらないこと

やること

  1. ディレクティブの紹介
  2. カスタムディレクティブの紹介
  3. スクロールディレクティブの実装
  4. 画面内判定の実装
  5. カスタムディレクティブとアニメーション処理の繋ぎ込み

やらないこと

  • vueやgsapの記法説明
  • リッチなアニメーションパターンの紹介

開発環境

Mac Big Sur(v11.6)
Google Chrome ver:100.0.4896.75(Official Build)(x86_64)
Nuxt.js(@nuxt/cli v2.15.8)

ディレクティブ

ディレクティブとは

Vueには標準で v-modelv-show といったディレクティブが搭載されてます。
一部を除いて、これらはJSの式と合わせて使います。
例えば v-if は =(イコール) で結ばれた条件式のtrue, falseによって画面表示を制御できます。

// これは画面に表示されます。
<div v-if="1 + 1 == 2">
    <p>Hello World!</p>
</div>

// これはif以降の式がfalseなので表示されません。
<div v-if="1 + 1 == 5">
    <p>Hello World!</p>
</div>

公式の定義(出典: Vue.js公式サイト)

ディレクティブは v- から始まる特別な属性です。ディレクティブ属性値は、単一の JavaScript 式を期待します(ただし、v-forは例外で、これについては後から説明します)。ディレクティブの仕事は、属性値の式が変化したときに、リアクティブに副作用を DOM に適用することです。イントロダクションで見た例を振り返ってみましょう:

カスタムディレクティブとは

Vueにはスクロール動作や量に対応したディレクティブが存在しません。
そこでv-xxxxのように自前実装したカスタムディレクティブを登録することで更にリッチな表現がつくれます。
https://jp.vuejs.org/v2/guide/custom-directive.html

スクロールディレクティブをつくろう

まず、スクロールの度にスクロール量を出力するディレクティブをつくりましょう。

customDirectives.js
// Vueオブジェクトをインポート
import Vue from "vue";

// 第一引数にディレクティブ名を入れます(v-xxxxのxxxx部分)
// 今回の場合はv-scrollとなります
Vue.directive("scroll", {
  inserted: () => {
    const handleOnScroll = () => console.log(window.scrollY);
    window.addEventListener("scroll", function);
  }
});
sample.vue
<template>
    <div v-scroll></div>
</template>

これでスクロールをトリガーにスクロール量を取れます⏬

Vueインスタンスを使う場合は、dataやmethodsと同じ階にdirectivesを指定します。
Nuxt.jsを使う場合は、cumtomDirectives.jsといったファイルをpluginディレクトリ内に作り、nuxt.config.js内で読込処理を書きます。

画面内判定を取ろう

前項のv-scrollはwindow全体のスクロール量を出力するためUIごとに使い分けられません。
そこで次はv-scrollをつけたUIがwindow全体のどこにいるかを取得しましょう。
カスタムディレクティブをつけた要素はintertedの第1引数から取得できます。

customDirectives.js
Vue.directive("scroll", {
  // 第1引数: elでHTML要素を取得
  inserted: (el) => {
    let handleOnScroll = () => {
      const positionTop = el.getBoundingClientRect().top;
      console.log(positionTop);
    };
    window.addEventListener("scroll", function);
  }
});

これでHTML要素の上端と画面最上端の距離が出力されるようになりました。
これを応用してスクロール時に要素が画面内にいるか判定してみましょう。
画面内判定は

画面下端 < HTML要素の下端 && HTML要素の上端 < 画面上端

で行います。
これをjsで表現すると

customDirectives.js
Vue.directive("scroll", {
  inserted: (el) => {
    let handleOnScroll = () => {
      // 画面内判定がtrueの際に実行
      if (isInScreen(el)) {
        console.log("画面の中にいるよ〜〜");
        window.removeEventListener("scroll", handleOnScroll);
      }
    };
    window.addEventListener("scroll", handleOnScroll);
  },
});
// 画面内判定処理の切り出し
const isInScreen = (el) => {
  const { top: elementTop, bottom: elementBottom } = el.getBoundingClientRect();
  // 画面下端 < HTML要素の下端 && HTML要素の上端 < 画面上端
  return (window.screenTop < elementTop && elementBottom < window.innerHeight);
};

といったところでしょうか。
v-scrollをつけたUIが画面内に入った時に画面の中にいるよ〜〜と表示されるようになりました。

アニメーション処理との繋ぎ込み

スクロール時にUIの画面内判定が取れました。
あとは切り分けたアニメーションとの繋ぎ込みをしましょう。
今回はアニメーションライブラリにgsapを使用します。

まず、bindでHTML要素の透明度を0に設定します
bindはカスタムディレクティブがついたHTML要素がレンダリングされた際に実行される関数です。

customDirectives.js
import Vue from "vue";
import gsap from "gsap";

Vue.directive("animateFadeIn", {
  // 
  bind: (el) => gsap.set(el, {
    opacity: 0,
  }),
  inserted: (el) => {
    let handleOnScroll = () => {
      // 画面内判定がtrueの際に実行
      if (isInScreen(el)) {
        window.removeEventListener("scroll", handleOnScroll);
	//アニメーション関数にHTML要素を引渡し
        animateFadeIn(el);
      }
    };
    window.addEventListener("scroll", handleOnScroll);
  },
});

const isInScreen = (el) => {
  const { top: elementTop, bottom: elementBottom } = el.getBoundingClientRect();
  return window.screenTop < elementTop && elementBottom < window.innerHeight);
};

// 1秒間で透明度を1にフェードイン
const animateFadeIn = (el) => gsap.to(el, 1, {
  opacity: 1,
});

こんな感じでスクロールして画面内に要素が入った時にフェードインアニメーションが入るようになります。

これで

  • ディレクティブ処理
  • 画面内判定(ユーザアクションの数値判定)
  • アニメーション処理

を分離して管理できます。
アニメーションや画面内判定はutilsディレクトリ等に分けても良いかもです。

終わりに

私はこの方法でアニメーション管理が大分楽になりましたが、おそらく最善策ではないです。
当然プロジェクトや組織単位で開発環境や文化が変わると思います。
ですのでご参考程度にしていただければ。

何気に人生初記事でした。
もっとメンテナンスしやすく、もっとCPU(GPU)に優しくアニメーションできるよう勉強します。
マサカリください。
ご覧いただきありがとうございました。

Discussion