CodingTrain / node-p5-test

6 stars 1 forks source link

Using p5.js in Node runtime #1

Open dipamsen opened 3 years ago

dipamsen commented 3 years ago

TL;DR: scroll down to the bottom for the templates


After studying the source code of p5.js, this is what I have gathered so far:

These variables are available in browser window and unavailable in node

p5 Instance Mode

For making p5 work in instance mode I created this template:

// Adapted from:
// ! Heart Curve
// ! Daniel Shiffman
// https://thecodingtrain.com/CodingChallenges/134-heart-curve.html
// https://youtu.be/oUBAi9xQ2X4
// https://editor.p5js.org/codingtrain/sketches/egvieHyt0

const w = 400,
  h = 400;
setupWindow(w, h);

const p5 = require("p5");

new p5((p) => {
  const heart = [];
  let a = 0;
  p.setup = () => {
    p.createCanvas(w, h);
    while (true) {
      const r = p.height / 40;
      const x = r * 16 * p.pow(p.sin(a), 3);
      const y = -r * (13 * p.cos(a) - 5 * p.cos(2 * a) - 2 * p.cos(3 * a) - p.cos(4 * a));
      heart.push(p.createVector(x, y));
      // So that it stops
      if (a > p.TWO_PI) {
        break;
      }

      a += 0.1;
    }
    p.background(0);
    p.translate(p.width / 2, p.height / 2);

    p.stroke(255);
    p.strokeWeight(2);
    p.fill(150, 0, 100);
    p.beginShape();
    for (let v of heart) {
      p.vertex(v.x, v.y);
    }
    p.endShape();
    saveAsPNG(p, "filename");
  };
});

async function saveAsPNG(p5Inst, filename = "sketch", exit = true) {
  // --- save as png function code ---
}

function setupWindow(w = 100, h = 100) {
  // --- setup window function code ---
}

In this way, p5.js can be used in Instance Mode to work with node-canvas

Click here to see the codes for saveAsPNG and setupWindow: **`saveAsPNG` - Saves the canvas Image to filname using fs** ```js async function saveAsPNG(p5Inst, filename = "sketch", exit = true) { const fs = require("fs"); const buffer = p5Inst._renderer.drawingContext.canvas.toBuffer() await fs.promises.writeFile(filename + ".png", buffer) if (exit) process.exit(0) } ``` **`setupWindow` - Add necessary variables to `global` for p5 to run** ```js function setupWindow(w = 100, h = 100) { // Import required stuff const { createCanvas } = require("canvas"); const { JSDOM } = require("jsdom"); const { performance } = require("perf_hooks"); // All the global properties will be available to p5 global.window = global; // Use JSDOM to simulate DOM methods const dom = new JSDOM(); global.document = dom.window.document; const nodeCanvas = createCanvas(w, h); // p5 will use the methods on nodeCanvas context (overriding JSDOM's getContext function) dom.window.HTMLCanvasElement.prototype.getContext = (type) => { return nodeCanvas.getContext(type); }; // p5 uses window.performance global.performance = performance; // window.screen is used to determine displayWidth and displayHeight // in this case it will be undefined global.screen = {}; // p5 uses events "load" and "error" // Using JSDOM equivalent global.addEventListener = dom.window.addEventListener.bind(dom.window); global.removeEventListener = dom.window.removeEventListener.bind(dom.window); // Navigator.userAgent is used by p5 to polyfill methods in older browsers (safari) global.navigator = { userAgent: "node" }; // when we declare a function in node, it doesn't pollute global scope // So implicitly write code so that p5 can access these functions on global/window. // UNCOMMENT NEXT TWO LINES WHEN USING GLOBAL MODE // global.setup = setup // global.draw = draw } ```

Going further - Global Mode!

This is the template for using p5.js in node with Global Mode:

// Adapted from:
// ! Heart Curve
// ! Daniel Shiffman
// https://thecodingtrain.com/CodingChallenges/134-heart-curve.html
// https://youtu.be/oUBAi9xQ2X4
// https://editor.p5js.org/codingtrain/sketches/egvieHyt0

