🎄メリークリスマスイブ!🎄この記事は、React-Spring1というアニメーションのライブラリを紹介する NTTテクノクロス Advent Calendar 2019の24日目の記事です。23日目は@yuitomoさんの記事、明日25日最終日は@korodroidさんの記事です。
2019年、令和初の年末も押しせまってまいりましたが、みなさん如何おすごしでしょうか? NTTテクノクロスの上原と申します。React/Gatsbyを用いた社内キュレーションサイトの構築や運用などを担当しています。当社では上記含め、SPAの開発にReactが採用されるケースも比較的多く、社外ブログにReactVRの記事を書いたり、去年のアドベントカレンダーイベントではGatsbyの記事「Reactベース静的サイトジェネレータGatsbyの真の力をお見せします」を書いたりしております。
はじめに
Webサイトの要所にあるアニメーションって、効果的に使えばかっこいいですよね。
でも、アニメーションって作るのは結構難しいです。私もですが、今まで修得を試みたものの挫折した経験がある方もいらっしゃるのではないかと思います。まあ出来合いの画面ライブラリでなんとかなっちゃう時も少なくないわけですが、シュッとした動きが思い通りにつけられたらなあ、とも常々おもっておりました。
そんな昨今、React-SpringというモダンなReact用のアニメーショライブラリ2を見つけて、Reactであれば! Hooksであれば!理解できそうなので(理解したとは言っていない)、解説記事を書いてみました。
対象読者
React経験者の方。Hooksの経験があるとなおよい。CSS Transitionとかの経験は不要である。
この文章の位置付け
本文書はreact-spring公式ドキュメントの代替を目指してはいません。ただ、公式ドキュメントはおそらく要点を絞りこみすぎていて、他のアニメーションライブラリやイージングライブラリの使用経験がないと、いきなり読みくだし理解することは難しいと感じました。本書では公式に抜けている「概念の説明」に重点をおいて、導入時に併読することで有用であることを目指しています。
本文書は筆者が調査したり類推した内容を含んでおり、間違いを含む可能性があります。問題がありましたら、ご連絡いただけますと幸いです。
アニメーションとは何か
最初にアニメーションの基本について説明します。不要であれば「react-springの紹介」まで読み飛ばしてください。
「アニメーション」とは、広義には絵を初めとする本来動かないものを動くように見せる映像表現のことです。ブラウザで表示しているページがスクロールしたり、ブラウザウィンドウをドラッグして移動させる、なども大きな意味では立派なアニメーションです。アニメーションGIFだってアニメーションです。
その部分集合として、react-springが扱う「アニメーション」とは、「DOMで表示されている画面上のオブジェクトの色や属性などが連続的に変化する」というものです。DOMアニメーションとCSSアニメーションの両方を含むものと考えてください。動画やGIFアニメの再生は対象外です。
「連続的に変化する」とはどういうことか
一般に、CSSやDOMをJavaScriptから更新すると、その設定内容は「瞬時に」「離散的に」変化します。途中経過がないのです。こんな感じです。
● → ◯
一瞬で変化する
厳密には一瞬ではないでしょうが、ブラウザはさまざまな再計算やレンダリングを行い「最終結果」を表示するための処理を一目散に行います。
これに対してreact-springの意味で「アニメーション」として表示することは、以下のように表示するということです。
● → ● → ● → ..◯ → ◯ →◯ → ◯
細かい単位(1/60秒ごとに)で変化する
1/60秒ごと(60Hz)というのは一般的なPCやMacでのリフレッシュレート、すなわち画面変化が物理的に視認できる最短の時間間隔です。この間隔でフレームバッファからディスプレイに情報が転送されるので、この単位よりも細かく画面を変化させることはできません。ちなみに、Oculus RiftやHTC ViveなどのVRヘッドマウントディスプレイでは、リフレッシュレートは90Hzであり、どんなディスプレイでも60Hzであるわけではありません。
連続的変化を表現するための方法
表示する画像をパラパラ漫画のように、たとえば60枚の画像を用意して1秒間に切り替えれば1杪分のアニメーションを表現できます。しかし容量は大きくなるでしょうし、前述のようにリフレッシュレートが異なるケースがあることも考えれば望ましくありません。
なので、一般にブラウザのUIのアニメーションでは以下のようにします。
- ブラウザ内の表示要素の「動き」の元になるものとして、DOM要素の位置、大きさ、透明度などに使用する実数値をピックアップします。
- その値を、時刻を引数とする関数値と考えます
- なんらかの方法でその関数を実装します。たとえば、
- 現在の値と最終値を与え、その間を補完する値を返す関数を生成する
- その値変化に対応する、JavaScriptの関数を定義する
- 1/60間隔で以下の処理を実行する
- 上記関数のその時点での値を決定し、
- DOMの属性をその値で更新する
このように関数もしくは計算式で定義すれば、間隔が60Hzであろうが120Hzであろうが一般的に定義できます。あるいはCPUが重くて処理が表示においつかなかった場合でも、更新を間引いて間隔を長くすることでなめらかさは劣るとしても動きとしては正しいアニメーションを表示することができます。
と、言葉では簡単そうですが、問題はこの関数を定義するのが難しいことです。単純な一次関数では自然な動きになりません。その問題を解決する適切な関数を生成する機能をもっているのがアニメーションライブラリであり、イージングライブラリ3です。
react-springの紹介
ということでここからが本題です。react-springは、DOMアニメーションやCSSアニメーションを行うためのReactライブラリです。以下の特徴をもっています。
💐宣言的アニメーション
- 最終的な姿や「こうなっているときこうする」といったルールを設定で指定するだけでアニメーションを表現します。「なにかのメソッドを呼び出したらアニメーションを開始する」とかはありません。「アニメーションのタイムラインのx杪目を実行中」みたいな概念もありません。Reactが「宣言的UI」であるのと同様に、宣言的にアニメーションを指定します。
💐物性ベースのタイミング指定
- 従来のアニメーションライブラリだと、アニメーションのタイミングや移動速度などは、継続時間とベジエ曲線(イージング関数)で指定するのが普通でした。これに対してreact-springでは慣性、摩擦力、張力をもった物理的な性質(物性)でタイミングを指定します。
どういうことか?- 張力が強いバネなら、シュと戻り、摩擦力が高いと、ジワーっと移動します。慣性が大きいと、ふんぬっ、ぬお〜、と一拍おく感じで物体が動きはじめます。張力が高いと、ビッビッと力強い動きをします。そういう感じに、コンピュータ上の図形の変化でも、物理的なモノがあるかのような動きをさせるのです。
- 移動時間を2.5杪にするか、1.5杪にするかなどは、天才アニメーターじゃないんだから常人には考えても答えなんかわかりません。バネのようにビョーンなのか、ハチミツのようにニチャーっと動くのか、という風に直感的に指定します。
- Appleの元UI-Kit開発者、Andy Matuschakは以下のように言っているそうです
継続時間とイージング曲線を引数とするアニメーションAPIは、継続的でなめらからなインタラクティブ性に根本的に反するものである。
- 従来のアニメーションライブラリだと、アニメーションのタイミングや移動速度などは、継続時間とベジエ曲線(イージング関数)で指定するのが普通でした。これに対してreact-springでは慣性、摩擦力、張力をもった物理的な性質(物性)でタイミングを指定します。
💐React Hooksベース/TypeScript対応
- HooksベースのAPIが使用できます4。当然TypeScirpt対応です。
💐React Native対応。
- Webだけではなく、react-native, react-native-webの開発をサポートします。
やってみようReact-Spring
⚠注意!⚠ |
---|
react-springのバージョンは、原稿執筆時の最新stableのv8ではなく、次期版であるv9ベースのものを使用してください。v8には特にTypeScriptの型定義に致命的な問題があります。「yarn add react-spring@next」 でインストールできます。 |
react-springのアニメーションプリミティブ一覧
react-springのHooksベースAPIの基本的なプリミティブには以下があります。
- (1) useSpring Hooks
- 1つの設定(config)のもとで、1つもしくは複数のアニメーション値(アニメーション的に変化する数値)を束ねるSpringオブジェクトを生成する。
- (2) useSprings Hooks
- 複数の設定(config)に対する複数のSpringオブジェクトを生成する。
- (3) useTrail Hooks
- 後続のものが先行するものに追随するような、複数のアニメーション値を定義する(Trail)。
- (4) useTransition Hooks
- 表示コンポーネントを別のコンポーネントに「切り替える」ときのアニメーション効果(Transtiion)を定義する。
- (5) useChain Hooks
- Spring,Trails,Transitionなどによる効果を連鎖的に実行する。
これらが、react-springにおけるアニメーション表現のための基本的な枠組みになります。それぞれの詳細については後述します。
アニメーションのAPIの概観
APIの個別の説明に入るまえに、useSpringを例にとって、react-springにおけるHooks APIのおおまかなイメージをまず説明します。
ureSpringはreact-springのプリミティブの中でもっとも基本的なものです。
useSpringのAPIは以下のようなHook関数です。
// (A)useSpring:({...アニメーション値:目標値,...configs})=>アニメーション値// (B)useSpring:(()=>{...アニメーション値:目標値,...configs})=>[アニメーション値,トリガ関数]
つまり2つのオーバーロードされた関数があって、引数がオブジェクトか関数かによってそれぞれ
- (A) アニメーション値
- (B) アニメーション値とそのトリガ関数
をそれぞれ返します。この二種類は、コントロールの方法が違います。
以降、ここでいくつか出てきている用語を説明します。
【用語説明】アニメーション値(AnimatedValue)
react-springによるアニメーション処理における最も基本的で重要なプリミティブがアニメーション値です。これは時間経過によって変化する値です。「キーとその値」というオブジェクトの形をしていて、useSpringなどのHook関数の返り値として得ることができます。
アニメーション値は以下の特徴を持っています。
- アニメーション値は、後述の「アニメーション化されたコンポーネント」でのみ使用できる。
- useStateが提供するような状態値を保持する。違いとしては
- useStateが返却するセッター関数で可能であるような「前の値から次の値を設定」などはできない。
- requestAnimationFrameのタイミングで勝手に変化する。
- アニメーション値は文字列や配列であってもよい。変化を計算する以上、本質的には一つ一つのnumberに対応するが、その表現として"18pt"とか単位がついてもいいし、"scale(3.0)"や"translate3d(0px,0,0)"みたいに文字列とに埋め込まれていてもいい。"red","green"、角度などDOMの修飾に使用できる多様な値を扱える。
アニメーション値によるアニメーションをトリガし開始するには、主に3つの方法があります。
- (A)の引数のアニメーション値を変化させる。変化させると、その値に向かってアニメーションの変化が開始される。
- (B)の呼び出し結果に含まれる「トリガ関数」で新しい値を引数にして呼び出す。たとえば、
setAnimValue({key: value});
のように、アニメーション値のキーと値を選択的に指定できる。 - (A),(B)いずれの場合でも、後述configのfromプロパティを設定する。immidiate: falseでなければ、from値とto目的値に差があれば、マウントされた時点でアニメーションのトリガがかかる。
【用語説明】アニメーション化されたコンポーネント(Animated Component)
「アニメーション値」の実体は、react-springライブラリが生成する、状態をもったオブジェクトなのですが、これをそのままコンポーネントのスタイル指定に与えることはできません。仮想DOMが理解する通常の数値や文字列に変換する必要があるのですが、アニメーション値の方を変換することはしません。その代りに、それを受け取って使用する側のコンポーネントの方を変換します。何を言ってるかというと、たとえば、
constMyComponent=({fontSize})=>(<divstyle={{fontSize:fontSize}}>HelloWorld</div>
);
こんなコンポーネントのstyle属性としてのfontSizeプロパティにアニメーション値を与えたいなら、
import{useString,animated}from'react-spring';constaprops=useSpring({fontSize:'150%'})constAnimatedMyComponent=animated(MyComponent);// ★...<AnimatedMyComponentstyle={{fontSize:aprops.fontSize}}/>
<!--もしくは<AnimatedMyComponentstyle={aprops}/> -->
-->
上記の★のところで、関数animatedにコンポーネントを渡して変換をかけます。ここで得られる「AnimatedMyComponent」は、プロパティにアニメーション値が来たときに、明示的にrequestAnimationFrameを呼んだりしなくて、そのアニメーション値に従ったアニメーション表示を行うコンポーネントになります。これを本文書では「アニメーション化されたコンポーネント」と呼びます1。
div,span,imgなどについては、あらかじめアニメーション化されたコンポーネントが用意されています。
コンポーネント | 意味 |
---|---|
animated.div | アニメーション化されたdivコンポーネント |
animated.span | アニメーション化されたspanコンポーネント |
animated.img | アニメーション化されたimgコンポーネント |
animated.svg | アニメーション化されたsvgコンポーネント |
animated.h1,h2.. | アニメーション化されたh1,h2,..コンポーネント |
【用語説明】config
configはHooksに与える設定用オブジェクトです。
= useSpring({ここにキー:バリューで指定})
主なキーには以下があります。
プロパティ名 | 型 | 説明 |
---|---|---|
任意 | num/string | 目的値。キーが既定義のものに被らなければ、toで指定する目標値として扱われる。 |
from | obj | アニメーション値の初期値。オプション。トリガされる前に使用される値。 |
to | obj/fn/array(obj) | アニメーション値が収束する目標値。 |
delay | number/fn | 開始時の遅延(ms)。オプション。引数にkeyをとる関数を与えると、複数のアニメーション値を設定することができる(fnについては以下同様)。 |
config | obj/fn | 慣性、摩擦力、張力などの物性を指定。既定義の物性もある(config.default/config.gentle/ config.wobbly/config.stiff/config.slow/config.molasses)。オプションであり指定しないとconfig.defaultが使用される。 |
ref | Reactのref | 後述のuseChainで連鎖的に実行するアニメーションの一環として動作させる。 |
API説明
(1) useSpring Hook
ureSpringはreact-springのプリミティブの中でもっとも基本的なものです。
1つまたは複数のアニメーション値をハンドリングします。
useSpring によるアニメーションの例(SampleA, SampleB)
上記は、1行目がSampleSpringAというコンポーネント、2行目がSampleSpringBというコンポーネントで実装しています。見た目は同じですが、処理がことなります。
SampleSpringAは、useSpringに目的値のプロパティを与え、Springを得ます。
SampleSpringBは、useSpringに初期値を返す関数を与え、Springとトリガ関数を得ます。
useSpringコード例(SampleSpringA.tsx)
以下SampleSpringAのソースコードです。
importReact,{useState}from"react";import{useSpring,animated}from"react-spring";constSampleSpringA=()=>{// (A)const[enter,setEnter]=useState(false);constspring=useSpring({fontSize:enter?"48pt":"24pt",color:enter?"red":"green"});return(<animated.divstyle={spring}onMouseEnter={e=>setEnter(true)}onMouseLeave={e=>setEnter(false)}>HelloReactSpring</animated.div>
);};exportdefaultSampleSpringA;
enterというstateを間接的にSpringに参照させ、そのstateを変化させることで、目的値が変化します。すなわち、stateの更新と引き続くrenderの呼び出しのタイミングで、アニメーションのトリガがかかり、アニメーションが進行します。
useSpringコード例(SampleSpringB.tsx)
以下はSampleSpringBのソースコードです。
こちらではstateを介在させる必要がなく、useSsringに関数をわたすことで、トリガ関数が返ってくるので、トリガ関数を任意のイベントハンドラ等から呼び出すことでアニメーションの進行がはじまります。
importReactfrom"react";import{useSpring,animated}from"react-spring";constSampleSpringB=()=>{// (B)const[spring,set]=useSpring(()=>({fontSize:"24pt",color:"green"}));return(<animated.divstyle={spring}onMouseEnter={e=>set({fontSize:"48pt",color:"red"})}onMouseLeave={e=>set({fontSize:"24pt",color:"green"})}>HelloReactSpring</animated.div>
);};exportdefaultSampleSpringB;
(2) useSprings Hook
複数の設定(config)に対する複数のSpringオブジェクトを生成します。
同種のアニメーションを行う一連のアニメーション化されたコンポーネントを生成することができます。
useSpringsによるアニメーションの例
useSpringsコード例(SampleSprings.tsx)
springの列のインデックスを引数とするコールバック関数で、それぞれのspringの設定をします。
トリガ関数もspringの列のインデックスを引数とする関数を指定します。
importReact,{useState}from"react";import{useSprings,animated,config}from"react-spring";constSampleSprings=()=>{constmsg="Hello React Spring";const[springs,set]=useSprings(msg.length,(idx)=>({// idxによって異なる設定をしてもよい。config:config.wobbly,fontSize:"24pt"}));return(<divstyle={{fontSize:"24pt"}}>{springs.map((item,idx)=>(<animated.spanonMouseEnter={e=>set(i=>(i===idx?{fontSize:"48pt"}:{}))}onMouseLeave={e=>set(i=>(i===idx?{fontSize:"24pt"}:{}))}style={{verticalAlign:"top",...item}}>{msg[idx]}</animated.span>
))}</div>
);};exportdefaultSampleSprings;
(3) useTrail Hook
後続のものが先行するものの変化に追随するような、複数のアニメーション値のリストを定義する(Trail)。
マウストラッキングアニメーションのようなものが簡単に定義できます。
useTrailによるアニメーションの例
useTrailコード例(SampleTrail.tsx)
importReact,{useState}from"react";import{useTrail,animated,config}from"react-spring";constSampleTrail=()=>{constmsg="Hello React Spring";const[{x,y},setXY]=useState({x:0,y:0});consttrails=useTrail(msg.length,{config:config.gentle,left:`${x}px`,top:`${y}px`,position:"absolute"});return(<divstyle={{width:"100%",height:1000,fontSize:"24pt"}}onMouseMove={e=>{e.persist();setXY({x:e.clientX,y:e.clientY});}}>{trails.map((trail,idx)=>(<animated.spanstyle={{...trail,paddingLeft:idx*23}}>{msg[idx]}</animated.span>
))}</div>
);};exportdefaultSampleTrail;
(4) useTransition Hook
表示コンポーネントを別のコンポーネントに「切り替える」ときのアニメーション効果を定義する。
以下を一手にまとめてやってくれます。
- これから表示しようとするコンポーネントをDOMに新たにマウントとする処理
- 新しくマウントしたコンポーネントに対するアニメーションの実行
- 新しくマウントしたコンポーネントによって、置き換えられてしまうコンポーネントをDOMからアンマウントする処理
- 置き換えられてしまうコンポーネントのアンマウント時のアニメーションの実行
一般に、コンポーネントを「切り替える」操作として、「古い方をアンマウントと新しいののマウントする」を同意に行うのが自然なのですが、アニメーションとしては、アンマウントされる方が消えていくアニメーションと新しい方が表われてアニメーションは、時間的重なりをもって動かないとそれらしくありません。なので、useTransionの返り値は高々2要素の配列であり、消えていくコンポーネントは時間差をもってアンマウントされます。
例と説明は都合にて割愛します。(後で追記するかも)
(5) useChain Hook
Spring,Trails,Transisionなどによる効果を連鎖的に実行する。
- Springなどのアニメーション値を作成する際のconfigに、refプロパティを指定し、useRefの結果を組込むすることで、useChainがrefを使ってトリガ関数の役割りを果してくれるようになります。逆に言えば、ref属性を組込むとトリガ関数経由ではコントロールできなくなります。
- このせいか、useSpringの(B)「アニメーション値とそのトリガ関数」のパターンのものに対してはuseChainは機能しません。
useChainによるアニメーションの例
refを準備し、制御下における部品に組み込み、chainで繋げます。
useChainコード例(SampleChain.tsx)
importReact,{useState,useRef}from"react";import{useSpring,useChain,animated,config}from"react-spring";constSampleSpring=({ref})=>{const[enter,setEnter]=useState(false);constref1=useRef();constref2=useRef();constspring1=useSpring({fontSize:enter?"48pt":"18pt",ref:ref1});constspring2=useSpring({fontSize:enter?"48pt":"18pt",ref:ref2});useChain([ref1,ref2]);return(<divstyle={{textAlign:"center"}}onMouseEnter={e=>setEnter(p=>!p)}onMouseLeave={e=>setEnter(p=>!p)}><animated.divstyle={spring1}>HelloReactSpring</animated.div>
<animated.divstyle={spring2}>HelloReactSpring</animated.div>
</div>
);};exportdefaultSampleSpring;
おわりに
ということで、react-springによる最先端Webアニメーション技術のサワリを紹介しました。
今回、紹介したのは、react-springの機能の一部ですが、主要なところはカバーしたつもりです。
本書のデモでは主に、fontSizeという地味な属性を変化させましたが、transform: scale, rotateなどのプロパティを変化させたり、SVGを使用すると複雑で派手なアニメーションを行うことができ、基本は同じです。
公式サイトには他に多数のデモが掲載されていますので参考ください。
もうアニメーションも怖くない! かも!
参考リンク
ロゴ画像はhttps://user-images.githubusercontent.com/619186/51572411-7e04a880-1e8c-11e9-802c-251f150a1e69.gifより引用 ↩
GitHubスター数15.4k(2019年12月現在)となかなかの人気なのではないかと思います。 ↩
「世界一わかりやすい「イージング」と、その応用」などが参考になります: ↩
HoCやRender PropsベースのAPIもあります。
↩