react-p5ライブラリについて

react-p5は、Reactコンポーネント内でp5.jsを使用するためのラッパーです

Next.jsの環境でp5.jsを使いたかったのですが、「react-p5ライブラリ」を利用すると、スムーズに実装できました。

目次
  1. 基本的な使い方
  2. Props
  3. p5.jsで蛍を作ってみました

基本的な使い方

Sketchコンポーネントが主役となります。
このSketchコンポーネントは、p5.jsで行う通常のsetupやdraw関数をReactコンポーネントとして扱えるようにする優れものです。
これにより、Reactでの状態管理やライフサイクルメソッドと、p5.jsの描画機能をスムーズに組み合わせることができます。

Next.jsを使っている場合、Sketchコンポーネントはクライアントサイドでのみレンダリングされるため、サーバーサイドレンダリング(SSR)に起因する問題を気にせずに済みます。

インストール

npm i react-p5

setup関数: キャンバスのサイズを設定するなど、初期設定を行います。
draw関数: 画面の描画内容を定義します。この例では円を描画しています。

import React from "react";
import Sketch from "react-p5";

let x = 50;
let y = 50;

export default (props) => {
  const setup = (p5, canvasParentRef) => {
    p5.createCanvas(500, 500).parent(canvasParentRef);
  };

  const draw = (p5) => {
    p5.background(0);
    p5.ellipse(x, y, 70, 70);
    x++;
  };

  return <Sketch setup={setup} draw={draw} />;
};

SSR環境での使用
Next.jsなどのサーバーサイドレンダリング(SSR)環境では、windowオブジェクトが利用できないためエラーになることがあります。
これを回避するためにdynamic importsを使用てSketchをインポートします

import dynamic from 'next/dynamic';

// クライアント側でのみ「react-p5」をインポートします
const Sketch = dynamic(() => import('react-p5').then((mod) => mod.default), {
  ssr: false,
});

イベントハンドリングを独自に定義し、それをSketchコンポーネントにPropsとして渡すことが可能です

import React, { useState } from 'react';
import Sketch from "react-p5";

function App() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>クリック回数: {count}</p>
//<Sketchのpropsで渡す
      <Sketch setup={setup} draw={draw} mouseClicked={handleClick} />
    </div>
  );
}

// 以下はp5.jsの関数です
function setup(p5, canvasParentRef) {
  p5.createCanvas(400, 400).parent(canvasParentRef);
}

function draw(p5) {
  p5.background(200);
  p5.ellipse(200, 200, 50, 50);
}

export default App;

p5.soundを使う場合はp5.soundライブラリを単にインポートするだけです

import Sketch from 'react-p5';
import 'p5/lib/addons/p5.sound';

p5.sound + Next.js(またはSSRをサポートしている他のフレームワーク)の場合は

import dynamic from 'next/dynamic'

const Sketch = dynamic(() => import("react-p5").then((mod) => {

  // react-p5がロードされた後、コードの実行時に動的にp5.soundをロード
  require('p5/lib/addons/p5.sound');

  return mod.default
}), {
  ssr: false
});

Props

  • className(任意): キャンバスの親要素に適用するCSSクラス名。
  • style(任意): キャンバスの親要素に適用するスタイル(JavaScriptオブジェクト形式)。
  • setup(必須): プログラムが開始するときに一度だけ呼び出される関数。
  • draw(任意): setup()の直後に継続的に呼び出される関数。
  • windowResized(任意): ブラウザウィンドウがリサイズされるたびに一度呼び出される関数。
  • preload(任意): setup()の前に非同期の外部ファイルのローディングを行う関数。
  • mouseClicked(任意): マウスボタンが押されてから放されると一度呼び出される関数。
  • mouseMoved(任意): マウスが移動するたびに呼び出される関数(マウスボタンは押されていない)
  • doubleClicked(任意): マウスボタンがダブルクリックされると呼び出される関数。
  • mousePressed(任意): マウスボタンが押されると一度呼び出される関数。
  • mouseWheel(任意): マウスホイールが動かされると呼び出される関数。
  • mouseDragged(任意): マウスがドラッグされる(マウスボタンが押されている状態で移動する)と呼び出される関数。
  • mouseReleased(任意): マウスボタンが放されると呼び出される関数。
  • keyPressed(任意): キーが押されると一度呼び出される関数。
  • keyReleased(任意): キーが放されると一度呼び出される関数。
  • keyTyped(任意): キーが押されると一度呼び出される関数(特定のアクションキーは除く)。
  • touchStarted(任意): タッチが登録されると一度呼び出される関数。
  • touchMoved(任意): タッチが移動するたびに呼び出される関数。
  • touchEnded(任意): タッチが終了すると呼び出される関数。
  • deviceMoved(任意): デバイスがX、Y、Z軸に沿って一定量移動すると呼び出される関数。
  • deviceTurned(任意): デバイスが90度以上連続で回転すると呼び出される関数。
  • deviceShaken(任意): デバイスが特定の閾値以上揺れると呼び出される関数。

p5.jsで蛍を作ってみました

