Edited at

Webフロントエンドでアニメーションを実装する時になにを考えるか

明けましておめでとうございます。

アニメーションの実装に抵抗を覚えている方、多いのではないでしょうか?

特に昨今ではサーバサイド出身で最近フロントエンドを触り始めたという方も多いと思います。私が属している組織でもそんな流れは少なからずあるのですが、そういったバックグラウンドの方の声として聞くことが多い声はやはり「CSSは触りたくない、アニメーション怖い」というものです。

私も少し前まではちょっと複雑なアニメーションを見ると「えぇ…」と思っていたものですが、今ではCoolなアニメーションを見ると「どうやって実現しよう」とワクワクするようになりました(とても正直に言うとあまりに激しめなことされると今でも「えぇ…」とはなります)。

この記事ではあるアニメーション要求がある時に、どうやってそれを実装に落とし込むか、その考え方を私なりに整理したものをまとめてみました。アニメーションに抵抗がある方や、これからフロントエンド学んでいこうと思う方の力になれば幸いです。


想定読者


  • これからWebフロントエンドのアニメーションに取り組んでいきたい方

  • 今までサーバサイドやってきて、最近フロントエンドやり始めたけどやっぱりCSSとかアニメーションには抵抗ある、そんなあなた


免責


  • この記事はアニメーション実装の「考え方」について話します。CSSトランジションやSVGアニメーションなどの具体的なHowについては触れません

  • 後々のサンプルコードは React で書きます。ただReactが分からずとも意図は伝わると思います

  • サンプルコードは本文の趣旨を伝えやすくするため「状態とCSSの関係が宣言的に書ける」 CSS in JS(styled-components)を使っています。ただstyled-componentsが分からずとも意図は伝わると信じています。伝われ!


アニメーションの目的

アニメーションの目的とはなんだと思いますか?

「カッコよくする・使い心地をよくすること」が目的と思っている方もいらっしゃたりするかもしれません。その目的も間違いなくあります。ただ、個人的に特にアプリケーション開発で意識したいこととして

「Cognitive Loadを減らすこと」

があります。いきなり訳わからん横文字を使いやがってと思われたかもしれませんが、一応海外のUX界隈では少しくらいは?知られた用語みたいです(UX界隈詳しいわけではないので自信ない)。起源が誰かは不明ですがどうやら元は心理学用語だったものをニールセンがUX文脈に使い出したのがはしりっぽいです。

Minimize Cognitive Load to Maximize Usability

さて、この Cognitive Load とはなんぞやという話ですが、上記リンク内では次のように定義されています。


In the field of user experience, we use the following definition: the cognitive load imposed by a user interface is the amount of mental resources that is required to operate the system. Informally, you can think of mental resources as "brain power" — more formally, we're talking about slots in working memory.


雑に意訳すると「アプリケーションを操作する時に消費するワーキングメモリーの量」ですね。

ユーザがアプリケーションを使っていると"状態"が変化します。それはページが変わったり、モーダルが開いたり、ログイン状態が切れたり、様々な変化が起こります。この時一切アニメーションがないとユーザは「なにが変わったのかを自分で見つけて考える」必要が出てきます。これはユーザの体験としては負荷となります。この Cognitive Load を減らす手段としてはアプリ内で「同じ目的の要素のデザインは一貫性を持たせる」などもありますが、それ以外に一手段としてアニメーションがあります。

congnitiveLoad.gif

アニメーションがあることによって「自分の起こしたアクションがどんな変化を起こしたのか」を画面を見ているだけで把握することができます。これによってユーザは余計なエネルギーを使わずに、そのアプリケーションで達成したいゴールにフォーカスすることができます。

