如何添加 p5.js 社区库,例如 p5.clickable 和 p5.gui 以在 React 项目中使用?

How to add p5.js community libraries such as p5.clickable and p5.gui to use in React projects?

目前我正在使用以下样板来创建我在 react-p5 (https://www.npmjs.com/package/react-p5) 中概述的 p5 草图。我

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

function App() {
  let setup = (p5, canvasParentRef) => {

  };
  let draw = (p5) => {
    
  };
  return (
    <div className="App">
      <Sketch setup={setup} draw={draw} className="App" />
    </div>
  );
}

export default App;

我尝试重写库以匹配上面的格式,然后导出它们并导入为“import Clickable from './clickable.js'”,但没有成功。

我该如何将这些库添加到我的 React 应用程序中?我仍在寻找通过 React 的方法,但这个问题对我来说有点障碍。

不幸的是,大多数 p5.js 附加组件都不是设计良好的 JavaScript 库。现代 JavaScript 库作为导出特定 functions/classes/variables 的模块发布,然后您可以导入这些模块。 are ways 构造 JavaScript 模块,使它们支持不同的包含它们的方式(包括经典的 <script /> 标签导入、ES6 import 语句或 require()功能),但许多附加组件,例如 p5.clickable 不这样做。此外,p5.clickable 还遭受了依赖 p5.js 库函数 (rect/fill/stroke/etc.) 的不幸设计决策的影响。然而,通过一些适度的更改,我能够让它在 React 应用程序中工作:

p5.clickable.js 的修改版本


//Determines if the mouse was pressed on the previous frame
var cl_mouseWasPressed = false;
//Last hovered button
var cl_lastHovered = null;
//Last pressed button
var cl_lastClicked = null;
//All created buttons
var cl_clickables = [];

//This function is what makes the magic happen and should be ran after
//each draw cycle.

// EDIT: Anywhere p5 was referenced, now using global.p5
global.p5.prototype.runGUI = function () {
  for (let i = 0; i < cl_clickables.length; ++i) {
    if (cl_lastHovered !== cl_clickables[i])
      cl_clickables[i].onOutside();
  }
  if (cl_lastHovered != null) {
    if (cl_lastClicked !== cl_lastHovered) {
      cl_lastHovered.onHover();
    }
  }
  if (!cl_mouseWasPressed && cl_lastClicked !== null) {
    cl_lastClicked.onPress();
  }

  // EDIT: Use this.mouseIsPressed instead of the global mouseIsPressed.
  // hopefully this works but I haven't investigated the this binding when
  // runGUI is invoked by 'post'
  if (cl_mouseWasPressed && !this.mouseIsPressed && cl_lastClicked != null) {
    if (cl_lastClicked === cl_lastHovered) {
      cl_lastClicked.onRelease();
    }
    cl_lastClicked = null;
  }
  cl_lastHovered = null;
  cl_mouseWasPressed = this.mouseIsPressed;
}

global.p5.prototype.registerMethod('post', global.p5.prototype.runGUI);

//This function is used to get the bounding size of a
//string of text for use in the 'textScaled' property
function getTextBounds(m, font, size) {
  let txt = document.createElement("span");
  document.body.appendChild(txt);

  txt.style.font = font;
  txt.style.fontSize = size + "px";
  txt.style.height = 'auto';
  txt.style.width = 'auto';
  txt.style.position = 'absolute';
  txt.style.whiteSpace = 'no-wrap';
  txt.innerHTML = m;

  let width = Math.ceil(txt.clientWidth);
  let height = Math.ceil(txt.clientHeight);
  document.body.removeChild(txt);
  return [width, height];
}

//Button Class
// EDIT: Clickable now takes a p5 instance
function Clickable(p) {
  if (!p) {
    // EDIT: If a p5 instance is not passed to the Clickable constructor,
    // fallback on global mode
    p = global;
  }

  this.x = 0;      //X position of the clickable
  this.y = 0;      //Y position of the clickable
  this.width = 100;    //Width of the clickable
  this.height = 50;    //Height of the clickable
  this.color = "#FFFFFF";    //Background color of the clickable
  this.cornerRadius = 10;    //Corner radius of the clickable
  this.strokeWeight = 2;    //Stroke width of the clickable
  this.stroke = "#000000";  //Border color of the clickable
  this.text = "Press Me";    //Text of the clickable
  this.textColor = "#000000";  //Color for the text shown
  this.textSize = 12;    //Size for the text shown
  this.textFont = "sans-serif";  //Font for the text shown
  this.textScaled = false;     //Scale the text with the size of the clickable
  
  // image options
  this.image = null; // image object from p5loadimage()
  this.tint = null; // tint image using color
  this.noTint = true; // default to disable tinting
  this.filter = null; // filter effect

  this.updateTextSize = function () {
    if (this.textScaled) {
      for (let i = this.height; i > 0; i--) {
        if (getTextBounds(this.text, this.textFont, i)[0] <= this.width && getTextBounds(this.text, this.textFont, i)[1] <= this.height) {
          console.log("textbounds: " + getTextBounds(this.text, this.font, i));
          console.log("boxsize: " + this.width + ", " + this.height);
          this.textSize = i / 2;
          break;
        }
      }
    }
  }
  this.updateTextSize();

  this.onHover = function () {
    //This function is ran when the clickable is hovered but not
    //pressed.
  }

  this.onOutside = function () {
    //This function is ran when the clickable is NOT hovered.
  }

  this.onPress = function () {
    //This function  is ran when the clickable is pressed.
  }

  this.onRelease = function () {
    //This funcion is ran when the cursor was pressed and then
    //released inside the clickable. If it was pressed inside and
    //then released outside this won't work.
  }

  this.locate = function (x, y) {
    this.x = x;
    this.y = y;
  }

  this.resize = function (w, h) {
    this.width = w;
    this.height = h;
    this.updateTextSize();
  }

  this.drawImage = function(){
    // EDIT: All references to p5 library functions now use the instance p
    p.image(this.image, this.x, this.y, this.width, this.height);
    if(this.tint && !this.noTint){
      p.tint(this.tint)
    } else {
      p.noTint();
    }
    if(this.filter){
      p.filter(this.filter);
    }
  }

  this.draw = function () {
    p.push();
    p.fill(this.color);
    p.stroke(this.stroke);
    p.strokeWeight(this.strokeWeight);
    p.rect(this.x, this.y, this.width, this.height, this.cornerRadius);
    p.fill(this.textColor);
    p.noStroke();
    if(this.image){
      this.drawImage();
    }

    p.textAlign(p.CENTER, p.CENTER);
    p.textSize(this.textSize);
    p.textFont(this.textFont);
    p.text(this.text, this.x + this.width / 2, this.y + this.height / 2);
    if (p.mouseX >= this.x && p.mouseY >= this.y &&
      p.mouseX < this.x + this.width && p.mouseY < this.y + this.height) {
      cl_lastHovered = this;
      if (p.mouseIsPressed && !cl_mouseWasPressed) {
        cl_lastClicked = this;
      }
    }
    p.pop();
  }

  cl_clickables.push(this);
}

// Export Clickable globally.
// In this case it would also be trivial to change this into a legitimate
// module by simply exporting Clickable. But I wanted to demonstrate how
// this could be done with globals
global.Clickable = Clickable;

用法示例

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

import '../lib/p5.clickable-1.2.js';
const Clickable = global.Clickable;

let x = 50;
let y = 50;

let click1;

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

    // Example Clickable
    click1 = new Clickable(p);
    click1.locate(x, y);
    click1.onHover = function () {
      this.color = props.color;
      this.textColor = "#FFFFFF";
      this.text = "Yay!";
    };
    click1.onOutside = function() {
      this.color = "#FFFFFF";
      this.textColor= "#000000";
      this.text = "Press Me";
    };
  };

  const draw = (p) => {
    p.background(0);
    p.fill(props.color || 'blue');
    click1.locate(x % p.width, y);
    click1.draw();
    x++;
  };

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