Can users ADD functions to p5.js code through interaction with the sketch?

Hello,

I would like to write code that does the following: User clicks and drags on the canvas, and when they release a square has been drawn. The code, now has a rect() function, but no rect() function existed before the sketch was run. Is this possible using p5.js?

The only way that I can think of to do this with my current knoweldge is if you have a rect function already in your code and then you poplulate the parameters with data based on the clicks and release. However, what if you want the user to be able to do more than draw just one rectangle. What if you want them to be able to draw 30 of them?

So essentially I’m wondering if it’s possible for a user to be able to add functions to the code through their interaction with the canvas, such that there’s no rect(a, b, c, d); when they begin the sketch, but after interacting, they’ve managed to ADD, say, 4 rect(); functions and their corresponding parameters.

Thanks for any insights you can share,
Tori

1 Like

One possible approach would go something like this:

  1. Create a rectangle class with a constructor which uses x, y, w, h parameters and possibly color.
  2. Create a rectangle array to keep track of all the rectangles.
  3. Draw each rectangle using mouse x,y coordinates and events such as mouse pressed, dragged, and released.
  4. Write and save the array to a file if you want to reopen and edit your previous creation.
2 Likes

Ok, thanks Svan. So there’s no way for a user to add functions directly to your code by the sounds of it. Is that correct?

I’m not sure I understand what you want to do. Perhaps give an example of what you have in mind. You only need one rectangle function (which is the constructor in the Rect class) and then the user can use it to create as many subsequent rectangles as desired.

The following demo shows how to use a Rect class to create multiple rectangles. If you want to move the rectangles around then you would need to add an array to keep track of rectangle locations/size and mouse functions for selection/repositioning.

let r;
let button;

function setup() {
  createCanvas(600, 600);  
  button = createButton('Add Rect');
  button.position(0, 0);
  button.mousePressed(addRect);
  background(209);
}

function draw() {
}

function addRect() {
  r = new Rect(random(400),random(400),random(200),random(200), color(random(255),random(255),random(255)));
  r.display();
}

class Rect {
  constructor(x,y,w,h,bkgrnd) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.bkgrnd = bkgrnd;
  }
  
  display() {
    fill(this.bkgrnd);
    rect(this.x, this.y, this.w, this.h);
  }
}

2 Likes

Your question is almost on a Metalevel, since it’s about how we can write a program.

  • To change a program that is already running is not possible (to my knowledge) since it’s already loaded from the website into the RAM of the computer. So even when you could add code to the code / text on your website, you would need to restart the altered program. Or the program would need to restart itself. Maybe the gurus can do this.

  • As has been described, you can of course let the user draw rectangles and store them like in a drawing program. Here the code is not changed though.

  • Additionally you can of course generate code with the program (generate an entire Sketch with the new rectangles for example) and store it separately (into the console). So next time you have to run the new generated code (instead of the old code).

  • Finally you can write a language where the user types code, saves it runs it within your code. Like your own programming language. I did this once in processing (not p5.js).

Regards,

Chrisir

1 Like

Thank you both for your time and answers on this. You each provided something that was quite helpful.

Ultimately, what I was wanting to be able to do was to write a program that allowed me to draw something that generated the written functions for those shapes, so that I could copy the written functions and use them in a different sketch. (I was using a rectangle as an example here, but I would like to be able to draw complex shapes.)

So Chrisir, you answered this for me: it’s possible, but possibly too difficult a challenge for me at this time.

And svan, thanks for providing that example code. I used it, and tweaked it to create the sort of sketch that I was envisioning (minus the fact that it doesn’t write out the function for the rectangle that I just drew). Again, doing this with a rectangle is one thing, but doing it with a shape that requires multiple functions (like beginShape(), etc.) is a whole other challenge.

Here’s the code that I made:

let r;
let xPos;
let yPos;
let xWidth;
let yWidth;

function setup() {
  createCanvas(600, 600);
  background(209);
}

function draw() {
}

function addRect() {
  r = new Rect(
    xPos,
    yPos,
    xWidth,
    yWidth,
    color(random(255), random(255), random(255))
  );
  r.display();
}

