Search code examples
javascriptnode.jsp5.js

p5 breaks in node.js


I'm trying to make a node app that runs on the server. I created a motion detection system in p5 and its library vida.

I first tried global mode. Then I got errors like createCanvas is not defined. Now I tried instance mode, but I feel it has gotten even worse. I get the errors window is not defined in the p5.js script and require is not defined from the p5.dom.js script.

How should I correctly implement my p5 elements in the node app?

I read a.o. this stackoverflow post, but I don't know what it means to run p5 in the browser within a node server.

Here my server.js

const express = require('express');
const multer = require('multer');
const sketch = require('./public/app.js');

const PORT=3000;
const upload = multer({dest: __dirname + '/uploads/images'});
const app = express();

//things possible to be accessed by the client
app.use(express.static('./public'));
app.use(express.static('./public/scripts'));

console.log(sketch.setup, sketch.draw);

//test
app.get('/', function (req, res) {
    res.send('hello world');
  });

app.post('/upload', upload.single('photo'), (req, res) => {
    if(req.file) {
        res.json(req.file);
    }
    else throw 'error';
});

app.listen(PORT, () => {
    console.log('Listening at ' + PORT );
});

And here are the main parts of my app.js

'use strict';
/******** {SKETCH INSTANCE} *********/
var p5 = require('p5');
var myp5 = new p5(sketch);
var sketch = (s) => {
/**** { VIDA video } ****/
var myCapture;
var myVida;

function initCaptureDevice() {
  //code
};

/**** { P5 } ****/
  s.setup = function() {
    s.createCanvas(640, 480);
    initCaptureDevice();
    startup();

    myVida = new s.Vida(this); // create the object
    //vida code

    s.frameRate(10);
  };

  s.draw = function() {
    if(myCapture !== null && myCapture !== undefined) { // safety first
      s.background(0, 0, 255);

      myVida.update(myCapture);
      s.image(myVida.thresholdImage, 320, 240);

      // let's describe the displayed images
      
      /*DRAWING*/
      // define size of the drawing

      /*ACTIVE ZONES*/
      for(var i = 0; i < myVida.activeZones.length; i++) {
        //code

        /*DETECT MOTION*/
        if(myVida.activeZones[i].isChangedFlag) {
          if(myVida.activeZones[i].isMovementDetectedFlag) {
            //the state changed from noMovement to movement
            //take snapshot and upload it to a public folder
            snapshot();
          };
        };
      }
      s.pop(); // restore memorized drawing style and font
    }
    else {
      s.background(255, 0, 0);
    }
  }
}
exports.sketch = sketch;

Thank you for your time, Mustard Shaper


Solution

  • P5.js is not meant to be used outside of the browser. However, if you must, it can technically be made to work. I've posted an example below (which you can also view and run via my Repl.it project). But beware, there is no guarantee this will work for your use case. Just getting the canvas's image data to write to a file for this example was a humungous pain, so your mileage may vary.

    index.js

    const Window = require('window');
    const fs = require('fs');
    const { Blob } = require('./blob.js');
    
    // globals expected to exist by p5js (must come before the import)
    global.window = new Window();
    // Override JSDOM's horrible Blob implementations
    global.window.Blob = Blob;
    global.document = global.window.document;
    global.screen = global.window.screen;
    global.navigator = global.window.navigator;
    
    const p5 = require('p5');
    
    const inst = new p5(p => {
      p.setup = function() {
        console.log('rendering');
        let canvas = p.createCanvas(200, 200);
        p.background('gray');
        p.fill('crimson');
        p.circle(p.width / 2, p.height / 2, 100);
        canvas.elt.toBlob(
          data => {
            let writeStream = fs.createWriteStream('./out.png')
            writeStream.on('finish', () => {
              // All writes are now complete.
              done();
            });
            writeStream.on('error', (err) => {
              console.error(err);
            });
    
            console.log('file size: ' + data.size);
            writeStream.write(data.arrayBuffer());
            writeStream.end();
          },
          'image/png'
        );
      };
    });
    
    function done() {
      console.log('done!');
      inst.remove();
      process.exit(0);
    }
    

    blob.js:

    "use strict";
    
    const { Readable } = require('stream');
    
    exports.Blob = class Blob {
      constructor(parts, options) {
        this._buffer = Buffer.concat(parts);
    
        this.type = options.type;
      }
    
      get size() {
        return this._buffer.length;
      }
    
      arrayBuffer() {
        return this._buffer;
      }
    
      stream() {
        return Readable.from(this._buffer);
      }
    
      slice(start, end, contentType) {
        const { size } = this;
    
        let relativeStart, relativeEnd, relativeContentType;
    
        if (start === undefined) {
          relativeStart = 0;
        } else if (start < 0) {
          relativeStart = Math.max(size + start, 0);
        } else {
          relativeStart = Math.min(start, size);
        }
        if (end === undefined) {
          relativeEnd = size;
        } else if (end < 0) {
          relativeEnd = Math.max(size + end, 0);
        } else {
          relativeEnd = Math.min(end, size);
        }
    
        if (contentType === undefined) {
          relativeContentType = "";
        } else {
          // sanitization (lower case and invalid char check) is done in the
          // constructor
          relativeContentType = contentType;
        }
    
        const span = Math.max(relativeEnd - relativeStart, 0);
    
        const slicedBuffer = this._buffer.slice(
          relativeStart,
          relativeStart + span
        );
    
        return new Blob([slicedBuffer], { type: relativeContentType });
      }
    };
    

    package.json:

    {
      "name": "p5js-test",
      "version": "1.0.0",
      "description": "Test p5.js Node.js app.",
      "main": "./index.js",
      "bin": {
        "p5js-test": "./index.js"
      },
      "scripts": {
        "start": "node index.js"
      },
      "author": "Paul Wheeler",
      "license": "MIT",
      "dependencies": {
        "canvas": "^2.7.0",
        "p5": "^1.3.1",
        "window": "^4.2.7",
        "jsdom": "^16.5.3"
      },
      "devDependencies": {}
    }