アニメーションは価値が定量化しづらいものではありますが、ちゃんと目的があります。「カッコよくする」ことが目的となっているのであれば実はそのアニメーションはいらないのかもしれません。いらないどころかユーザには嫌がられているかもしれません。そんなものを実現するために工数を頑張って割くのは悲しいし、そのアニメーションの難易度が高ければ「なんで俺はこんなものを作っているんだ…」とモチベが下がってしまうかもしれないので、目的はしっかり捉えていきましょう。

ちなみに上記の Cognitive Load の話なんですが、Sarah Drasner さんというSVGアニメーション界の帝王みたいな人のトークがオススメなのでよかったら見てみてください。


それ以外の目的

一応の補足なのですが「カッコよくする」だったり「気持ちいいエフェクト」を作ることも立派な目的になりえます。

例えば、小さな例ですがMediumの clap の confetti はいい例です。

confetti.gif

これにはアクションが成功したことに対して分かりやすいフィードバックを与えるということと、そのアプリで重要なアクションと「キモチいい」エフェクトを結びつけることによって、そのアクションを促進することができます。


実装する上で考える要素

次に実装上でのアニメーションとは何かを考えてみましょう。

アニメーションはある状態から状態へ遷移する時に、その遷移の軌跡を描くものです。そしてWebフロントエンドにおいて画面は基本的にDOM + CSSによって表現されます。(本記事では簡略化のためこれ以降"DOM"とだけ呼びますが意味合いとしてはCSSも含みます)

なので次のような式で表現することができます。あるDOMに対してアニメーションが起こると、違うDOMになるというわけですね。

DOM + Animation = DOM'

次に、この DOM ですが View(State) という関数で表現することができます。例としてReactのコンポーネントを考えてみましょう。

const Button = ({ text }) => <button>{text}</button>

この Button というコンポーネントは text という引数を元に <button>{text}</button> という DOMを作成し返す関数として捉えることができます。先ほど述べた View(State) に当てはめると View() がこのコンポーネントで State がこのコンポーネントを使う側が持つ text の値です。

そして基本的にはこの関数は純粋であり、Stateに対して一意に定まるDOMを返します。(現実にはコンポーネント自体に状態を持たせることはありますが)

従って先ほどのアニメーションの式は次のように代入することができます。