class Rect {
  constructor(x, y, w, h, bkgrnd) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.bkgrnd = bkgrnd;
  }

  display() {
    fill(this.bkgrnd);
    rect(this.x, this.y, this.w, this.h);
  }
}

function mousePressed() {
  xPos = mouseX;
  yPos = mouseY;
}

function mouseReleased() {
  xWidth = mouseX - xPos;
  yWidth = mouseY - yPos;
  addRect();
}
2 Likes

I had time to work on this.

Here is a new version oriented on my bullet point #3.

  • First, internally it fills an array with rect data.
  • So we can start draw() with background() command and draw the entire array on screen every time.
  • Second, while you draw, the new changing rect is displayed (and you can draw over the old rects, but they re-appear when you make it smaller. This is the advantage of having the list in combination with background() command).
  • 3rd, delete entire list with c (clear)
  • 4th you can export your rects as Code with e
  • The code is: make a rect object and append it to the array. Instead we could also print:
    fill(color(.....)); rect(......, ....., ....., ....); for each rect but since we have the array, I thought we can use it.
  • Export is to the console and you can copy paste the console into the setup() of your Sketch.
  • This requires that you have the array and the class in your Sketch
  • Of course you could print the entire code for a new Sketch in the console like console.log("let r, r2;"); console.log("let xPos;"); ..... .... console.log("function setup() {"); etc., but I just print the rect data of the entire array.

I hope this helps.

Regards,

Chrisir


Full Sketch

let r, r2;
let xPos;
let yPos;

let xWidth;
let yWidth;

// this is useful to draw the rect in progress
let mouseDown = false;

// list of all rects
let myArray = [];

// ----------------------------------------------

function setup() {
  createCanvas(600, 600);
  background(209);
} // setup

function draw() {
  background(209); // delete screen

  // display from rect
  for (let i = 0; i < myArray.length; i++) {
    myArray[i].display();
  }

  // this is useful to draw the rect in progress
  if (mouseDown) {
    r2 = new Rect(xPos, yPos, mouseX - xPos, mouseY - yPos, color(213));
    r2.display();
  }
}

// ----------------------------------------------
// Inputs

function mousePressed() {
  xPos = mouseX;
  yPos = mouseY;
  mouseDown = true;
}

function mouseReleased() {
  xWidth = mouseX - xPos;
  yWidth = mouseY - yPos;
  mouseDown = false;
  addRect();
}

function keyPressed() {
  if (key == "e") {
    // Export code to console
    console.log("// Code");
    for (let i = 0; i < myArray.length; i++) {
      console.log("r2 = new Rect(" + myArray[i].myToString() + " );");
      console.log("append(myArray, r2);");
    }
  } else if (key == "c") {
    // DELETE list of all rects (clear)
    myArray = [];
  } //else if
} //func

// ----------------------------------------------------------
// Tools

function addRect() {
  r = new Rect(
    xPos,
    yPos,
    xWidth,
    yWidth,
    color(random(255), random(255), random(255))
  );
  // r.display();
  append(myArray, r);
}

//===============================================================

class Rect {
  constructor(x, y, w, h, bkgrnd) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.bkgrnd = color(bkgrnd);
  }

  display() {
    fill(this.bkgrnd);
    rect(this.x, this.y, this.w, this.h);
  }

  myToString() {
    // Export data for console export.
    // Exporting the color seems to work only this way.
    let s1 =
      this.x +
      ", " +
      this.y +
      ", " +
      this.w +
      ", " +
      this.h +
      ", color(" +
      red(this.bkgrnd) +
      ", " +
      green(this.bkgrnd) +
      ", " +
      blue(this.bkgrnd) +
      ")";
    return s1;
  }
} //class
//

1 Like

new version with

  • ESCAPE to stop current rect (while you draw the rect)
  • Backspace to delete last rect - this can be used multiple times of course

let r, r2;
let xPos;
let yPos;

let xWidth;
let yWidth;

// this is useful to draw the rect in progress
let mouseDown = false;

// list of all rects
let myArray = [];

// ----------------------------------------------

function setup() {
  createCanvas(600, 600);
  background(209);
} // setup

function draw() {
  background(209); // delete screen

  // display from rect
  for (let i = 0; i < myArray.length; i++) {
    myArray[i].display();
  }

  // this is useful to draw the rect in progress
  if (mouseDown) {
    r2 = new Rect(xPos, yPos, mouseX - xPos, mouseY - yPos, color(213, 66));
    r2.display();
  }
}

