画面を開いたタイミングで配列の要素をtransition-group
を使ってアニメーションさせる方法を記載します。
最後はanime.js
と組み合わせて、
↑の動画のような少し複雑なアニメーションをさせる方法についても記載します。
検証した環境
1 | nuxt | 2.14.6 |
2 | animejs | 3.2.1 |
3 | @types/animejs | 3.1.3 |
アニメーションをする前の状態を作る
TypeScriptで極力型を指定したいので nuxt-property-decorator
を使います。
Nuxtらしい記述はしないので vue-property-decorator
で考えてもらって差異はありません。
まず data
で用意した文字列をただリスト表示させてみます
<template>
<ul>
<li v-for="item in items">
{{ item }}
</li>
</ul>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
@Component
export default class IndexPage extends Vue {
items = ['JavaScript', 'Swift', 'Kotlin', 'GO', 'PHP', 'Ruby']
}
</script>
ここでは後ほどのwarningをあえて表示するためにv-for
で回している <li> 要素に対してkey
を記載していませんが、
実際の場合は公式にも書かれている通りkey
を設定するのが理想です。
繰り返される DOM の内容が単純な場合や、性能向上のために標準の動作に意図的に頼る場合を除いて、可能なときはいつでも v-for に key 属性を与えることが推奨されます。
アニメーションさせる
ここから徐々にtransition-group
を使っていきます
下準備
<template>
<ul>
<transition-group name="list">
<li v-for="item in items">
{{ item }}
</li>
</transition-group>
</ul>
</template>
・・・
まずv-for
で回している順番に表示させたい要素を<transition-group>
で囲みます。
ただし、これで起動してみると ERROR [Vue warn]: <transition-group> children must be keyed: <li>
と怒られます
transition-groupを使う場合は key
が必須なんですね、
アニメーションさせる要素を確実に判断するためかな
そのため、li
にkey
を設定します、key
には適宜一意になる値を設定します
<template>
<ul>
<transition-group name="list">
<li v-for="item in items" :key="item">
{{ item }}
</li>
</transition-group>
</ul>
</template>
・・・
Buttonタップでまずアニメーションさせる
まずよくある「ボタンを押したらアニメーションする」ところから確認します。
<template>
<div>
<ul>
<!-- nameで付けた名前がlist-enter-activeなどtransition-group特有のクラスとして付与される -->
<transition-group name="list">
<li v-for="(item, index) in items" :key="item">
{{ item }}
</li>
</transition-group>
</ul>
<!-- ボタンを押したら add() を呼び出す -->
<button @click="add">追加</button>
</div>
</template>
<script lang="ts">
・・・
export default class IndexPage extends Vue {
items = ['JavaScript', 'Swift', 'Kotlin', 'GO', 'PHP', 'Ruby']
add() {
const langs = ['C#', 'Rust']
// スプレッド構文を使って items と langs をマージする
this.items = [...this.items, ...langs]
}
・・・
</script>
<style>
/* アイテムが追加されアニメーションしている間付与される */
.list-enter-active {
transition: all 0.3s ease;
}
/* アイテムが追加されアニメーションが始まる最初だけ付与される */
.list-enter {
transform: translateX(10px);
opacity: 0;
}
</style>
要素が追加されるタイミングでアニメーションされるようになりました!
しかし追加した2つの要素どちらも同時に表示されてしまう 😅
その点については後ほど。
まず簡単に流れについて書きます。
- ボタンを押すとaddメソッドが呼ばれ
items
が更新される - keyを元に新たに追加された要素に対して
transition-group
特有のクラスを付与
.list-enter-active
と.list-enter
- 付与されたクラスを元にアニメーションする
transition-group
のクラスについては
公式のドキュメントがイメージも合わせてかなり分かりやすく書かれています
さていったん雑にですが、
2つ同時に表示されてしまう問題について<li>
要素に対しtransition-delay
を付与する事で対応します。
<template>
・・・
<transition-group name="list">
<li
v-for="(item, index) in items"
:key="item"
:style="{ 'transition-delay': `${index * 0.3}s` }"
>
{{ item }}
</li>
</transition-group>
</template>
ロジックとしては新たに追加された<li>
要素に設定されたtransition-delay
に応じてアニメーションが実行されます。
その際に配列のindex番号を使っているので、
↑のように最初から表示されている言語に対してもtransition-delay
が設定され、
今回追加した要素だとtransition-delay
が1.8s;
と2.1s;
とかなり時間がかかるため動画のように中々表示がされない、という問題点があります 😅
画面描画時にアニメーションさせる
ここまでを踏まえて、画面描画時に要素をアニメーションさせてみます
<template>
<ul>
<transition-group name="list">
<li
v-for="(item, index) in items"
:key="item"
:style="{ 'transition-delay': `${index * 0.3}s` }"
>
{{ item }}
</li>
</transition-group>
</ul>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
@Component
export default class IndexPage extends Vue {
// items = [] のみだと何の型の配列か型推論が出来ないため string[] を明記
items: string[] = []
mounted() {
this.items = ['JavaScript', 'Swift', 'Kotlin', 'GO', 'PHP', 'Ruby']
}
}
</script>
<style>
.list-enter-active {
transition: all 0.3s ease;
}
.list-enter {
transform: translateX(10px);
opacity: 0;
}
</style>
画面を開くと同時にアニメーションされました!
ロジックは画面を開いた際にmounted
が呼ばれitems
が更新されます。
すると先ほどと同じでtransition-delay
が<li>
要素に順に設定されつつ、
.list-enter-active
や.list-enter
クラスが付与されるため順番に表示がされます。
注意点として、Nuxt.jsを用いる場合はcreated()
だと反応しません!
理由はcreatedはSSR(サーバ側)でも呼び出されるメソッドのため、
サーバ側でレンダリングされてしまうためです。
そのため、Nuxt.jsの場合はcreated
ではなくmounted
を使いましょう
jsでアニメーション
↑の場合だと、
<template>
と<style>
にCSSが分かれてしまいます。
出来れば同じ要素のアニメーションに対してはcssはひとまとめにしたいところです。
そこでここではアニメーションに対するcssを全てjsに持たせてみます。
anime.jsの導入
jsを用いて様々なアニメーションをさせる事が出来るライブラリのanime.js
anime.js • JavaScript animation engine
↑トップページのアニメーション見るだけでワクワクするのでぜひ見てみて下さい!😍
最近簡単なアニメーションさせるのはほぼcssで行う事が多くなった印象ですが、
jsを使う分、よりプログラマブルに複雑で色々な事が出来ます。
必要なパッケージを追加します
# anime.jsの導入
$ yarn add animejs
# TypeScriptの場合は@typesを追加
$ yarn add -D @types/animejs
nuxt.configなどに対する設定は必要ありません。
anime.jsを使用するコンポーネントでimport
するのを忘れないようにして下さい
// anime.jsを使うコンポーネントではimportが必須
import anime from 'animejs'
anime.jsを使ってアニメーションさせる
transition-group
はcssのクラスを自動付与してくれる機能とは別に、
jsのメソッドを呼び出す事が出来ます。
<template>
<transition-group
name="list"
@before-enter="beforeEnter /* 要素が追加されアニメーションを実行する直前に呼ばれる */"
@enter="enter /* 要素が追加されアニメーションを実行する時に呼ぶ */"
@before-leave="beforeLeave /* 要素が削除されアニメーションを実行する直前に呼ばれる */"
@leave="leave /* 要素が削除されアニメーションを実行する時に呼ぶ */"
>
・・・
</template>
ここではbefore-enter
とenter
でanime.jsを用いたメソッドを実行しアニメーションさせてみます
<template>
<ul>
<transition-group name="list" @before-enter="beforeEnter" @enter="enter">
<li v-for="(item, index) in items" :key="item" :data-index="index">
{{ item }}
</li>
</transition-group>
</ul>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import anime from 'animejs'
@Component
export default class IndexPage extends Vue {
items: string[] = []
mounted() {
this.items = ['JavaScript', 'Swift', 'Kotlin', 'GO', 'PHP', 'Ruby']
}
// 要素が追加されアニメーションが始まる前に呼ばれる
beforeEnter(el: HTMLElement) {
anime({ targets: el, translateY: 50, opacity: 0 })
}
// 要素が追加されアニメーションが始まる時に呼ばれる
enter(el: HTMLElement, done: () => void) {
// data属性として設定した index の値を取得
const index: number = (el.dataset as any).index as number
if (!index) return
const delay = index * 420
setTimeout(() => {
anime({
targets: el,
translateY: [50, 0],
opacity: [0, 1],
easing: 'easeInOutSine',
complete: done
})
}, delay)
}
}
</script>
いい感じにヌルヌル動きますね!
まずbeforeEnter
でアニメーションする前の状態を設定しておきます
beforeEnter(el: HTMLElement) {
anime({ targets: el, translateY: 50, opacity: 0 })
}
そして enter()
でアニメーションに関する設定をします。
enter(el: HTMLElement, done: () => void) {
// data属性として設定した index の値を取得
const index: number = (el.dataset as any).index as number
if (!index) return
const delay = index * 420
setTimeout(() => {
anime({
targets: el,
translateY: [50, 0],
opacity: [0, 1],
easing: 'easeInOutSine',
complete: done
})
}, delay)
}
<li v-for="(item, index) in items" :key="item" :data-index="index">
{{ item }}
</li>
data属性を用いて配列の添字を持たせ(:data-index="index"
の部分)
その値とsetTimeout
を組み合わせて各要素に対しそれぞれどれくらい遅れてアニメーションをするかを設定しています。
data属性の取得がTypeScript使うと複雑ですね 🤔
anime.jsを複雑にしてみる
上記コードのanime({})
をサンプルコードを見て少しいじってみていたところ、面白い動きが出来ました 😃
anime.jsのサンプルコードは以下に公式がまとめてくれていまして、
こちらも見ているだけでワクワク出来ます!
enter(el: HTMLElement, done: () => void) {
const index: number = (el.dataset as any).index as number
if (!index) return
const delay = index * 420
setTimeout(() => {
anime({
targets: el,
translateY: [50, 0],
translateX: [10, 0],
scale: [1.3, 1],
opacity: [0, 1],
delay: anime.stagger(200, { direction: 'reverse' }),
complete: done
})
}, delay)
}
anime.random()
というメソッドがあって使ってみると予想外の動きをして面白いです😎
enter(el: HTMLElement, done: () => void) {
const index: number = (el.dataset as any).index as number
if (!index) return
const delay = index * 420
setTimeout(() => {
anime({
targets: el,
translateY: [anime.random(-200, 200), 0],
translateX: [anime.random(-300, 300), 0],
rotate: [anime.random(-360, 360), 0],
color: ['#00c4ff', '#0f0', '#000'],
opacity: [0, 1],
easing: 'easeOutBack',
complete: done
})
}, delay)
}