🎄メリークリスマスイブ!🎄 この記事は、React-Spring1というアニメーションのライブラリを紹介する NTTテクノクロス Advent Calendar 2019 の24日目の記事です。23日目は@yuitomoさんの記事、明日25日最終日は@korodroidさんの記事です。
[^1]: ロゴ画像はhttps://user-images.githubusercontent.com/619186/51572411-7e04a880-1e8c-11e9-802c-251f150a1e69.gif より引用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では***慣性、摩擦力、張力をもった物理的な性質(物性)***でタイミングを指定します。[こちら](https://www.react-spring.io/docs/hooks/api#configs)で各パラメータをいじって試せます。
どういうことか?
* 張力が強いバネなら、シュと戻り、摩擦力が高いと、ジワーっと移動します。慣性が大きいと、ふんぬっ、ぬお〜、と一拍おく感じで物体が動きはじめます。張力が高いと、ビッビッと力強い動きをします。そういう感じに、コンピュータ上の図形の変化でも、物理的なモノがあるかのような動きをさせるのです。 * 移動時間を2.5杪にするか、1.5杪にするかなどは、天才アニメーターじゃないんだから常人には考えても答えなんかわかりません。バネのようにビョーンなのか、ハチミツのようにニチャーっと動くのか、という風に直感的に指定します。 * Appleの元UI-Kit開発者、Andy Matuschakは以下のように言っているそうです
[継続時間とイージング曲線を引数とするアニメーションAPIは、継続的でなめらからなインタラクティブ性に根本的に反するものである。](https://twitter.com/andy_matuschak/status/566736015188963328) * easing関数を指定する選択肢も[ある](https://www.react-spring.io/docs/hooks/api#Configs)。
-
💐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つのプロパティ設定のもとで、1つもしくは複数のアニメーション値キー(アニメーション的に変化する数値)を束ねるSpringオブジェクトを生成する。
-
(2) useSprings Hooks
- それぞれ固有のプロパティ設定を持つ複数の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: ({
...アニメーション値のキー:目標値,
...アニメーションプリミティブのプロパティ設定,
}) => アニメーション値
// (B)
useSpring: (() => {
...アニメーション値のキー:目標値,
...アニメーションプリミティブのプロパティ設定,
}) => [アニメーション値, トリガ関数]
つまり2つのオーバーロードされた関数があって、引数がオブジェクトか関数かによってそれぞれ
- (A) アニメーション値
- (B) アニメーション値とそのトリガ関数
をそれぞれ返します。この二種類は、コントロールの方法が違います。
以降、ここでいくつか出てきている用語を説明します。
【用語説明】アニメーション値(AnimatedValue)
react-springによるアニメーション処理における最も基本的で重要なプリミティブが特別な「アニメーション値」です。これは時間経過によって変化する値です。「キーとその値」というオブジェクトの形をしていて、useSpringなどのHook関数の返り値として得ることができます。
アニメーション値は以下の特徴を持っています。
- アニメーション値の現在値は、後述の「アニメーション化されたコンポーネント」と、あとinterporateの計算でのみ間接的に使用できる(文脈の外に取り出せない。取り出すと意味がない)。
- useStateが提供するような状態値を保持する。違いとしては
- useStateが返却するセッター関数で可能であるような「前の値から次の値を設定」などはできない。
- 現在値が設定で指定した物理特性と時刻経過によって、requestAnimationFrameのタイミングで自動的に再計算、設定される。その更新を意識する必要がない。
- アニメーション値は文字列や配列であってもよい。変化を計算する以上、本質的には一つ一つのnumberに対応するが、その表現として"18pt"とか単位がついてもいいし、"scale(3.0)"や"translate3d(0px,0,0)"みたいに文字列に埋め込まれていてもいい。"red","green"などの色名、rgb/hsvの指定、角度など、DOMの修飾に使用できる多様な値を扱える。
【用語説明】 アニメーションのトリガ
アニメーション値によるアニメーションをトリガし開始するには、主に3つの方法があります。
- (A)の引数の目標値を前回render呼び出し時から変化させる(propsやuseStateによって)。変化させると、その値に向かってアニメーションの変化が再度開始される。
- (B)の呼び出し結果に含まれる「トリガ関数」をイベントハンドラで呼び出し、新しい目標値を設定する。たとえば、
setAnimValue({key: value});
のように、アニメーション値のキーと目標値を選択的に指定できる。 - (A),(B)いずれの場合でも可能な方法として、後述アニメーションプリミティブのfromプロパティを設定する。immidiate: falseでなければ、from値とto目標値に差があれば、マウントされた時点で目標値へのアニメーションのトリガがかかる。
【用語説明】アニメーション化されたコンポーネント(Animated Component)
「アニメーション値」の実体は、react-springライブラリが生成する、状態をもったオブジェクトなのですが、これをそのままコンポーネントのスタイル指定に与えることはできません。仮想DOMが理解する通常の数値や文字列に変換する必要があるのですが、アニメーション値の方を変換することはしません。その代りに、それを受け取って使用する側のコンポーネントの方をwrapperに変換します。何を言ってるかというと、たとえば、
const MyComponent = ({fontSize}) => (
<div style={{fontSize: fontSize}} >Hello World</div>
);
こんなコンポーネントのstyle属性としてのfontSizeプロパティにアニメーション値を与えたいなら、
import { useString, animated } from 'react-spring';
const aprops = useSpring({fontSize: '150%'})
const AnimatedMyComponent = animated(MyComponent); // ★
...
<AnimatedMyComponent style={{fontSize: aprops.fontSize}} />
<!-- もしくは <AnimatedMyComponent style={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,..コンポーネント |
【用語説明】アニメーションプリミティブのプロパティ設定
Hooksに与える共通する設定用オブジェクトです。例として、useSpringの第一引数にあたえる場合以下のようになります
= useSpring({ここにキー:バリューで指定}) // 前述の(A)
あるいは
= useSpring(() => {ここにキー:バリューで指定}) // 前述の(B)
主なキーには以下があります。他すべてについてはこちら。
プロパティ名 | 型 | 説明 |
---|---|---|
任意 | num/string | キーがアニメーションプロパティ設定のキーに被らなければ、toで指定する目標値として扱われる。 |
from | obj | アニメーション値の初期値。オプション。トリガされる前に使用される値。 |
to | obj/fn/array(obj) | アニメーション値が収束する目標値。 |
delay | number/fn | 開始時の遅延(ms)。オプション。引数にkeyをとる関数を与えると、複数のアニメーション値を設定することができる(fnについては以下同様)。 |
config | obj/fn | 慣性、摩擦力、張力などの物性を指定。既定義のプリセット物性もある(config.{default,gentle,wobbly,stiff,slow,molasses})。オプション。 |
ref | Reactのref | 後述のuseChainで連鎖的に実行するアニメーションの一環として動作させる。オプション。 |
API説明
(1) useSpring Hook
ureSpringはreact-springのプリミティブの中でもっとも基本的なものです。
1つのプロパティ設定のもとで、1つもしくは複数のアニメーション値キー(アニメーション的に変化する数値)を束ねるSpringオブジェクトを生成します。
useSpring によるアニメーションの例(SampleA, SampleB)
(以降含め、デモはこちらで試せます。ソースコードはこちらです。)
上記は、1行目がSampleSpringAというコンポーネント、2行目がSampleSpringBというコンポーネントで実装しています。見た目も動作も同じですが、処理がことなります。
SampleSpringAは、前述「アニメーションのAPIの概観」における(A)のパターンでuseSpringにアニメーションプリミティブのプロパティを与え、Springを得ています。
SampleSpringBは、同様に(B)のパターンでseSpringにアニメーションプリミティブのプロパティを返す関数を与え、Springとトリガ関数を得ています。
useSpringコード例(SampleSpringA.tsx)
以下SampleSpringAのソースコードです。
import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
const SampleSpringA = () => {
// (A)
const [enter, setEnter] = useState(false);
const spring = useSpring({
fontSize: enter ? "48pt" : "24pt",
color: enter ? "red" : "green"
});
return (
<animated.div
style={spring}
onMouseEnter={e => setEnter(true)}
onMouseLeave={e => setEnter(false)}
>
Hello React Spring
</animated.div>
);
};
export default SampleSpringA;
enterというstateを間接的にSpringに参照させ、そのstateを変化させることで、目標値が変化します。すなわち、stateの更新と引き続くrenderの呼び出しのタイミングで、アニメーションのトリガがかかり、アニメーションが進行します。
useSpringコード例(SampleSpringB.tsx)
以下はSampleSpringBのソースコードです。
こちらではstateを介在させる必要がなく、useSsringに関数をわたすことで、トリガ関数が返ってくるので、トリガ関数を任意のイベントハンドラ等から呼び出すことでアニメーションの進行がはじまります。
import React from "react";
import { useSpring, animated } from "react-spring";
const SampleSpringB = () => {
// (B)
const [spring, set] = useSpring(() => ({
fontSize: "24pt",
color: "green"
}));
return (
<animated.div
style={spring}
onMouseEnter={e => set({ fontSize: "48pt", color: "red" })}
onMouseLeave={e => set({ fontSize: "24pt", color: "green" })}
>
Hello React Spring
</animated.div>
);
};
export default SampleSpringB;
(2) useSprings Hook
それぞれ固有のプロパティ設定を持つ複数のSpringオブジェクト(ここではSpring列と呼ぶ)を生成します。
似たようなアニメーションを行う一連のアニメーション化されたコンポーネントを生成することができます。
useSpringsによるアニメーションの例
useSpringsコード例(SampleSprings.tsx)
Spring列のインデックスを引数とするコールバック関数で、個々のSpringで異なる設定をします。
トリガ関数もSpring列のインデックスを引数とする関数で指定します。
import React, { useState } from "react";
import { useSprings, animated, config } from "react-spring";
const SampleSprings = () => {
const msg = "Hello React Spring";
const [springs, set] = useSprings(msg.length, (idx) => ({
// idxによって異なる設定をしてもよい。
config: config.wobbly,
fontSize: "24pt"
}));
return (
<div style={{ fontSize: "24pt" }}>
{springs.map((item, idx) => (
<animated.span
onMouseEnter={e => set(i => (i === idx ? { fontSize: "48pt" } : {}))}
onMouseLeave={e => set(i => (i === idx ? { fontSize: "24pt" } : {}))}
style={{ verticalAlign: "top", ...item }}
>
{msg[idx]}
</animated.span>
))}
</div>
);
};
export default SampleSprings;
(3) useTrail Hook
後続のものが先行するものの変化に追随するような、複数のアニメーション値のリストを定義する(Trail)。
マウストラッキングアニメーションのようなものが簡単に定義できます。
useTrailによるアニメーションの例
useTrailコード例(SampleTrail.tsx)
import React, { useState } from "react";
import { useTrail, animated, config } from "react-spring";
const SampleTrail = () => {
const msg = "Hello React Spring";
const [{ x, y }, setXY] = useState({ x: 0, y: 0 });
const trails = useTrail(msg.length, {
config: config.gentle,
left: `${x}px`,
top: `${y}px`,
position: "absolute"
});
return (
<div
style={{ width: "100%", height: 1000, fontSize: "24pt" }}
onMouseMove={e => {
e.persist();
setXY({ x: e.clientX, y: e.clientY });
}}
>
{trails.map((trail, idx) => (
<animated.span style={{ ...trail, paddingLeft: idx * 23 }}>
{msg[idx]}
</animated.span>
))}
</div>
);
};
export default SampleTrail;
(4) useTransition Hook
表示コンポーネントを別のコンポーネントに「切り替える」ときのアニメーション効果を定義する。
以下のようなマウント・アンマウントおよびアニメーションの処理を一手に手際良くやることができます。
- これから表示しようとするコンポーネントをDOMに新たにマウントとする処理
- 新しくマウントしたコンポーネントに対するアニメーションの実行
- 新しくマウントしたコンポーネントによって、置き換えられてしまうコンポーネントをDOMからアンマウントする処理
- 置き換えられてしまうコンポーネントのアンマウント時のアニメーションの実行
一般に、コンポーネントを「切り替える」操作として、「古い方のアンマウントと、新しい方のマウント」を同時に行うのが自然なのですが、アニメーションとしては、アンマウントされる方が消えていくアニメーションと新しい方が表われてくるアニメーションは、時間的重なりをもって動かないとそれらしくありません。なので、useTransionの返り値はアニメーション進行中のコンポーネントを表わす配列であり、これに基づいて消えていくコンポーネントを並行してアニメーションさせつつ、時間差をもってアンマウントできます。
useTransitionによるアニメーションの例
useTransitionコード例(SampleTransition.tsx)
import React, { useState } from "react";
import { useTransition, animated, config } from "react-spring";
const SampleTransition = () => {
const [idx, setIdx] = useState(0);
const comps = [
({ style }) => (
<animated.div
style={{ position: "absolute", backgroundColor: "lightblue", ...style }}
>
Hello React Spring 1
</animated.div>
),
({ style }) => (
<animated.div
style={{
position: "absolute",
backgroundColor: "lightgreen",
...style
}}
>
Hello React Spring 2
</animated.div>
),
({ style }) => (
<animated.div
style={{ position: "absolute", backgroundColor: "pink", ...style }}
>
Hello React Spring 3
</animated.div>
)
];
const transitions = useTransition(idx, item => item, { // ★
unique: true,
from: { opacity: 0 },
enter: {
opacity: 1,
transform: "translateY(0px) rotate(0turn)"
},
leave: {
opacity: 0,
transform: "translateY(100px) rotate(0.3turn)"
}
});
return (
<div
style={{ width: "100%", height: 1000, fontSize: "24pt" }}
onClick={e => {
e.persist();
setIdx(x => (x + 1) % comps.length);
}}
>
{transitions.map(({ item, props, key }) => { // ★★
const Comp = comps[item];
return <Comp key={key} style={props} />;
})}
</div>
);
};
export default SampleTransition;
上記で、compsは切り替えをおこなう候補としてのコンポーネントの一覧です。
★で、切り替える現在のインデックスをuseTrainsionの第一引数に与えていきます。
useTraisitionの返り値は、このインデックス値に加えて、前回のトランジションアニメーションが終了していないもののインデックス値がかえってきます。これらはアンマウントしてはいけません。
一般には、useTraisitionが返す、「インデックス値をキーに含む配列要素」すべてに対して、インデックス値対応するコンポーネントをmapで無条件にマウントしてやればよいわけです(★★)。
(5) useChain Hook
Spring,Trails,Transisionなどによる効果を連鎖的に実行する。
- Springなどのアニメーション値を作成する際のアニメーションプリミティブのrefプロパティを指定し、useRefの結果を組込むすることで、useChainがrefを使ってトリガ関数の役割りを果してくれるようになります。逆に言えば、ref属性を組込むとトリガ関数経由ではコントロールできなくなります。
- このせいか、useSpringの(B)「アニメーション値とそのトリガ関数」のパターンのものに対してはuseChainは機能しません。
useChainによるアニメーションの例
useChainコード例(SampleChain.tsx)
refを準備し、制御下におく部品に組み込み、chainで繋げます
import React, { useState, useRef } from "react";
import { useSpring, useChain, animated, config } from "react-spring";
const SampleSpring = ({ ref }) => {
const [enter, setEnter] = useState(false);
const ref1 = useRef();
const ref2 = useRef();
const spring1 = useSpring({
fontSize: enter ? "48pt" : "18pt",
ref: ref1
});
const spring2 = useSpring({
fontSize: enter ? "48pt" : "18pt",
ref: ref2
});
useChain([ref1, ref2]);
return (
<div
style={{ textAlign: "center" }}
onMouseEnter={e => setEnter(p => !p)}
onMouseLeave={e => setEnter(p => !p)}
>
<animated.div style={spring1}>Hello React Spring</animated.div>
<animated.div style={spring2}>Hello React Spring</animated.div>
</div>
);
};
export default SampleSpring;
これはFRP(Functional Reactive Programming)か?
I think so.
おわりに
ということで、react-springによる最先端Webアニメーション技術のサワリを紹介しました。
今回、紹介したのは、react-springの機能の一部ですが、主要なところはカバーしたつもりです。
本書のデモでは主に、fontSizeという地味な属性を変化させましたが、transform: scale, rotateなどのプロパティを変化させたり、SVGを使用すると複雑で派手なアニメーションを行うことができ、基本は同じです。
公式サイトには他に多数のデモが掲載されていますので参考ください。
もうアニメーションも怖くない! かも!
参考リンク
-
GitHubスター数15.4k(2019年12月現在)となかなかの人気なのではないかと思います。 ↩
-
「世界一わかりやすい「イージング」と、その応用」などが参考になります: ↩