// ----------------------------------------------
// Inputs

function mousePressed() {
  xPos = mouseX;
  yPos = mouseY;
  mouseDown = true;
}

function mouseReleased() {
  xWidth = mouseX - xPos;
  yWidth = mouseY - yPos;
  if (!mouseDown) return;
  addRect();
  mouseDown = false;
}

function keyPressed() {
  if (key == "e") {
    // Export code to console
    console.log("// Code");
    for (let i = 0; i < myArray.length; i++) {
      console.log("r2 = new Rect(" + myArray[i].myToString() + " );");
      console.log("append(myArray, r2);");
    }
  } else if (key == "c") {
    // DELETE list of all rects (clear)
    myArray = [];
  } else if (keyCode == ESCAPE) {
    mouseDown = false;
  } else if (keyCode == BACKSPACE) {
    myArray = shorten(myArray);
  } //else if
} //func

// ----------------------------------------------------------
// Tools

function addRect() {
  r = new Rect(
    xPos,
    yPos,
    xWidth,
    yWidth,
    color(random(255), random(255), random(255))
  );
  // r.display();
  append(myArray, r);
}

//===============================================================

class Rect {
  constructor(x, y, w, h, bkgrnd) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.bkgrnd = color(bkgrnd);
  }

  display() {
    fill(this.bkgrnd);
    rect(this.x, this.y, this.w, this.h);
  }

  myToString() {
    // Export data for console export.
    // Exporting the color seems to work only this way.
    let s1 =
      this.x +
      ", " +
      this.y +
      ", " +
      this.w +
      ", " +
      this.h +
      ", color(" +
      red(this.bkgrnd) +
      ", " +
      green(this.bkgrnd) +
      ", " +
      blue(this.bkgrnd) +
      ")";
    return s1;
  }
} //class
//
1 Like

When you get tired of playing with the Rect class, the following link goes to source code for a MobiusRing class: https://gist.github.com/JoshuaSullivan/2b35e43466be11088c0e44f242cff5e2

1 Like

This may be what you want or at least an inspiration!

http://yining1023.github.io/p5PlayGround/

2 Likes

Absolutely. Here, no Code is generated from drawing though…

(Live-Coding)

This way also works:

function setup() {
  r = int(random(256))
  g = int(random(256))
  b = int(random(256))
    
  c = color(r, g, b)
    
  console.log(r, g, b)  
  console.log(c.toString() ) // This string can be used! :)
}

You could also use the r, g and b and concatenate into a desired string.

References:
https://p5js.org/reference/#/p5.Color
https://p5js.org/reference/#/p5.Color/toString

:)

Of course you can export this way.

But you cannot use the result in the target code, because of the rgba bit

That’s what I meant

But I didn’t looked into it more deeply

This is a working example (modified shared code in previous topic) using the default ‘rgba’ formatting for toString() in the target code:

myToString() {
    // Export data for console export.
    // Exporting the color seems to work only this way.
    let s1 =
      this.x +
      ", " +
      this.y +
      ", " +
      this.w +
      ", " +
      this.h +
      ", color(" +
        
//      red(this.bkgrnd) +
//      ", " +
//      green(this.bkgrnd) +
//      ", " +
//      blue(this.bkgrnd) +
        
      "\'" +  
      this.bkgrnd.toString() +          // default 'rgba' formatting
      // this.bkgrnd.toString('rgb') +  // There are many other formatting options
      "\'" +  
        ")"
    
    return s1;
  }
} //class
//

There are other formatting options for toString() in the references:

https://p5js.org/reference/#/p5.Color/toString < Lots of formatting options
https://p5js.org/reference/#/p5/color < Examples of color formats

:)

Does this work when you copy the console content back into the code (into setup()) and run it?

AFAIK there is no way to modify the source code of a running Javascript application. Others have put forward suggestions but it boils down to

  1. having a function or object to draw a particular shape
  2. collecting user supplied data related to the shape geometry
  3. pass the information collected in (2) to the function in (1) via its parameters.

There maybe a library that enables modification of running JS app but you will have to search for it.

