react-scrollable-canvas：スクロール可能なcanvasをつくるためのReactコンポーネントライブラリ

# react-scrollable-canvas

先日、スクロール可能なcanvasをつくるためのReactコンポーネントライブラリを作りました。

- ドキュメント・サンプル： https://solt9029.github.io/react-scrollable-canvas
- GitHub： https://github.com/solt9029/react-scrollable-canvas
- npm： https://www.npmjs.com/package/react-scrollable-canvas


本記事では、そのライブラリ（react-scrollable-canvas）の使い方についてご紹介します。
別記事にて、Reactコンポーネントライブラリの作成手順（TypeScript）についてもご紹介する予定です。



## 対象読者

- canvasに興味がある
- Reactの基本について理解している



## スクロール可能なCanvasとは

canvas要素とは、図形を描くことができるHTMLの要素です。
画面に一度に見せられる面積よりも大きな図形などを描画したい場合、スクロール可能にさせる必要があります。
しかし、大きいcanvas単体で配置した場合、スクロールさせたりすることはできません。
そのため、以下のようにdiv要素をネストさせて配置する必要があります。

```html
<div style="overflow: scroll;"> <!-- サイズは画面上で表示される面積 -->
  <div style="overflow: hidden;"> <!-- サイズは図形を描画したい全体の面積 -->
    <div style="position: relative;">
      <!-- canvasはスクロール量に合わせてtranslateXとtranslateYを変更 -->
      <!-- canvasのサイズは画面上で表示される面積 -->
      <canvas style="position: absolute;"></canvas> 
    </div>
  </div>
</div>
```

canvas要素は大きければ大きいほどメモリの使用量が大きくなるらしく、
スクロール可能なcanvasを作る場合には、画面上で表示される面積だけの大きさにしておきたいです。
そのため、上記のHTMLでは、positionがabsoluteのcanvasをpositionがrelativeのdivでラッピングし、
スクロール量に合わせてtranslateXとtranslateYを変化させることによって、canvasの画面上で表示される面積に抑えています。

本ライブラリが提供するコンポーネントでは、上記のdivのラッピングを行われた状態のcanvasが提供されます。



## 使用手順

### インストール

Reactプロジェクト内で、以下コマンドを利用しreact-scrollable-canvasをインストールします。
（本ライブラリはTypeScriptで作成されているため、型定義ファイルを別途インストールする必要はありません。）

```
npm install react-scrollable-canvas
```

### コーディング

#### 単純なスクロール可能なcanvas

早速スクロール可能なcanvasを作っていきます。
例として、画面上に表示される面積は300*300px, 図形を描画したい全体の面積は600*600pxとします。

```jsx
import React, { Component, createRef } from 'react';
import { ScrollableCanvas } from './react-scrollable-canvas';

const WIDTH = 300;
const HEIGHT = 300;
const LARGE_WIDTH = 600;
const LARGE_HEIGHT = 600;
const CIRCLE_RADIUS = 5;
const CIRCLE_SIZE = 30;

export default class ScrollableCanvasExample extends Component {
  canvasRef = createRef();

  /**
   * スクロールされた時に実行される関数
   * 上方向のスクロール量、横方向のスクロール量が引数として渡される
   */
  draw = (scrollTop, scrollLeft) => {
    // canvasのcontextを利用した描画処理
    this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
    for (let y = -scrollTop % CIRCLE_SIZE; y < HEIGHT - (scrollTop % CIRCLE_SIZE); y += CIRCLE_SIZE) {
      for (let x = -scrollLeft % CIRCLE_SIZE; x < WIDTH - (scrollLeft % CIRCLE_SIZE); x += CIRCLE_SIZE) {
        this.ctx.beginPath();
        this.ctx.arc(x + CIRCLE_SIZE / 2, y + CIRCLE_SIZE / 2, CIRCLE_RADIUS, 0, 360, false);
        this.ctx.fillStyle = `rgba(${(scrollLeft + x) / 2}, ${(scrollTop + y) / 2}, 128, 0.8)`;
        this.ctx.fill();
      }
    }
  };

  componentDidMount() {
    this.ctx = this.canvasRef.current.getContext('2d');
    this.draw(0, 0);
  }

  render() {
    return (
      <ScrollableCanvas
        width={WIDTH} // 画面上で表示される幅
        height={HEIGHT} // 画面上で表示される高さ
        largeWidth={LARGE_WIDTH} // 図形を描画したい全体の幅
        largeHeight={LARGE_HEIGHT} // 図形を描画したい全体の高さ
        canvasRef={this.canvasRef} // canvas要素を参照する変数
        onScroll={this.draw} // スクロールした時に呼び出される関数
      />
    );
  }
}

```

以上のコードにより、以下のようなcanvasをつくることができます。

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141427/90aa9973-6e24-82cd-c2dc-9e57d5ccaa7e.gif)



