[React.js]React-Move入門


先日2.0がリリースされたこともあり、React-Moveを試してみました。


![react-move-sample](https://yoshiromasuhara.github.io/react-move-sample/img/react-move-sample.png)


## はじめに
React-MoveはReactでデータ駆動によるHTMLやSVGのアニメーションを行うツールです。
D3のようにグラフを作ることができて、状態変化のアニメーションが柔軟にカスタマイズできるような感じです。

例えばComponentのstateとSVGの属性などを紐付けて、動的かつスムーズにアニメーションすることができます。
アニメーションにはstart、enter、update、leaveとデータの動きに合わせて処理を行うことができます。
また[d3-interpolate](https://github.com/d3/d3-interpolate)の便利機能も利用することが可能です。
上記以外にも軽量とかReact-Nativeでも利用できるなど、詳細は[React-MoveのFeatures](https://github.com/d3/d3-interpolate)をご確認ください。
※補足としてReact-Move2.0にて[Resonance](https://github.com/sghall/resonance)([react-d3-transitions](https://github.com/sghall/react-d3-transitions/))が統合されました。

## 機能の概要

### NodeGroup
React-MoveのキモとなるNodeGroupコンポーネントです。

プロパティに描画するデータとTransitions（start、enter、update、leave）をセットします。
NodeGroupコンポーネントのinnerで、プロパティにセットしたデータ毎の描画要素を記述します。

※以下コードのプロパティとinnerの関数は必須です。

```js
<NodeGroup
  data={this.state.data} 
  
  keyAccessor={(d) => d.name} 

  start={(data, index) => ({ 
    ...
  })}
>
  {(nodes) => ( 
    ...
      {nodes.map(({ key, data, state }) => {
        ...
      })}
    ...
  )}
</NodeGroup>
```


### Transitions
start、enterプロパティなど所定の状態変化に対して関数形式で記述します。
ここでは移動値、色、サイズなどのスタイルをオブジェクトで返します。
後述しますが、duration, delay, easingなどはTimingを利用し、個別イベントをEventsで指定できます。
※もしトランジション中にデータが変更された場合、内部ではきちんと最終的な値を保持します。

```js
// start - starting state of the node. Just return an object.
start={(data, index) => ({
  opacity: 1e-6,
  x: 1e-6,
  fill: 'green',
  width: scale.bandwidth(),
})}

// enter - return an object or array of objects describing how to transform the state.
enter={(data, index) => ({
  opacity: [0.5], // transition opacity on enter
  x: [scale(data.name)], // transition x on on enter
  timing: { duration: 1500 }, // timing for transitions
})}
```


### Timing

基本的にはデフォルトが適用され、上書きしたい項目のみ指定することが可能です。
以下がデフォルト値となります。

```js
const defaultTiming = {
  delay: 0,
  duration: 250,
  ease: easeLinear,
};
```

また以下の様に複数のオブジェクトを指定することも可能です。

```js
enter={(data, index) => ([
  {
    opacity: [0.5],
    timing: { duration: 1000 },
  },
  {
    x: [scale(data.name)], 
    timing: { delay: 750, duration: 1500, ease: easeQuadInOut }, 
  },
])}
```

### Events
イベントはD3と同じで、Transitionのstart、interrupt、endの処理が記述できます。

```js
update={(data) => ({
  opacity: [0.5],
  width: [scale.bandwidth()],
  timing: { duration: 1500, ease: easeQuadInOut },
  events: {
    start() {
      console.log('start!', data, this);
    },
    interrupt() {
      console.log('interrupt!', data, this);
    },
    end() {
      console.log('end!', data, this);
      this.setState({ fill: 'tomato' });
    },
  },
})}

```

             
## 公式サンプル
[公式サンプルのソース](https://codesandbox.io/s/j4mv3lvj6v)を少し解説します。

### import句および定数宣言
D3関連のモジュールについて、`range`は後でダミーデータ作成時に利用しています。
`scaleLinear`を利用して画面の横幅に合わせたスケールオブジェクトを作成します。
これは後述のマウス移動時のX座標に合わせて円図形の背景色を識別します。


``` js:
import React from "react";
import { render } from "react-dom";
import { range } from "d3-array";
import { scaleLinear, interpolateInferno } from "d3-scale";
import "./demo.css";

// React-Move 2.0
import { easeElastic } from "d3-ease";
import { NodeGroup } from "react-move";

// A scale for changing the color
const linear = scaleLinear().domain([0, window.innerWidth]);

```


### コンポーネントの初期設定およびイベントハンドラ処理

`this.state = { x: 250, y: 300 }`をデフォルト設定としてマウスの移動で`state`を更新します。
マウスイベント内でコンテキストが変わってしまうため、`constructor`で`bind`しています。


``` js:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = { x: 250, y: 300 };

    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.handleTouchMove = this.handleTouchMove.bind(this);
  }

  componentDidMount() {
    // Track mouse/touch movement
    window.addEventListener("mousemove", this.handleMouseMove);
    window.addEventListener("touchmove", this.handleTouchMove);
  }

  handleMouseMove({ pageX: x, pageY: y }) {
    // Update the state with cursor position
    this.setState({ x, y });
  }

  handleTouchMove({ touches }) {
    // Update the state with touch position
    this.handleMouseMove(touches[0]);
  }

```

### NodeGroupコンポーネントのプロパティ
`data`には`range`を利用して6件のデータをセットし`x,y`には`state`の値を紐付けます。

`keyAccessor={d => d.key}` で受け取ったデータのキーを戻す関数を用意します。
これは内部でどのデータが入ってくるか確認するために利用します。

`start`では開始位置を指定し、`update`（マスポインタの移動）では更新後の位置に`timing`を利用して移動します。
以下では先頭データから順に0.12秒後に1.3秒かけて`easeElastic`の動きで移動します。

``` js:

  render() {
    return (
      // React-Move!
      <NodeGroup
        data={range(6).map(d => {
          return {
            key: `key-${d}`,
            x: this.state.x,
            y: this.state.y
          };
        })}
        keyAccessor={d => d.key}
        start={data => {
          return { x: data.x, y: data.y };
        }}
        update={(data, index) => {
          return {
            x: [data.x],
            y: [data.y],
            timing: {
              delay: index * 120,
              duration: 1300,
              ease: easeElastic
            }
          };
        }}
      >
```


### NodeGroupコンポーネントの中身

データ１件ずつに対して`<div>`を用意し、スタイルで円図形にして描画を行います
D3の`interpolateInferno`カラーテーマを利用して、x座標に紐づくカラーコードで円図形を描画します。
`translate3d`では円の中心がマウスポインタの位置になるように調整しています。
`<div>`で`position`は指定していないため順番に並んで表示されます。
`zIndex`では移動中の重なりを制御しています。

``` js:
        {nodes => {
          // Just a function!
          return (
            <div>
              {nodes.map((node, index) => {
                const { x, y } = node.state;

                return (
                  <div
                    key={node.key}
                    style={{
                      backgroundColor: interpolateInferno(linear(x)),
                      width: 50,
                      height: 50,
                      borderRadius: 25,
                      opacity: 0.7,
                      WebkitTransform: `translate3d(${x - 25}px, ${y -
                        25}px, 0)`,
                      transform: `translate3d(${x - 25}px, ${y - 25}px, 0)`,
                      zIndex: nodes.length - index + 5000
                    }}
                  />
                );
              })}
            </div>
          );
        }}
      </NodeGroup>
    );
  }
}

render(<Example />, document.getElementById("root"));


```

## 公式サンプルをローカルで動かす
![react-move-sample](https://yoshiromasuhara.github.io/react-move-sample/img/react-move-sample.gif)

別の[公式サンプル](https://react-move.js.org/#/examples/simple)を自分の環境に合わせて動くようにしました。[デモ](https://yoshiromasuhara.github.io/react-move-sample/)、[ソース](https://github.com/yoshiromasuhara/react-move-sample)
手元で動かしたい方は参考にしていただけたらと。