import dynamic from "next/dynamic";
// react-p5を動的にインポートするためのNext.jsのdynamic関数
const Sketch = dynamic(() => import("react-p5").then((mod) => mod.default), {
  ssr: false,
});
// アニメーションのステータスを定義(ここでは3としています)
const ANIMATION_ENABLED = 3;
export default function Fireflies({ animationStatus }) {
  class Jitter {
    constructor(p) {
      this.p = p;
      this.x = p.random(p.width);
      this.y = p.random(p.height);
      this.diameter = p.random(5, 15);
      this.hue = p.random(0, 60);
      this.speed = 1;
      this.phase = p.random(0, 2 * p.PI);
      this.frequency = p.random(0.01, 0.3);
      // オフセットする距離(ここでは円の半径の40%)
      let offsetDistance = 0.5 * this.diameter;
      // ランダムな角度(0から360度)
      let angle = Math.random() * 2 * Math.PI;
      // 角度と距離を使用してxとyのオフセットを計算
      this.offsetX = offsetDistance * Math.cos(angle);
      this.offsetY = offsetDistance * Math.sin(angle);
    }
  // 透明度を更新する関数
    updateAlpha() {
      this.phase += this.frequency;
      this.alpha =
        ((this.p.sin(this.phase) + 1) / 2) * (1 - this.diameter / 30); // sinの結果は-1から1なので、0から1に正規化
    }
  // 座標を移動する関数
    move() {
      this.updateAlpha();
      this.x += this.p.random(-this.speed, this.speed);
      this.y += this.p.random(-this.speed, this.speed);
      this.x = this.p.constrain(this.x, 0, this.p.width);
      this.y = this.p.constrain(this.y, 0, this.p.height);
    }
   // 描画する関数
    display() {
      this.p.noStroke();
      this.p.fill(this.hue, 100, 50, this.alpha * 0.1);
      this.p.ellipse(
        this.x,
        this.y,
        this.diameter * this.diameter * 1.5,
        this.diameter * this.diameter * 1.5
      );
      this.p.fill(this.hue, 100, 100, this.alpha);
      this.p.ellipse(this.x, this.y, this.diameter, this.diameter);
      this.p.fill(0, 0, 0, 0.5);

      this.p.ellipse(
        this.x + this.offsetX,
        this.y + this.offsetY,
        this.diameter * 0.3,
        this.diameter * 0.3
      );
    }
  }
  // Jitterのインスタンスを保持する配列
  let bugs = [];
  // p5.jsのsetup関数(初期化用)
  const setup = (p5, parentRef) => {
    p5.createCanvas(p5.windowWidth, p5.windowHeight).parent(parentRef);
    p5.frameRate(10);
    p5.colorMode(p5.HSB);
    for (let i = 0; i < 7; i++) {
      bugs.push(new Jitter(p5));
    }
  };
  // マウスが押されたときの処理
  const mousePressed = (p5) => {
    animationStatus !== ANIMATION_ENABLED ? p5.noLoop() : p5.loop();
    for (let i = 0; i < 5; i++) {
      const bug = new Jitter(p5);
      bug.x = p5.mouseX + p5.random(-50, 50);
      bug.y = p5.mouseY + p5.random(-50, 50);
      bugs.push(bug);
    }
  };
  // p5.jsのdraw関数(描画用)
  const draw = (p5) => {
    animationStatus !== ANIMATION_ENABLED ? p5.noLoop() : p5.loop();
//キャンバスをクリアしているので背景を暗くすることでホタルが際立ちます
    p5.clear();
    bugs.forEach((bug) => {
      bug.move();
      bug.display();
    });
  };
  // ウィンドウがリサイズされたときの処理
  const windowResized = (p5) => {
    p5.resizeCanvas(p5.windowWidth, p5.windowHeight);
  };
  // Reactコンポーネントとして返す
  return (
    <Sketch
      setup={setup}
      draw={draw}
      mousePressed={mousePressed}
      windowResized={windowResized}
    />
  );
}

注意:上記のコードの場合、bugs(インスタンスを保持する配列)は、リサイズ時に空になるため、蛍は消えます
bugsをuseStateをもちいて管理することで、リサイズ時に空になることを回避することができます

const [bugs, setBugs] = useState([]);

 const setup = (p5, parentRef) => {
    p5.createCanvas(p5.windowWidth, p5.windowHeight).parent(parentRef);
    p5.frameRate(10);
    p5.colorMode(p5.HSB);
    // 初期化
    const initialBugs = Array.from({ length: 7 }, () => new Jitter(p5));
    setBugs(initialBugs);
  };

  const mousePressed = (p5) => {
    animationStatus !== ANIMATION_ENABLED ? p5.noLoop() : p5.loop();

    const newBugs = [
      ...bugs,
      ...Array.from({ length: 5 }, () => {
        const bug = new Jitter(p5);
        bug.x = p5.mouseX + p5.random(-50, 50);
        bug.y = p5.mouseY + p5.random(-50, 50);
        return bug;
      }),
    ];
    setBugs(newBugs);
  };

注意
react-p5は、Reactのコンポーネントライフサイクルとp5.jsのライフサイクルを橋渡しするライブラリです。そのため、setupとdrawの中でReactのステートやプロップスを参照することはできますが、useEffectのようなReactのフックとp5のライフサイクルメソッド(setup, drawなど)を混在させるのは推奨されていない様です

*useStateでステートを管理するにであれば、useEffectを使って初期化を行うのは一般的な方法です。そしてそのuseEffect内で、requestAnimationFrameを使ってdraw関数を繰り返し呼び出す形

useEffect(() => {
  // ここで初期化処理(セットアップ)
  function draw() {
    // アニメーションやデータの更新処理
    requestAnimationFrame(draw);
  }  
  draw();  // 初回呼び出し
  return () => {
    // クリーンアップ処理
  }
}, []);