// width and height to be passed both in createCanvas and in setupWindow
setupWindow(400, 400);
// Note that in global mode even though we don't use the p5 variable, it still needs to be imported.
const p5 = require("p5");

const heart = [];
let a = 0;

function setup() {
  createCanvas(400, 400);
}

function draw() {
  while (true) {
    const r = height / 40;
    const x = r * 16 * pow(sin(a), 3);
    const y = -r * (13 * cos(a) - 5 * cos(2 * a) - 2 * cos(3 * a) - cos(4 * a));
    heart.push(createVector(x, y));
    // So that it stops
    if (a > TWO_PI) {
      break;
    }

    a += 0.1;
  }
  background(0);
  translate(width / 2, height / 2);

  stroke(255);
  strokeWeight(2);
  fill(150, 0, 100);
  beginShape();
  for (let v of heart) {
    vertex(v.x, v.y);
  }
  endShape();
  // in global mode, p5 is attatched to the window (global in node)
  // the third arg stands for whether to exit the process
  // in this case, if false is passed, then this sketch will run forever, until manually cancelled (because this is the draw loop)
  saveAsPNG(global, "globalimg", true);
}

In this case as well, the functions saveAsPNG and setupWindow should be appended at the end of the file, with the last two lines of setupWindow uncommented.

This is the output of the global mode code globalimg

Use these Templates!

To Use these templates, first install these node-modules npm i p5 canvas jsdom

dipamsen commented 3 years ago

Example Discord Bot using p5.js: DiscordP5Canvas

P.S. There is an issue with using this method of p5, whenever any kind of error is thrown, (undefined variable/invalid syntax, etc) in the sketch, only this error will show in the console:

Error: Uncaught [TypeError: Cannot read property '_ownerDocument' of undefined]
    at reportException (jsdom\lib\jsdom\living\helpers\runtime-script-errors.js:62:24)
    at innerInvokeEventListeners (jsdom\lib\jsdom\living\events\EventTarget-impl.js:333:9)
    at invokeEventListeners (jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
    at DocumentImpl._dispatch (jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
    at fireAnEvent (jsdom\lib\jsdom\living\helpers\events.js:18:36)
    at dispatchEvent (jsdom\lib\jsdom\living\nodes\Document-impl.js:475:9)
    at jsdom\lib\jsdom\living\nodes\Document-impl.js:480:11
    at new Promise (<anonymous>)
    at onLoad (jsdom\lib\jsdom\living\nodes\Document-impl.js:478:14)
    at Object.check (jsdom\lib\jsdom\browser\resources\resource-queue.js:76:23) TypeError: Cannot read property '_ownerDocument' of undefined
    at innerInvokeEventListeners (jsdom\lib\jsdom\living\events\EventTarget-impl.js:327:26)
    at invokeEventListeners (jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
    at EventTargetImpl._dispatch (jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
    at fireAnEvent (jsdom\lib\jsdom\living\helpers\events.js:18:36)
    at Document.<anonymous> (jsdom\lib\jsdom\browser\Window.js:776:9)
    at Document.callTheUserObjectsOperation (jsdom\lib\jsdom\living\generated\EventListener.js:26:30)
    at innerInvokeEventListeners (jsdom\lib\jsdom\living\events\EventTarget-impl.js:318:25)
    at invokeEventListeners (jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
    at DocumentImpl._dispatch (jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
    at fireAnEvent (jsdom\lib\jsdom\living\helpers\events.js:18:36)

I don't have any idea on what is causing this error, as I have not checked the JSDOM source code out.

dipamsen commented 3 years ago

CodingTrain/Random-Whistle ported to node: here

jkenzer commented 3 years ago

@dipamsen Great job! I worked on this over the weekend and got pretty close but I don't think I would have gotten there. Thanks for sharing this. I hope to use this to output p5.js sketches to pixel arrays using a raspberry pi.