Demo on how to display graphics (P5.js) in dashboard widget

Hi there !

Just sharing a small flow that allows to display P5.js graphics in a widget. The trick is to use iframe. Controls here are possible thru mqtt.

colors

boids

gol

Code based on the three.js demo found in https://flows.nodered.org/
P5.js sample code grabbed from https://p5js.org/examples

[{"id":"26b3c373.d3c1ac","type":"comment","z":"714d61bd.6125f","name":"P5.js example","info":"","x":130,"y":200,"wires":[]},{"id":"d1f80fe9.20ab5","type":"http in","z":"714d61bd.6125f","name":"","url":"/p5js","method":"get","upload":false,"swaggerDoc":"","x":120,"y":240,"wires":[["7e285f13.49a7d"]]},{"id":"7e285f13.49a7d","type":"template","z":"714d61bd.6125f","name":"Color","field":"payload.p5js","fieldType":"msg","format":"javascript","syntax":"plain","template":"// Write pure P5.js code here -----------------------------\n\nlet rSlider, gSlider, bSlider, mqttRadius;\n\nfunction setup() {\n  // create canvas\n  width = window.innerWidth;\n  height = window.innerHeight;\n  createCanvas(width, height); \n  textSize(15);\n  noStroke();\n\n  // create sliders\n  rSlider = createSlider(0, 255, 100);\n  rSlider.position(20, 20);\n  gSlider = createSlider(0, 255, 0);\n  gSlider.position(20, 50);\n  bSlider = createSlider(0, 255, 255);\n  bSlider.position(20, 80);\n}\n\nfunction draw() {\n  const r = rSlider.value();\n  const g = gSlider.value();\n  const b = bSlider.value();\n  background(r, g, b);\n  text('red', rSlider.x * 2 + rSlider.width, 35);\n  text('green', gSlider.x * 2 + gSlider.width, 65);\n  text('blue', bSlider.x * 2 + bSlider.width, 95);\n  \n  ellipse(width/2,height/2,mqttRadius,mqttRadius);\n}\n\n\n//----------------------------------------\n","output":"str","x":290,"y":240,"wires":[["b817762f.d6cac8"]]},{"id":"b817762f.d6cac8","type":"template","z":"714d61bd.6125f","name":"MQTT","field":"payload.mqtt","fieldType":"msg","format":"javascript","syntax":"plain","template":"// Write MQTT code here ----------------------------------------\n\nvar client = mqtt.connect('mqtt://192.168.1.68:8080');\n\nclient.on('connect', function () {\n    client.subscribe('p5js/#', function (err) {\n      if (!err) {\n        //client.publish('p5js', 'P5.js has subscribed to p5js/# topic')\n      }\n    })\n})\n\nclient.on('message', function (topic, payload) {\n    // payload is Buffer\n    console.log(\"MQTT:[\"+topic.toString()+\"/\"+payload.toString()+\"]\")\n        \n    myText = payload.toString();\n    mqttRadius = parseInt(myText)\n        \n    //redraw();\n})\n","output":"str","x":450,"y":240,"wires":[["a2223ec8.98c74"]]},{"id":"a2223ec8.98c74","type":"template","z":"714d61bd.6125f","name":"HTML","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n<head>\n<meta charset=\"utf-8\">\n\n<script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/p5@1.0.0/lib/p5.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/mqtt@3.0.0/dist/mqtt.min.js\"></script>\n    \n\n\n<script>{{{payload.p5js}}}</script>\n<script>{{{payload.mqtt}}}</script>\n\n\n</head>\n</html>\n\n","output":"str","x":610,"y":240,"wires":[["5cc4b28e.8729fc"]]},{"id":"5cc4b28e.8729fc","type":"change","z":"714d61bd.6125f","name":"Set Headers","rules":[{"t":"set","p":"headers","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"headers.content-type","pt":"msg","to":"text/html","tot":"str"},{"t":"set","p":"headers.Access-Control-Allow-Origin","pt":"msg","to":"*","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":240,"wires":[["ba902e89.332b2"]]},{"id":"ba902e89.332b2","type":"http response","z":"714d61bd.6125f","name":"","statusCode":"","headers":{},"x":970,"y":240,"wires":[]},{"id":"3a7288f3.9f5fc8","type":"ui_template","z":"714d61bd.6125f","group":"52c0f781.971f08","name":"P5.js display in iFrame","order":0,"width":"6","height":"6","format":"\n<iframe width=\"100%\" height=\"100%\" frameborder=\"0\" scrolling=\"no\"  src=\"http://192.168.1.68:1880/p5js\"></iframe>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":160,"y":280,"wires":[[]]},{"id":"af1079b6.031d48","type":"ui_slider","z":"714d61bd.6125f","name":"","label":"Radius","tooltip":"","group":"52c0f781.971f08","order":1,"width":0,"height":0,"passthru":true,"outs":"end","topic":"p5js","min":"10","max":"100","step":1,"x":120,"y":340,"wires":[["f202ee4f.41ad"]]},{"id":"f202ee4f.41ad","type":"mqtt out","z":"714d61bd.6125f","name":"","topic":"","qos":"0","retain":"false","broker":"2bfc0288.2ed06e","x":290,"y":340,"wires":[]},{"id":"7987b649.b09648","type":"mqtt in","z":"714d61bd.6125f","name":"","topic":"p5js/#","qos":"2","datatype":"auto","broker":"2bfc0288.2ed06e","x":290,"y":400,"wires":[["4aea3d45.ed3aa4"]]},{"id":"4aea3d45.ed3aa4","type":"debug","z":"714d61bd.6125f","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":470,"y":400,"wires":[]},{"id":"186ae793.1f6ec8","type":"template","z":"714d61bd.6125f","name":"Game of life","field":"payload.p5js","fieldType":"msg","format":"javascript","syntax":"plain","template":"\n// Write P5.js code here -----------------------------\n\nlet w;\nlet columns;\nlet rows;\nlet board;\nlet next;\n\n    \n    \nfunction setup() {\n  width = window.innerWidth;\n  height = window.innerHeight;\n\n  createCanvas(width, height); \n  w = 10;\n  // Calculate columns and rows\n  columns = floor(width / w);\n  rows = floor(height / w);\n  // Wacky way to make a 2D array is JS\n  board = new Array(columns);\n  for (let i = 0; i < columns; i++) {\n    board[i] = new Array(rows);\n  }\n  // Going to use multiple 2D arrays and swap them\n  next = new Array(columns);\n  for (i = 0; i < columns; i++) {\n    next[i] = new Array(rows);\n  }\n  init();\n}\n\nfunction draw() {\n  background(255);\n  generate();\n  for ( let i = 0; i < columns;i++) {\n    for ( let j = 0; j < rows;j++) {\n      if ((board[i][j] == 1)) fill(0);\n      else fill(255);\n      stroke(0);\n      rect(i * w, j * w, w-1, w-1);\n    }\n  }\n\n}\n\n// reset board when mouse is pressed\nfunction mousePressed() {\n  init();\n}\n\n// Fill board randomly\nfunction init() {\n  for (let i = 0; i < columns; i++) {\n    for (let j = 0; j < rows; j++) {\n      // Lining the edges with 0s\n      if (i == 0 || j == 0 || i == columns-1 || j == rows-1) board[i][j] = 0;\n      // Filling the rest randomly\n      else board[i][j] = floor(random(2));\n      next[i][j] = 0;\n    }\n  }\n}\n\n// The process of creating the new generation\nfunction generate() {\n\n  // Loop through every spot in our 2D array and check spots neighbors\n  for (let x = 1; x < columns - 1; x++) {\n    for (let y = 1; y < rows - 1; y++) {\n      // Add up all the states in a 3x3 surrounding grid\n      let neighbors = 0;\n      for (let i = -1; i <= 1; i++) {\n        for (let j = -1; j <= 1; j++) {\n          neighbors += board[x+i][y+j];\n        }\n      }\n\n      // A little trick to subtract the current cell's state since\n      // we added it in the above loop\n      neighbors -= board[x][y];\n      // Rules of Life\n      if      ((board[x][y] == 1) && (neighbors <  2)) next[x][y] = 0;           // Loneliness\n      else if ((board[x][y] == 1) && (neighbors >  3)) next[x][y] = 0;           // Overpopulation\n      else if ((board[x][y] == 0) && (neighbors == 3)) next[x][y] = 1;           // Reproduction\n      else                                             next[x][y] = board[x][y]; // Stasis\n    }\n  }\n\n  // Swap!\n  let temp = board;\n  board = next;\n  next = temp;\n}\n","output":"str","x":310,"y":40,"wires":[["b817762f.d6cac8"]]},{"id":"34e6c43e.58ab9c","type":"template","z":"714d61bd.6125f","name":"Flock","field":"payload.p5js","fieldType":"msg","format":"javascript","syntax":"plain","template":"// Write P5.js code here -----------------------------\n\nlet flock;\n\nfunction setup() {\n  createCanvas(300, 300);\n  createP(\"Drag the mouse to generate new boids.\");\n\n  flock = new Flock();\n  // Add an initial set of boids into the system\n  for (let i = 0; i < 100; i++) {\n    let b = new Boid(width / 2,height / 2);\n    flock.addBoid(b);\n  }\n}\n\nfunction draw() {\n  background(51);\n  flock.run();\n}\n\n// Add a new boid into the System\nfunction mouseDragged() {\n  flock.addBoid(new Boid(mouseX, mouseY));\n}\n\n// The Nature of Code\n// Daniel Shiffman\n// http://natureofcode.com\n\n// Flock object\n// Does very little, simply manages the array of all the boids\n\nfunction Flock() {\n  // An array for all the boids\n  this.boids = []; // Initialize the array\n}\n\nFlock.prototype.run = function() {\n  for (let i = 0; i < this.boids.length; i++) {\n    this.boids[i].run(this.boids);  // Passing the entire list of boids to each boid individually\n  }\n}\n\nFlock.prototype.addBoid = function(b) {\n  this.boids.push(b);\n}\n\n// The Nature of Code\n// Daniel Shiffman\n// http://natureofcode.com\n\n// Boid class\n// Methods for Separation, Cohesion, Alignment added\n\nfunction Boid(x, y) {\n  this.acceleration = createVector(0, 0);\n  this.velocity = createVector(random(-1, 1), random(-1, 1));\n  this.position = createVector(x, y);\n  this.r = 3.0;\n  this.maxspeed = 3;    // Maximum speed\n  this.maxforce = 0.05; // Maximum steering force\n}\n\nBoid.prototype.run = function(boids) {\n  this.flock(boids);\n  this.update();\n  this.borders();\n  this.render();\n}\n\nBoid.prototype.applyForce = function(force) {\n  // We could add mass here if we want A = F / M\n  this.acceleration.add(force);\n}\n\n// We accumulate a new acceleration each time based on three rules\nBoid.prototype.flock = function(boids) {\n  let sep = this.separate(boids);   // Separation\n  let ali = this.align(boids);      // Alignment\n  let coh = this.cohesion(boids);   // Cohesion\n  // Arbitrarily weight these forces\n  sep.mult(1.5);\n  ali.mult(1.0);\n  coh.mult(1.0);\n  // Add the force vectors to acceleration\n  this.applyForce(sep);\n  this.applyForce(ali);\n  this.applyForce(coh);\n}\n\n// Method to update location\nBoid.prototype.update = function() {\n  // Update velocity\n  this.velocity.add(this.acceleration);\n  // Limit speed\n  this.velocity.limit(this.maxspeed);\n  this.position.add(this.velocity);\n  // Reset accelertion to 0 each cycle\n  this.acceleration.mult(0);\n}\n\n// A method that calculates and applies a steering force towards a target\n// STEER = DESIRED MINUS VELOCITY\nBoid.prototype.seek = function(target) {\n  let desired = p5.Vector.sub(target,this.position);  // A vector pointing from the location to the target\n  // Normalize desired and scale to maximum speed\n  desired.normalize();\n  desired.mult(this.maxspeed);\n  // Steering = Desired minus Velocity\n  let steer = p5.Vector.sub(desired,this.velocity);\n  steer.limit(this.maxforce);  // Limit to maximum steering force\n  return steer;\n}\n\nBoid.prototype.render = function() {\n  // Draw a triangle rotated in the direction of velocity\n  let theta = this.velocity.heading() + radians(90);\n  fill(127);\n  stroke(200);\n  push();\n  translate(this.position.x, this.position.y);\n  rotate(theta);\n  beginShape();\n  vertex(0, -this.r * 2);\n  vertex(-this.r, this.r * 2);\n  vertex(this.r, this.r * 2);\n  endShape(CLOSE);\n  pop();\n}\n\n// Wraparound\nBoid.prototype.borders = function() {\n  if (this.position.x < -this.r)  this.position.x = width + this.r;\n  if (this.position.y < -this.r)  this.position.y = height + this.r;\n  if (this.position.x > width + this.r) this.position.x = -this.r;\n  if (this.position.y > height + this.r) this.position.y = -this.r;\n}\n\n// Separation\n// Method checks for nearby boids and steers away\nBoid.prototype.separate = function(boids) {\n  let desiredseparation = 25.0;\n  let steer = createVector(0, 0);\n  let count = 0;\n  // For every boid in the system, check if it's too close\n  for (let i = 0; i < boids.length; i++) {\n    let d = p5.Vector.dist(this.position,boids[i].position);\n    // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)\n    if ((d > 0) && (d < desiredseparation)) {\n      // Calculate vector pointing away from neighbor\n      let diff = p5.Vector.sub(this.position, boids[i].position);\n      diff.normalize();\n      diff.div(d);        // Weight by distance\n      steer.add(diff);\n      count++;            // Keep track of how many\n    }\n  }\n  // Average -- divide by how many\n  if (count > 0) {\n    steer.div(count);\n  }\n\n  // As long as the vector is greater than 0\n  if (steer.mag() > 0) {\n    // Implement Reynolds: Steering = Desired - Velocity\n    steer.normalize();\n    steer.mult(this.maxspeed);\n    steer.sub(this.velocity);\n    steer.limit(this.maxforce);\n  }\n  return steer;\n}\n\n// Alignment\n// For every nearby boid in the system, calculate the average velocity\nBoid.prototype.align = function(boids) {\n  let neighbordist = 50;\n  let sum = createVector(0,0);\n  let count = 0;\n  for (let i = 0; i < boids.length; i++) {\n    let d = p5.Vector.dist(this.position,boids[i].position);\n    if ((d > 0) && (d < neighbordist)) {\n      sum.add(boids[i].velocity);\n      count++;\n    }\n  }\n  if (count > 0) {\n    sum.div(count);\n    sum.normalize();\n    sum.mult(this.maxspeed);\n    let steer = p5.Vector.sub(sum, this.velocity);\n    steer.limit(this.maxforce);\n    return steer;\n  } else {\n    return createVector(0, 0);\n  }\n}\n\n// Cohesion\n// For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location\nBoid.prototype.cohesion = function(boids) {\n  let neighbordist = 50;\n  let sum = createVector(0, 0);   // Start with empty vector to accumulate all locations\n  let count = 0;\n  for (let i = 0; i < boids.length; i++) {\n    let d = p5.Vector.dist(this.position,boids[i].position);\n    if ((d > 0) && (d < neighbordist)) {\n      sum.add(boids[i].position); // Add location\n      count++;\n    }\n  }\n  if (count > 0) {\n    sum.div(count);\n    return this.seek(sum);  // Steer towards the location\n  } else {\n    return createVector(0, 0);\n  }\n}\n","output":"str","x":290,"y":80,"wires":[["b817762f.d6cac8"]]},{"id":"faea71ac.d19e2","type":"template","z":"714d61bd.6125f","name":"Clock","field":"payload.p5js","fieldType":"msg","format":"javascript","syntax":"plain","template":"// Write P5.js code here -----------------------------\n\nlet cx, cy;\nlet secondsRadius;\nlet minutesRadius;\nlet hoursRadius;\nlet clockDiameter;\n\nfunction setup() {\n  width = window.innerWidth;\n  height = window.innerHeight;\n  \n  width = width-((width*10)/100);\n  height = height - ((height*10)/100);\n    \n  createCanvas(width, height);   stroke(255);\n\n  let radius = min(width, height) / 2;\n  secondsRadius = radius * 0.71;\n  minutesRadius = radius * 0.6;\n  hoursRadius = radius * 0.5;\n  clockDiameter = radius * 1.7;\n\n  cx = width / 2;\n  cy = height / 2;\n}\n\nfunction draw() {\n  background(230);\n\n  // Draw the clock background\n  noStroke();\n  fill(244, 122, 158);\n  ellipse(cx, cy, clockDiameter + 25, clockDiameter + 25);\n  fill(237, 34, 93);\n  ellipse(cx, cy, clockDiameter, clockDiameter);\n\n  // Angles for sin() and cos() start at 3 o'clock;\n  // subtract HALF_PI to make them start at the top\n  let s = map(second(), 0, 60, 0, TWO_PI) - HALF_PI;\n  let m = map(minute() + norm(second(), 0, 60), 0, 60, 0, TWO_PI) - HALF_PI;\n  let h = map(hour() + norm(minute(), 0, 60), 0, 24, 0, TWO_PI * 2) - HALF_PI;\n\n  // Draw the hands of the clock\n  stroke(255);\n  strokeWeight(1);\n  line(cx, cy, cx + cos(s) * secondsRadius, cy + sin(s) * secondsRadius);\n  strokeWeight(2);\n  line(cx, cy, cx + cos(m) * minutesRadius, cy + sin(m) * minutesRadius);\n  strokeWeight(4);\n  line(cx, cy, cx + cos(h) * hoursRadius, cy + sin(h) * hoursRadius);\n\n  // Draw the minute ticks\n  strokeWeight(2);\n  beginShape(POINTS);\n  for (let a = 0; a < 360; a += 6) {\n    let angle = radians(a);\n    let x = cx + cos(angle) * secondsRadius;\n    let y = cy + sin(angle) * secondsRadius;\n    vertex(x, y);\n  }\n  endShape();\n}\n\n\n","output":"str","x":290,"y":120,"wires":[["b817762f.d6cac8"]]},{"id":"d3c3b87.5496148","type":"template","z":"714d61bd.6125f","name":"Mobile cube","field":"payload.p5js","fieldType":"msg","format":"javascript","syntax":"plain","template":"// Write P5.js code here -----------------------------\n\nx = 0;\n\nfunction setup() {\n width = window.innerWidth;\n  height = window.innerHeight;\n  \n  // Add some borders\n  width = width-((width*10)/100);\n  height = height - ((height*10)/100);\n    \n  createCanvas(width, height, WEBGL);   \n  stroke(255);\n}\n\nfunction draw() {\n    x = x + 0.5;\n  background(250);\n  normalMaterial();\n  rotateX(accelerationX+x * 0.01);\n  rotateY(accelerationY * 0.01);\n  box(100, 100, 100);\n}\n\n//----------------------------------------\n\n\n","output":"str","x":310,"y":160,"wires":[["b817762f.d6cac8"]]},{"id":"52c0f781.971f08","type":"ui_group","z":"","name":"iFrame","tab":"200f07c6.f6ba98","order":1,"disp":true,"width":"6","collapse":true},{"id":"2bfc0288.2ed06e","type":"mqtt-broker","z":"","name":"","broker":"192.168.1.68","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"200f07c6.f6ba98","type":"ui_tab","z":"","name":"P5js","icon":"dashboard","disabled":false,"hidden":false}]

Be sure to modify the mqtt server ip address in the code according to your config

Please comment and help to enhance this piece of code. Once done, I expect to publish this on https://flows.nodered.org/

All the best !

3 Likes

Hi , Helpful demo! worked on my raspi and I was going to try some sound input. However, in the HTML even when I just include it stops any of the nodes ( Clock etc) not to load. any Idea what may be the reason?Thx

p5JS NoderedFlowWithSoundinHtml.txt (15.9 KB)

Hey Paul,
Glad to see that this piece of code makes you curious...

In order for the flow to work, please update the ip address according to your environnment in the ui-template node...

This should work...

Le jeu. 9 avr. 2020 Ă  18:39, Paul O'Brien via Node-RED Forum nodered@discoursemail.com a Ă©crit :

Hi jibeer,

The flows works for me - I had already changed the template to my ip addresses. I wanted to add in the 5js sound library to HTML and try some FFT but adding

into the html template causes the flow not work. I don;t know enough about this to know why including this script library would cause even the other flows, which were working, to not load. Seems to hang at the at this p5 sound library.

Any ideas, I could try.

Thanks Paul

If you have any questions regarding George Brown College’s commitment to comply with Canada’s Anti-Spam Legislation (CASL), please view our Anti-Spam Commitment at http://www.georgebrown.ca/casl

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.