Vueのアニメーションはカスタムディレクティブで管理しよう
概要
みなさんWebのスクロールアニメーションは好きですか?
私は見るのは好きですが、都度アニメーション処理を書き直すのがとても面倒っちいと感じます。
クリエイティブ系のWebサイトでよく使われている(気がする)Vue.jsですが、標準機能であるカスタムディレクティブを使ったアニメーション管理がとてもメンテしやすかったので共有します。
この記事でやること/やらないこと
やること
やらないこと
- 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-model
や v-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
のように自前実装したカスタムディレクティブを登録することで更にリッチな表現がつくれます。
スクロールディレクティブをつくろう
まず、スクロールの度にスクロール量を出力するディレクティブをつくりましょう。
// Vueオブジェクトをインポート
import Vue from "vue";
// 第一引数にディレクティブ名を入れます(v-xxxxのxxxx部分)
// 今回の場合はv-scrollとなります
Vue.directive("scroll", {
inserted: () => {
const handleOnScroll = () => console.log(window.scrollY);
window.addEventListener("scroll", function);
}
});
<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引数から取得できます。
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で表現すると
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要素がレンダリングされた際に実行される関数です。
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