#### レイヤ構造になったスクロール可能なcanvas

canvas同士を重ねることによって、レイヤを表現することもできます。
最初の例よりは複雑になりますが、以下のようにコードを書くことで実現できます。

```jsx
import React, { Component, createRef } from 'react';
import { ScrollableCanvasContainer, Canvas } from './react-scrollable-canvas';

const WIDTH = 300;
const HEIGHT = 300;
const LARGE_WIDTH = 600;
const LARGE_HEIGHT = 600;
const CIRCLE_RADIUS = 5;
const CIRCLE_SIZE = 30;

export default class ScrollableCanvasContainerExample extends Component {
  frontCanvasRef = createRef();
  backCanvasRef = createRef();

  state = { scrollTop: 0, scrollLeft: 0 };

  draw = () => {
    const { scrollTop, scrollLeft } = this.state;

    // draw canvas here.
    this.frontCtx.clearRect(0, 0, WIDTH, HEIGHT);
    for (let y = -scrollTop % CIRCLE_SIZE; y < HEIGHT - (scrollTop % CIRCLE_SIZE); y += CIRCLE_SIZE) {
      for (let x = -scrollLeft % CIRCLE_SIZE; x < WIDTH - (scrollLeft % CIRCLE_SIZE); x += CIRCLE_SIZE) {
        this.frontCtx.beginPath();
        this.frontCtx.arc(x + CIRCLE_SIZE / 2, y + CIRCLE_SIZE / 2, CIRCLE_RADIUS, 0, 360, false);
        this.frontCtx.fillStyle = `rgba(${(scrollLeft + x) / 2}, ${(scrollTop + y) / 2}, 128, 0.8)`;
        this.frontCtx.fill();
      }
    }
    this.backCtx.clearRect(0, 0, WIDTH, HEIGHT);
    this.backCtx.beginPath();
    const gradient = this.backCtx.createLinearGradient(-scrollLeft, -scrollTop, LARGE_WIDTH, LARGE_HEIGHT);
    gradient.addColorStop(0.0, 'rgb(255, 0, 0)');
    gradient.addColorStop(0.5, 'rgb(0, 255, 0)');
    gradient.addColorStop(1.0, 'rgb(0, 0, 255)');
    this.backCtx.fillStyle = gradient;
    this.backCtx.rect(-scrollLeft, -scrollTop, LARGE_WIDTH, LARGE_HEIGHT);
    this.backCtx.fill();
  };

  onScroll = (scrollTop, scrollLeft) => {
    this.setState({ scrollTop, scrollLeft });
  };

  componentDidUpdate() {
    this.draw();
  }

  componentDidMount() {
    this.frontCtx = this.frontCanvasRef.current.getContext('2d');
    this.backCtx = this.backCanvasRef.current.getContext('2d');
    this.draw();
  }

  render() {
    const { scrollTop, scrollLeft } = this.state;

    return (
      <>
        <ScrollableCanvasContainer
          width={WIDTH}
          height={HEIGHT}
          largeWidth={LARGE_WIDTH}
          largeHeight={LARGE_HEIGHT}
          onScroll={this.onScroll}
        >
          <Canvas
            ref={this.backCanvasRef}
            width={WIDTH}
            height={HEIGHT}
            translateX={scrollLeft}
            translateY={scrollTop}
          />
          <Canvas
            ref={this.frontCanvasRef}
            width={WIDTH}
            height={HEIGHT}
            translateX={scrollLeft}
            translateY={scrollTop}
          />
        </ScrollableCanvasContainer>
      </>
    );
  }
}

```

ScrollableCanvasContainerは 「##スクロール可能なCanvasとは」でご紹介した内の3層のdiv要素を提供しています。
その中に、自前でcanvasを必要なレイヤの数の分だけ配置することで、レイヤ構造を表現することができます。
自前でcanvasにtraslateXとtranslateYを指定する必要があるため、
stateとして上方向・横方向のスクロール量を保存する必要があります。



#### その他

スクロール可能なcanvasでクリックイベントも拾いたい場合は、
ScrollableCanvasにonClickを設定したdivをラッピングすることで実現できます。

また、cssで幅100%を設定したり高さ50%を設定したりしたい場合には、
[react-measure](https://github.com/souporserious/react-measure)というライブラリと組み合わせて使うことで実現できます。

こちらでサンプルコードを確認することができるため、興味のある方はご参照ください。
https://solt9029.github.io/react-scrollable-canvas/#/examples



## おわりに

本記事では、スクロール可能なCanvasをつくるためのReactコンポーネントライブラリである
react-scrollable-canvasの使い方について解説し、その例をあげました。

TypeScriptでReactコンポーネントライブラリを作成するという視点から別記事を書く予定です。
よろしくお願いします。