はじめに
Reactでアニメーションを実装するとき、transition: all 300ms ease-outのような
duration(何ミリ秒で動かすか)とeasing(どんな曲線で動かすか)の組み合わせを、
毎回手探りで決めていないでしょうか。値を変えるたびにブラウザをリロードして確認し、
「もう少し弾ませたい」「途中で止めたい」となるたびにまた数値をいじり直す――
この試行錯誤に付き合いたくない人向けのライブラリがReact-Motionです。
React-Motionは、durationやeasingを一切指定しません。代わりにstiffness(剛性)と
damping(減衰)というばねの物理パラメータだけを渡すと、あとは物理シミュレーションが
値を動かしてくれます。途中で目標値が変わっても、今の速度を保ったまま自然に方向転換する
ので、アニメーションの「積み木崩し」が起きにくいのも特徴です。
とはいえ、読むより触った方が早いと思います。ばねの動きがCSSのeasingとどう違うのかを その場で確認できるサンプルを用意したので、先に挙動を見たい方はこちらからどうぞ。
React-Motionとは
React-Motionは、Cheng Lou氏が開発したReact向けのアニメーションライブラリです。
「95%のユースケースでは、ハードコードされたeasing曲線やdurationを使う必要がない」
という思想のもとに作られており、Motion・StaggeredMotion・TransitionMotion
という3つのコンポーネントで、単一要素から一覧表示、マウント・アンマウント時の
演出まで一貫してばねの物理演算に任せられます。
主な特徴
- 物理ベースのばねアニメーション -
spring(value, { stiffness, damping })で 目標値とばねの硬さ・減衰を渡すだけで、あとはフレームごとに現在値が計算されます - 中断への強さ - アニメーション中に目標値が変わっても、現在の速度を引き継いで 方向転換するため、CSSのtransitionにありがちな「一瞬止まってから逆再生」が起きません
- リストの出入りに強い
TransitionMotion- マウント時・アンマウント時それぞれに 独立したスタイルを指定でき、削除アニメーションが終わるまでDOMから消さずに待てます - レンダー結果のみを操作する設計 - ライブラリ自身はDOMを直接触らず、
子要素に現在値のオブジェクトを渡すだけなので、
divでもSVGでもCanvasでも 同じ書き方でアニメーションさせられます
インストール
npm install react-motion
yarn add react-motion
React-MotionのpeerDependenciesはreactとreact-domの16系までを対象としています。
React 18以降のプロジェクトに追加する場合は、npm install --legacy-peer-depsのように
peer依存の警告を無視するオプションが必要になることがあります。
React-Motionのサンプルを動かす
Motionコンポーネントにstyleとして{ x: spring(目標値) }を渡すと、
子要素のrender関数に現在のフレームでの値が渡ってきます。下のサンプルは
ボックスの横位置をspringでアニメーションさせる最小構成です。ボタンを押すたびに
目標値が切り替わり、途中でもう一度押すと今の速度のまま逆方向に動き出します。
import { Motion, spring } from 'react-motion'
function Box({ toggled }) {
return (
<Motion style={{ x: spring(toggled ? 300 : 0, { stiffness: 170, damping: 26 }) }}>
{(value) => (
<div style={{ transform: `translateX(${value.x}px)` }}>box</div>
)}
</Motion>
)
}
実際に動かせるものが下です。「動かす」ボタンを連打してみてください。stiffness
(ばねの硬さ)を大きくすると素早くカクッと動くようになり、damping(減衰)を
小さくするとゴム紐のように行き過ぎてから戻るオーバーシュートが目に見えて増えます。
spring()の第一引数は目標値、第二引数がばねの硬さと減衰です。CSSの
cubic-bezier()のようにカーブの形を直接指定するのではなく、「どれくらい硬いばねで、
どれくらい早く止まるか」という物理量で動きの質感を調整できるのがReact-Motionの
発想の中心です。
基本的な使い方
最小構成では、Motionコンポーネントにstyleオブジェクトを渡し、childrenに
関数を渡す形で現在値を受け取ります。springを通さない値(数値そのもの)を
混ぜることもでき、その場合はアニメーションせず即座にその値が使われます。
import { Motion, spring } from 'react-motion'
function FadeBox({ visible }) {
return (
<Motion style={{ opacity: spring(visible ? 1 : 0) }}>
{({ opacity }) => <div style={{ opacity }}>content</div>}
</Motion>
)
}
stiffnessとdampingを省略した場合は既定値(stiffness: 170, damping: 26)が
使われます。presetsとしてnoWobble・gentle・wobbly・stiffという
プリセットも用意されており、spring(value, presets.wobbly)のように呼び出せます。
実践的なユースケース
StaggeredMotionで一覧に時間差をつける
リストの各項目を少しずつ遅れて動かしたいとき、setTimeoutを要素数ぶん並べるのは
面倒です。StaggeredMotionは、各要素の目標スタイルを「1つ前の要素の現在値」を
参照しながら計算する関数を渡すことで、要素間の追従関係だけでずれを表現できます。
stylesに渡す関数の第一引数prevStylesが「直前フレームでの各要素の現在値」なので、
1つ前の要素のopacityを参照して自分の目標値を決めれば、連鎖的な時間差が生まれます。
遅延の量を大きくしたいときは、参照する要素を1つ前ではなく2つ前にするなど、
参照先の間隔を広げると効果が強まります。
TransitionMotionでリストの追加・削除をアニメーションさせる
配列から要素をfilterで取り除くと、Reactは即座にDOMから消してしまうため、
「フェードアウトしながら消える」ような退場アニメーションが素のReactでは書けません。
TransitionMotionはwillEnter(登場時の初期値)・willLeave(退場時の目標値)を
指定でき、退場アニメーションが完了するまで要素をDOMに残してくれます。
willEnterが返す値は要素がマウントされた瞬間の初期値、willLeaveが返す値は
削除がトリガーされてから消えるまでの目標値です。willLeaveでheightをspring(0)
にしているため、削除ボタンを押すと高さが縮みながら消えるのが確認できるはずです。
willLeaveを省略すると即座にDOMから消えるので、退場演出はこの関数の有無で
オン・オフを切り替えられます。
まとめ
React-Motionは、durationとeasing曲線の組み合わせを手探りで決める代わりに、
stiffnessとdampingというばねの物理パラメータでアニメーションの質感を
コントロールするライブラリです。Motionで単一の値を、StaggeredMotionで
要素間の時間差を、TransitionMotionでリストの出入りを、それぞれ同じ
「目標値をspring()で包んで渡すだけ」という発想でカバーできます。
React 18以降のプロジェクトに導入する場合はpeer依存の警告に注意しつつ、 まずは1つの要素をばねで動かすところから試してみてください。