In Java there are libraries that allow the creation, compilation and execution of Java classes at runtime. I used one called ASM in my library Jasmine but I found it hard to use even though it was only at the most basic level…

2 Likes

I think the “generate code and log it” approach is a good idea.
While you can modify an HTML entity to add a js function when “onload()” or “onclick()” are triggered, it is dangerous.

If you don’t want to stay on rects and want other shapes, you could create an array that logs shape type, number of parameters, and the parameters themselves. I made something similar in the nineties to create a vector-based illustration program. But I was using QBASIC, so objects were not an option.

The following source code demonstrates how to save rectangle array data to the user’s system (Downloads folder on a Mac) and subsequently retrieve the data to re-populate the sketch window rectangles. To use as intended follow these steps:

  1. Run the code and create as many rectangles as desired by hitting ‘Add Rect’ button for each.
  2. Hit ‘SaveArray’ button to write rectangle array data to a file titled “rect.txt” in the Downloads folder.
  3. Stop the code to close the window.
  4. Run the code a second time to display an empty window.
  5. Hit the ‘Choose File’ button and select the appropriate rect.txt file to repopulate the window. Be aware that a new rect.txt file is saved with sequential numbering for each run (i.e., rect(x).txt); the original file is not over-written. Pay attention to the name of the file when it is saved and open the same file in order to see your last array.

I would like to have saved the file to a sketch folder inside the p5.js web editor, but apparently this is not possible. The source code has not been tested extensively so there could be bugs, but should work as intended as long as the above steps are followed.

How it works:

  1. A rectangle array holds the data for each added rectangle according to a Rect class: x,y location coordinates, w,h size values, plus the r,g,b values for the fill color (7 values/rectangle).
  2. createWriter() is then used to write these comma separated values to a file using a separate line for each rectangle (each line is automatically line feed terminated, i.e., followed by ‘\n’)
  3. To retrieve the data createFileInput() locates the file with a dialog screen and opens it. The rectangle array is reconstructed by two split() calls. The first split() breaks the file at the line feeds to produce an element for each rectangle. The second split() breaks each rectangle element at its commas to reproduce the rectangle class values which are then used to re-construct and display the rectangles.
let r = [];
let rOut = [];
let saveBtn;
let addRectBtn;
let counter;
let input;
let rectArray = [];

function handleFile(file) {
  print(file);
  if (file.type === "text") {
    rectArray = split(file.data, "\n");
    print(rectArray.length); // one more than no. rects (counts last line feed)
    counter = rect.length - 1;
    for (let i = 0; i < rectArray.length - 1; i++) {
      rOut = split(rectArray[i], ",");
      print(rOut);
      r[i] = new Rect(
        rOut[0],
        rOut[1],
        rOut[2],
        rOut[3],
        color(rOut[4], rOut[5], rOut[6])
      );
      r[i].display();
    }
  }
}

function setup() {
  createCanvas(600, 600);
  addRectBtn = createButton("Add Rect");
  addRectBtn.position(0, 0);
  addRectBtn.mousePressed(addRect);
  saveBtn = createButton("Save Array");
  saveBtn.position(100, 0);
  saveBtn.mousePressed(saveArray);
  input = createFileInput(handleFile);
  input.position(220, 0);
  background(220);
  counter = -1;
}

function draw() {}

function saveArray() {
  print("rectArray length =", r.length);
  print(r);
  writer = createWriter("rect.txt");
  for (let i = 0; i < r.length; i++) {
    writer.print(
      r[i].x +
        "," +
        r[i].y +
        "," +
        r[i].w +
        "," +
        r[i].h +
        "," +
        red(r[i].bkgrnd) +
        "," +
        green(r[i].bkgrnd) +
        "," +
        blue(r[i].bkgrnd)
    );
  }
  writer.close();
  writer.clear();
}

function addRect() {
  counter++;
  r[counter] = new Rect(
    random(400),
    random(400),
    random(200),
    random(200),
    color(random(255), random(255), random(255))
  );
  r[counter].display();
}

class Rect {
  constructor(x, y, w, h, bkgrnd) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.bkgrnd = color(bkgrnd);
  }

  display() {
    fill(this.bkgrnd);
    rect(this.x, this.y, this.w, this.h);
  }
}