React+TypeScriptではじめるp5.jsチュートリアル

この記事ではp5.jsをReactと一緒に使えるようプロジェクトの構築方法をまとめた記事です。記事前半ではTypeScriptやPercelについても言及しているので、p5を使わない場合でも環境構築の参考にしていただけたら嬉しいです。

## p5.jsとは

p5.jsとはクリエイティブコーディングで使われるJavaScriptライブラリです。以下のような表現豊かな描写が可能になります。


![Untitled_ Dec 1 2019 1_04 AM - Edited.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/c1694a25-5b4a-47ca-61d2-369404b2060d.gif)


## 確かにp5はかっこいいけどp5だけで使いたいわけじゃない

[公式チュートリアル](https://p5js.org/get-started/)のページを覗くと以下のような画像とともにコーディングの初め方が丁寧に解説されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/dd5e0b4c-9f9f-ef63-e5c0-14617055add9.png)

上記をご覧いただくと分かるように、既に`setup()`や`draw()`と行った関数が指定されていて、そこに以下のように記述すると描画することができる仕組みとなっています。

```javascript:sketch.js
function setup() {

}

function draw() {
  ellipse(50, 50, 80, 80);
}
```

```html:index.html
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/p5@0.10.2/lib/p5.js"></script>
    <script src="sketch.js"></script>
  </head>
  <body>
  </body>
</html>
```

ただ、通常のプロジェクトに組み込む場合、htmlやjsファイルを以下のように使うことは稀で、今だとVueやReact等のフレームワークと一緒に使用することが大半かと思います。

そこで今回はReactをベースとしてp5.jsを組み込んだ場合の構築方法をチュートリアル形式でまとめていきます。

## 1. プロジェクト作成

まずはプロジェクトの雛形を作るために以下のコマンドを入力します。今回は`npm init`ではなく`yarn init`を使っていきます。

```
$ brew install yarn
$ yarn init
```
また、Webpackではなく`Percel`を使ってプロジェクトをビルドしていきます。
- :pencil: [Percelとは](https://parceljs.org/)

```
$ yarn global add parcel-bundler
```

最低限であるReactとp5をプロジェクトに追加します。

```
$ yarn add @types/p5 @types/react @types/react-dom
```

ライブラリを追加したので、ここからはファイルの中身を作成していきましょう。
ここで必要になるのは骨格となるファイル3つとTypeScript用の設定ファイル1つで計4つのみです。どれもシンプルな最小限の記述にしています。

```index.html
<html>
<head>
</head>
<body>
    <div id="root"></div>
    <script src="./index.tsx"></script>
</body>
</html>
```

```index.tsx
import * as React from "react"
import { render } from "react-dom"
import App from './App'

const Root = () => {
    return <App/>
}
const rootElement = document.getElementById("root")
render(<Root />, rootElement)
```

```App/index.ts
import React from 'react'

const App = () => {
    return (
        <p>Hello World!</p>
    )
}
export default App
```

```tsconfig.json
{
    "compilerOptions": {
        "typeRoots": [
            "./node_modules/@types",
            "./@types"
        ],
        "traceResolution": false,
        "module": "esnext",
        "target": "es5",
        "lib": [
            "es2015",
            "es2017",
            "es6",
            "es7",
            "es5",
            "dom",
            "scripthost"
        ],
        "jsx": "react",
        "experimentalDecorators": true,
        "moduleResolution": "node",
        "baseUrl": "./",
        "paths": {
            "~*": ["./*"]
        },
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "esModuleInterop": true,
        "importHelpers": true,
        "strict": true,
        "sourceMap": true,
        "resolveJsonModule": true
    },
    "include": [
        "**/*",
    ]
}
```

ここまででプロジェクトは以下のようになっているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/64a51e69-223a-45aa-9a2b-9cc1165dfe04.png)

それでは実行してみましょう。以下のコマンドをプロジェクトのルートから打ち込むと画面(`http://localhost:1234`)に文字が表示されるはずです。

```
$ parcel ./index.html
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/e02f60ae-8fa2-b6f2-a81f-cfa7791e05ad.png)

## 2. p5.js用キャンバスの追加

ここからp5を使って単純なサークルを画面に描いていきます。[公式チュートリアル](https://p5js.org/get-started/)にある関数`ellipse`を使って以下のファイルを追加しましょう。

```typescript:sketch/circle.ts
import p5 from 'p5'

const circle = (p: p5) => {
    p.setup = () => {
    }
    p.draw = () => {
        p.ellipse(50, 50, 80, 80)
    }
}
export default circle
```

```tsx:App/Canvas.tsx
import React, { useEffect } from "react"
import p5 from 'p5'

const Canvas = (props: any) => {
    useEffect(() => {
        new p5(props.sketch)
    }, [props.sketch])
    return (<></>)
}
export default Canvas
```

ここまででプロジェクト構成は以下のようになります。`node_modules`等一部のディレクトリは自動的に追加されているものですので手動での操作は不要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/ea928aa0-a23f-36c2-6739-660da3d38c90.png)


`index.tsx`は以下のように修正します。

```tsx:index.tsx
import React from 'react'
import sketch from '~sketch/circle'
import Canvas from './Canvas'

const App = () => {
    return (
        <>
            <p>Hello World!</p>
            <Canvas sketch={sketch} />
        </>
    )
}
export default App
```

以下のように画面が更新されていることが確認できるかと思います。

<img width="565" alt="Screen Shot 2019-11-28 at 18.18.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/68b0ac41-c88a-6b5a-483b-fc2b8b541aa3.png">

## 3. Headerの追加

画面にヘッダーを追加して構成を整えます。以下のファイルを追加してください。

```tsx:App/Header.tsx
import React from "react"
import { AppBar, Typography, Toolbar, IconButton } from '@material-ui/core'
import MenuIcon from '@material-ui/icons/Menu'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'

const useStyles = makeStyles((theme: Theme) =>
    createStyles({
        root: {
            flexGrow: 1,
        },
        menuButton: {
            marginRight: theme.spacing(2),
        },
        title: {
            flexGrow: 1,
        },
    }),
);

const Header = () => {
    const classes = useStyles({})
    return (
        <AppBar>
            <Toolbar>
                <IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="menu">
                    <MenuIcon />
                </IconButton>
                <Typography variant="h6" className={classes.title}>
                    Tutorial
                </Typography>
            </Toolbar>
        </AppBar>
    )
}

export default Header
```

Headerを`App/index.tsx`に追加します。

```tsx:App/index.tsx
import React from 'react'
import sketch from '~sketch/circle'
import Canvas from './Canvas'
import Header from './Header'

const App = () => {
    return (
        <>
            <Header/>
            <Canvas sketch={sketch} />
        </>
    )
}
export default App
```

せっかくなので描画もインタラクティブなものへアップデートしましょう。

```sketch/circle.ts
import p5 from 'p5'

const circle = (p: p5) => {
    p.setup = () => {
        p.createCanvas(640, 480);
    }
    p.draw = () => {
        if (p.mouseIsPressed) {
            p.fill(0);
        } else {
            p.fill(255);
        }
        p.ellipse(p.mouseX, p.mouseY, 80, 80)
    }
}

export default circle
```

ここまでで、ヘッダーを含めたWebアプリケーションの一部としてp5.jsを組み込んできました。

マウスを画面上で動かしてみましょう。以下のような描画が可能になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/9a64d475-4fd2-e672-bb78-5cfd2d6b0d12.png)

## おまけ：3Dグラフィックスの追加

p5.jsを使った３D描画を行うため、以下のファイルを追加して試してみましょう。

```typescript:sketch/sincos.ts
import p5 from 'p5'

const sincos = (p: p5) => {
    p.setup = () => {
        p.createCanvas(
            p.windowWidth,  // 画面いっぱいまで拡大
            p.windowHeight, // 画面いっぱいまで拡大
            p.WEBGL)        // WebGL
    }
    p.draw = () => {
        p.background(0)     // 背景を黒
        for (let i = 0; i < 3; i++) {
            p.translate(100, 100, 100)
            p.sphere(20, 10, 5)
        }

    }
}

export default sincos
```

`sphere`は球体を表示します。今回は3つループを回しているので3つの球体が表示されていますが、`translate`を使用することでx軸y軸z軸それぞれを少しずつずらしています。3つ目の球体は他に比べて手前に表示されているのが確認できます。

<img width="592" alt="Screen Shot 2019-12-01 at 1.15.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/7cd3a857-1e7e-d1ae-f1f7-c6b1e22cff12.png">

次に、フレームごとにX軸を中心とした回転を加えてみましょう。

```typescript:sketch/sincos.ts
...
    p.draw = () => {
        p.background(0)
        for (let i = 0; i < 3; i++) {
            p.translate(100, 100, 100)
            p.sphere(20, 10, 5)
            p.rotateX(p.frameCount * 0.01) // 追加
        }

    }
...

```

1つ前の球体を中心とした軸をもち、X軸に対して回転していることがわかります。

![Untitled_ Dec 1 2019 1_27 AM - Edited.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/d7c85ea4-c627-e571-35c3-a0ab47254d8f.gif)

続いてsinとcosを使用した運動の描画です。`translate`で指定した定数を以下のように書きかえてみます。

```typescript:sketch/sincos.ts
...
        p.background(0)
        for (let i = 0; i < 3; i++) {
            p.translate(
                10 * p.sin(p.frameCount * 0.1 + i),
                10 * p.cos(p.frameCount * 0.1 + i), 
                50)
            p.sphere(20, 10, 5)
        }
...
```

EXILEのチューチュートレインはsinとcosによって再現できることがわかります。

![Untitled_ Dec 1 2019 1_35 AM - Edited.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/b3846afe-a52b-14be-32fe-bd5a74199496.gif)

最後に、ここまでの要素を組み合わせていきましょう。X軸回転に加えてY軸Z軸にも回転を加えていきます。さらに、よりインパクトのある表現となるようオブジェクトの個数を80まで増やしていきます。色も白から艶やかな色へと変更してみます。

```typescript:sketch/sincos.ts
...
import p5 from 'p5'

const sincos = (p: p5) => {
    p.setup = () => {
        p.createCanvas(
            p.windowWidth,
            p.windowHeight,
            p.WEBGL)
        p.colorMode(p.HSB, 100) // 色の追加
    }
    p.draw = () => {
        p.background(0)
        for (let i = 0; i < 80; i++) {
            p.fill(50, 55, 100) // 色の追加
            p.translate(
                10 * p.sin(p.frameCount * 0.1 + i), 
                10 * p.cos(p.frameCount * 0.1 + i), 
                50)
            p.sphere(20, 10, 5)
            p.rotateX(p.frameCount * 0.01)
            p.rotateY(p.frameCount * 0.01)
            p.rotateZ(p.frameCount * 0.01)
        }
    }
}

export default sincos
...
```
![Untitled_ Dec 1 2019 1_43 AM - Edited.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/8de93539-0ca8-7d25-af87-18c6c3154baa.gif)


## さらなる拡張

今回のチュートリアルではプロジェクトの骨格となるファイルを作成していきました。
ヘッダーを追加した要領でサイドバーやポップアップを追加することも可能です。また、`sketch`ディレクトリの下に`circle.ts`以外のファイル（例えば`triangle.ts`など）を作成し、画面の描画を動的に変えることも可能です。以下にソースコードを置いておくのでぜひ独自の拡張を加えてみてください。

https://github.com/shunp/p5_react