Defining setup() and draw() at runtime

The following probably ought not be done, as it seems, at best, poor programming style. Perhaps, even worse, it could break in the future. However, as of this time, it does work without any obvious problem, so here goes, followed after the second example by a question …

The following randomly chooses between two different setup() functions at runtime, each designating a different canvas size, among other details:

function preload() {
  
  /**** randomly choose a setup function ****/
  
  switch (random([0, 1])) {
    case 0:
      setup = function () {
        createCanvas(377, 233);
        background(255, 64, 128);
        fill(255, 128);
        stroke(0);
        noLoop();
      };
      break;
    default:
      setup = function () {
        createCanvas(233, 144);
        background(255, 128, 64);
        fill(0, 128);
        stroke(255);
        noLoop();
      };
      break;
  }
}

function draw() {
  rect(5 * width / 16, height / 5, width / 2, height / 3);
  triangle(
    width / 2,
    height / 4,
    width / 4,
    (height * 3) / 4,
    (width * 3) / 4,
    (height * 3) / 4
  );
}

We have been warned about Things That May Break Your 2.x Sketches. Specifically, it was stated:

Do not use variables in size() - This time we really mean it. …

Technically, the above example does not use variables in size(), but it does defer defining the dimensions of the canvas.

This second example randomly chooses between three different draw() functions at runtime:

function setup() {
  createCanvas(377, 233);
  noLoop();
  rectMode(CENTER);
  background(255, 255, 127);
  fill(255, 255, 255, 127);
  
  /**** randomly choose a draw function ****/
  
  switch (random([0, 1, 2])) {
    case 0:
      draw = function () {
        let nudge = width / 12;
        rect(width / 2 + nudge, height / 2, width / 2, height / 2);
        rect(width / 3 + nudge, height / 3, width / 2, height / 3);
      };
      break;
    case 1:
      draw = function () {
        let nudge = width / 12;
        ellipse(width / 2 + nudge, 3 * height / 5, width / 2, height / 2);
        rect(width / 3 + nudge, height / 3, width / 2, height / 3);
      };
      break;
    default:
      draw = function () {
        let nudge = width / 12;
        triangle(
          width / 2 + nudge,
          height / 4,
          width / 4 + nudge,
          (height * 3) / 4,
          (width * 3) / 4 + nudge,
          (height * 3) / 4
        );
        rect(width / 3 + nudge, height / 3, width / 2, height / 3);
      };
  }
}

Regarding both of these examples, there are obviously other means of accomplishing the same effects. But, here’s the question … Are there any circumstances at all under which it would be beneficial to choose between alternative definitions of setup() or draw() at runtime?

1 Like

The caution about size() is specific to processing. It is not relevant for p5.js.

2 Likes

Declaring global draw and setup functions inside of the preload() function is not going to work because p5.js never calls preload() unless it finds a global setup(). However, you could declare your draw function within setup:

function setup() {
	createCanvas(windowWidth, windowHeight);
	angleMode(DEGREES);
	background(100);
	window.draw = random([draw1, draw2, draw3]);
}

function draw1() {
	circle(mouseX, mouseY, 20);
}

function draw2() {
	square(mouseX, mouseY, 20);
}

function draw3() {
	triangle(
		mouseX + cos(30) * 20,
		mouseY + sin(30) * 20,
		mouseX + cos(150) * 20,
		mouseY + sin(150) * 20,
		mouseX + cos(270) * 20,
		mouseY + sin(270) * 20
	);
}

You could even change the draw function while the sketch is running:

function setup() {
	createCanvas(windowWidth, windowHeight);
	angleMode(DEGREES);
	background(100);
	window.draw = random([draw1, draw2, draw3]);
	setInterval(
		() => {
			window.draw = random([draw1, draw2, draw3].filter(d => d !== window.draw));
		},
		2000
	);
	rectMode(CENTER);
}

function draw1() {
	fill('red');
	circle(mouseX, mouseY, 20);
}

function draw2() {
	fill('green');
	square(mouseX, mouseY, 20);
}

function draw3() {
	fill('blue');
	triangle(
		mouseX + cos(30) * 10,
		mouseY + sin(30) * 10,
		mouseX + cos(150) * 10,
		mouseY + sin(150) * 10,
		mouseX + cos(270) * 10,
		mouseY + sin(270) * 10
	);
}

If you want to declare everything (preload/setup/draw) at runtime, then I would suggest you use instance mode:

function mySketch(p) {
	p.setup = function setup() {
		p.createCanvas(p.windowWidth, p.windowHeight);
		p.background(100);
	}

	p.draw = function draw() {
		p.ellipse(p.mouseX, p.mouseY, 20, 20);
	}
}

function startSketch() {
	console.log('starting');
	window.sketchInstance = new p5(mySketch);
}

One reason you might do this would be if you wanted to create your sketch when the user clicks a button on your webpage (perhaps you want to have multiple sketches that the user can toggle between.

In general I think that any user who has gotten past the basics of JavaScript should be using instance mode because it is much more reflective of best practices in JavaScript.


As far as I can tell, there is no way to get p5.js to properly load if you only declare a global setup() function at runtime (i.e. in response to some user action or on a timer).

2 Likes

I use these 3 lines below to force p5.js to instantiate on global mode:

window.mocha = 'Hack to block p5.js auto global instantiation.';
new p5; // Prematurely instantiates p5.js so its props are available now.
window._setupDone = undefined; // Blocks duplicate instantiation warn.

An online example sketch using the trick above:

2 Likes

Nice. That is quite the hack :joy:. All that so you can use cos() at the top level?

1 Like

Yea! I totally coulda just used Math.cos() instead. :roll_eyes:
But I’ve just wanted an opportunity to test the p5js premature instantiation. :stuck_out_tongue_winking_eye:

P.S.: Also needed to access constant DEG_TO_RAD. :joy_cat:

2 Likes

Thanks - good advice. This creates a new draw() function whenever the mouse is released:

// create anonymous p5 instance
new p5(function(p) {
  p.setup = function() {
    p.createCanvas(p.windowWidth, p.windowHeight);
    p.background(255);
    p.noStroke();
    
    // create initial draw function
    p.draw = function() {
      p.textAlign(p.CENTER, p.CENTER);
      p.textSize(p.min(p.windowWidth, p.windowHeight) / 12);
      p.text("Click mouse\nto begin\na new drawing", p.width / 2, p.height / 2);
    };
  };

  p.mouseReleased = function() {
    p.background(127);
    
    // mouse release creates a new draw function
    p.draw = function() {
      let x = p.mouseX;
      let y = p.mouseY;
      p.fill(((x + y) % 32) * 8);
      p.ellipse(x, y, 8, 8);
    };
  };
  
});
1 Like

A classic case where I see people trying to do things like this is sketch galleries / sketch portfolio loaders. There are many sketches – from a single person, or from students in a class, etc. – and the question is how to combine the existing draw function into either a slideshow or a pick-a-random-sketch frame populator.

The key element here is that the sketches to be combined / randomized already exist with separate draw functions – so the question of combining their contents is often “how little refactoring can I get away with and still make this work?”

1 Like

Therein lies much of the fun of trying. :grinning:

1 Like