View(State) + Animation = View(State')

DOMが変化するということは基本的には同じView関数を使っているはずであるため、引数であるStateの方が変化しているはずです。ここで認識しておきたいことは「アニメーションは何らかの状態の変化によって発火される」ということと「アニメーションはStateが変化する前と後のView関数の出力をつなげるもの」ということです。

ここまでで登場人物がいくつか出てきました。


  • State

  • View関数


    • アニメーション前のDOM

    • アニメーション後のDOM



これらがアニメーションに関わる要素で、決まればあとはHowの話のみとなります!

なので私がアニメーションを実装する際には次のことを考えます。


  1. 始まりと終わりの状態の DOM

  2. どんな状態の変化がアニメーションを発火させるのか

  3. どんな変化が起こっているのか

これらを一つ一つ分解して考えていけば、大体のアニメーションは実は怖くありません。次に実例を交えて紹介していこうと思います。


補足: あてはまらないのも色々ある

キレイな感じでアニメーションの表現をお伝えしましたが、もちろん上記のモデルに当てはまらないケースも存在します。

まず一つ目によくあるのが「削除時のアニメーション」ですね。削除するということはその対象のDOM自体がなくなってしまうわけですから、そもそもアニメーションをさせることができません。なので、削除時のアニメーションは「アニメーションさせてから状態を変える」などのワークアラウンドを取ったり、 TransitionGroup などの宣言的な書き方のまま削除時のアニメーションを書ける手段を取る必要があります。

二つ目によくあるのが「開始と終了のDOMが全く違う」ケースです。スマホでよく見るこんな感じのやつ。

anima.gif

こういったものはなんらかのライブラリパワーを使うか、アニメーション用のFakeの要素を用意して、それをアニメーションさせて、遷移が終わったら消す、みたいな方法で実現することができます。

なので先ほどご紹介した考え方が常にワークすることはないですが、こういったエッジケースも割とパターン化できるので一つずつ経験していきましょう。


実例で考えてみよう

1ステップずつ言葉で説明するより実例交えた方がよかろう、ということで例を二つ用意してみました。


例1. ドロワー (単一のトランジション)

よくあるこんな感じで右からニュッと出てくるドロワー。これをどうやったら作れそうか考えてみましょう。

drawer.gif

まず始まりのDOMはこんな感じですね。

const Drawer = () => <StyledDrawer>ドロワーだよ</StyledDrawer>

const StyledDrawer = styled.div`
...
right: 0;
transform: translateX(300px);
`

そして終わりのDOMはこんな感じ。

const Drawer = () => <StyledDrawer>ドロワーだよ</StyledDrawer>

const StyledDrawer = styled.div`
...
right: 0;
transform: translateX(0px);
`

変わったのは transform: translateX() ですね。画面に対して相対的にどの位置にいるかが変わっています。最初はドロワーの幅分を右にずらしておいて、登場する時に画面右にぴったりつく位置に変えることによってニュッと出しているわけです。

次にどんな状態が変化を与えるかという話ですが、今回は単純に ドロワーを開くかどうかという opened というbooleanの状態があると考えましょう。実際のアプリケーションではもう少し意味を持った状態になったりはするかもしれません。そしてどんなDOMの変化があるかというと単純な線形の translate なので CSS Transition で十分そうです。

これで準備はできました! opened という状態の変化をトリガーに 画面に対して相対的な位置を X方向に変化するアニメーションです。早速ですが完成形をみてみましょう。

const Drawer = ({ opened }) => <StyledDrawer opened={opened}>ドロワーだよ</StyledDrawer>

const StyledDrawer = styled.div`
...
transform: translateX(
${({ opened }) => opened ? 0 : -300}px );
transition: transform 0.3s ease-in-out;
`

なんとなく掴めたでしょうか。でもこの例は単純過ぎてちょっとつまらないので、次にもう少し複雑なアニメーションを考えてみましょう。


例2. クールなサイドナビ (複数のトランジション)

次に以下のようなサイドナビの表示を考えてみましょう。(難易度上げてみたかったので、はじめに述べた Cognitive Load 云々と言うよりCool成分多めですすみません)

coolsidenavi.gif

ちょっとハードルを上げてみました。「これどうやって作んねん…」と思われた方もいらっしゃるかもしれません。でも一個一個考えれば実はそんなに難しくありません。やっていきましょう!

まずは観察してみましょう。そうすると以下のことが順番に行われていることが分かります。


  1. 円が放射状に広がりサイドナビ表示領域と化す

  2. サイドナビの要素が上から順々に明度を上げつつ左からスライドしてくる

なんと、やっていることはたったの二つしかありません!

見て分かる通り、これらはそれぞれ独立したアニメーションです。ただ順序は依存関係があるため「二つ目のアニメーションのトリガーとなる状態変化は一つ目のアニメーション終了時に行う」ことだけは意識して、それ以外は独立したものと考えていきましょう。


放射状のサイドナビを考える

まずは一つ目のアニメーションを考えてみましょう。

始まりは小さい円です。それでアニメーションする時には画面全体を覆う四角になります。嘘です。円から四角へのアニメーションの実装というのは少々面倒なので実際はものすごく大きい円にします。Windowより大きくすれば見た目上は問題なくなります。

そして、変化のトリガーとなる状態は showSidenavi 的な状態変数があるとします。

完成形としてはこう。

const SidenaviBackground = ({ showSidenavi, toggle }) = (

<>
<Icon onClick={toggle} />
<Background />
</>
)

const Background = styled.div`
...
transform: scale(
${ ({ showSidenavi }) => showSidenavi ? 50 : 1 })
transition: transform 0.3s ease-in-out;
`

サイドナビを表示している場合は30倍の大きさにするって感じですね。これでアイコンをクリックした時のエフェクトはOKです。

onlyIcon.gif


サイドナビの要素のアニメーション

次にサイドナビの中身です。

今回変わっている要素は二つで opacityX軸の位置(transform: translateX()) です。そして、このアニメーションのトリガーとなる状態は、サイドナビが開かれた時に発火するため、先ほどと同じ showSidenavi です。また、上から順々に出現させるので、indexに応じてアニメーションに遅延を持たせます。

const Sidenavi = ({ showSidenavi }) => (

<ul>
{navs.map((nav, index) => (
<Nav showSidenavi={showSidenavi} index={index}>
{nav.text}
</Nav>
))}
</ul>
);

const STAGGER_SEC = 0.05;
const Nav = styled.li`
opacity:
${({ showSidenavi }) => (showSidenavi ? 1 : 0)};
transform: translateX(
${({ showSidenavi }) => (showSidenavi ? 0 : -150)}px);
transition: all 0.3s ease-in-out;
transition-delay:
${STAGGER_SEC * index}s;
`

これで動くようになりました。

showSidenav.gif


連続してアニメーションを実行させる

さて、それぞれ個別のアニメーションができましたが、今のままだと同時に実行されてしまうためあまりCoolではありません。先ほども申し上げた通り、今回のアニメーションは順序があります。同時に開始するわけではなく、一つ目のアニメーションが終わったら二つ目、です。

ここでどうやって実装するか二つ選択肢があります。


  • それぞれに対して状態変数を用意する

  • 二つ目のアニメーションに一つ目のアニメーション分のdelayを持たせる

今回の例では後者「二つ目のアニメーションに一つ目のアニメーション分のdelayを持たせる」を選択したいと思います。

理由としては両方のアニメーションが同じ状態である「サイドナビが開いているかどうか」に即して行われるからです。アニメーションのために新たな状態変数を登場させることはできればやりたくありません。

最初にアニメーションの目的をお伝えしたと思いますが、「アニメーションのためのアニメーション」となっていない場合、まずアニメーションより先にそのトリガーとなる状態の変化があるはずです。なので、アニメーションのためだけに新しい状態を追加しようとしている場合は危険信号かもしれません。

ちなみに、繰り返しにはなりますが今回は「同じ状態に対してアニメーションをするから」後者にしました。実際には異なる状態に依存したアニメーションが順に行われることもあります。その場合は、そのトリガーとなるアクションが実行されるところで制御するイメージです。

それでは先ほどの SidenaviSidenaviTrigger 分のトランジションにかかる時間のdelayを追加して上げましょう。

...

const DURATION_SEC = 0.3;
const Nav = styled.li`
transition-delay:
${ ({ index }) => DURATION_SEC + STAGGER_SEC * index }s;
`

これで円のアニメーションが終わってから順々に実行されるようになりました。ちなみに閉じる時は順序が逆だと思うので、背景の円の方にdelayを持たせます。

以上で完成です!一個一個考えてみたら実はそんなに難しくなかったと思います。一見難しそうに見えるアニメーションでも、しっかり観察して分解して考えるクセと立ち向かう勇気を身につけていきましょう。


おわりに

以上が私が思う「この辺り考えとけばアニメーション実装できるんじゃないの?」というもののまとめです。ただ色々ご託は述べたものの結局は経験あるのみで、いくつか実装を経験したらあまり考えずとも実現方法がパッと思い浮かぶようになってくると思います。

けっこうアニメーションは直接的な価値を生まない分、スケジュールとかがパツパツだと省かれがちだなあと思っているのですが、それが実はそんなに難しくないのに経験がないが故の恐れとかによって生じているのであればもったいないという想いからこの記事を書き始めました。この記事をきっかけに「ちょっとアニメーション実装してみたい!」など感じていただけたら嬉しいです。