Using instance mode to create multiple sketches on the same page

edited June 2016 in p5.js

Since this question crops up regularly I figured I'd write a separate post with a clear title so hopefully it will be easy to find...

To understand instance mode you have to understand what's happening inside p5 when you invoke it. The official example passes a callback function named 'sketch' to the p5 constructor:

var myp5 = new p5(sketch);

Here's a simplified representation of what p5 does with that callback:

// a pseudo class:
var iamP5 = function(callback) {

    this.name = "P5 impostor";
    this.someProperty = "foo";

    // *** pass a reference to the newly created object into the callback ***
    callback(this);
};

// pseudo-class method:
iamP5.prototype.sayName = function() {
    console.info(this.name);
};

// instantiate the pseudo-class passing it our callback function
new iamP5 (sketch);


// the callback being passed to foo.  Could equally be 
// a directly passed anonymous function
function sketch(input) {
  // Here 'input' is the 'this' being passed to callback() in iamP5's 'constructor'.
  // It therefore acts as a reference to the instance of foo created above.
  // Any property or method exposed by foo can be accessed via this variable.
  // If you need to reference it a lot you might choose a shorter variable 
  // name such as 'p'; which is what is generally recommended with p5...
  console.info(input.someProperty);
  input.sayName();

  // if they aren't protected you can override properties 
  // and methods of the instantiated object (see notes below)
  input.sayName = function() {
      console.log("I am a robot");
  }
  input.sayName();
}

It's obviously significantly more complex than that in reality but I think that's enough to explain the code structure... The ability to override or add properties and methods to the instance of p5 is important for two reasons:

  1. it allows us to pass p5 our own customised setup(); draw() and other functions
  2. it is otherwise very risky to add your own properties or method to the variable referencing p5 (commonly 'p'). The whole point of using instance mode is to protect the global scope (so that, for example, your global variables won't clash with p5 variables). If you start attaching properties/methods to the p5 reference you again risk name clashes.

It's worth mentioning that the ability to pass a function as a parameter to another function - as demonstrated above - is one of the features that makes JS such a powerful and flexible language. It's also a feature that, if not applied carefully, can lead to brittle, hard to maintain code ;)

This post is intended to be helpful to people unfamiliar with instance mode. Feedback is most welcome.

Tagged:

Answers

  • edited June 2016

    Example with two sketches

    index.html

    <html>
    
    <head>
        <title>instance mode demo</title>
    
        <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.23/p5.js"></script> 
    
        <script src="sketch.js"></script>
    
    </head>
    
    <body>
        <!-- these divs will act as containers for our separate sketches -->
        <div id="sketch01"> </div>
        <div id="sketch02"> </div>   
    </body>
    
    </html>
    

    sketch.js

    // this is the shortcut approach to using instance mode; which 
    // actually I'd argue is clearer as it reinforces the fact the callback function
    // is passed as an argument to p5 and that its naming is arbitrary
    // to the point of not being necessary
    new p5(function (p) {
    
      "use strict";
      // declare here any variables that should be global to your sketch
      // JS has function level scope so you don't have
      // to worry about this polluting the other sketches
      // DO NOT attach variables/methods to p!!!
      var colour = 0;
    
      p.setup = function () {
        p.createCanvas(600, 400);
      };
    
      p.draw = function () {
        p.background(colour);
      };
    
      p.mousePressed = function () {
        console.info("sketch01");
        colour = (colour + 16) % 256;
      };
    },
    // This second parameter targets a DOM 'node' - i.e.
    // an element in your hard-coded HTML or otherwise added via a script.
    // You can pass a direct reference to a DOM node or the element's 
    // id (simplest to manually set this in the HTML) can be used, as here:
    "sketch01");
    
    // Now creating a separate instance of p5.
    // Note that since this is being passed a separate callback function
    // we can reuse the variable name 'p' which here references this
    // second instance of p5
    new p5(function (p) {
      "use strict";
    
      // this colour variable is totally separate
      var colour = "#f90";
    
      p.setup = function () {
        p.createCanvas(600, 400);
      };
    
      p.draw = function () {
        p.background(colour);
      };
    
      // it's worth noting that mousePressed does nothing to 
      // check which sketch was clicked on.  It simply registers a click 
      // anywhere on the current page!!!
      p.mousePressed = function () {
        console.info("sketch02");
      };
    },
    // here I'm targeting a different container
    "sketch02");
    

    Note the comment on mousePressed in the second instance: the same will apply to other event listeners so you will need to do additional work to check if the mouse is over the current sketch and be careful with keyboard input as this will trigger events in all sketches (of course that might also be useful in some cases)...


    Attaching to a dynamically created node

    Notice that the sketch import has been moved to the body tag. It should appear after any node (i.e. content) that the sketch script might reference.

    index.html

    <html>
    <head>
        <title>instance mode demo</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.23/p5.js"></script> 
    </head>
    
    <body>
        <script src="sketch.js"></script>
    </body>
    
    </html>
    

    sketch.js

    // create a div attached to a variable (raw JS)
    var newNode = document.createElement("div");
    document.body.appendChild(newNode);
    
    
    new p5(function (p) {
      "use strict";
    
      var colour = "#f90";
    
      p.setup = function () {
        p.createCanvas(600, 400);
      };
    
      p.draw = function () {
        p.background(colour);
      };
    
    }, 
    // Here we pass a direct reference to the DOM node created above
    newNode);
    
  • edited June 2016

    Global variables between sketches

    // it's arguably better practice to attach any globals to a single object
    var myGlobals = {};   
    myGlobals.colour = "#0f6";
    // ...though by no means a requirement
    var foo = "foo";
    
    
    new p5(function (p) {
      "use strict";
    
      var colour = 0;
    
      p.setup = function () {
        p.createCanvas(600, 400);
      };
    
      p.draw = function () {
        p.background(colour);
        p.fill(myGlobals.colour);
        p.rect(100,100,400,200);
      };
    
    }, "sketch01");
    
    
    new p5(function (p) {
      "use strict";
    
      var colour = "#f90";
    
      p.setup = function () {
        p.createCanvas(600, 400);
      };
    
      p.draw = function () {
        p.background(colour);
        p.fill(myGlobals.colour);
        p.rect(100,100,400,200);
      };
    
    },
    "sketch02");
    
  • edited June 2016

    Passing a single p5 reference to external objects

    I'll start with an example of an anti-pattern mainly because I've advocated use of this when working with single sketches...

    WARNING: the following should only be considered if you have a single sketch:

    // Here we store a reference to our sketch instance globally so
    // it can be referenced directly from our pseudo-class
    var $p5 = new p5(function (p) {
      "use strict";
    
      var balls = [];
      var numBalls = 20;
    
      p.setup = function () {
        p.createCanvas(600, 400);
        p.background(0);  
        for(var i=0; i<numBalls; i++) {
            balls[i] = new Ball(20);
            balls[i].draw();
        }
    
      };  
    });
    
    // pseudo class
    function Ball(radius, x, y) {
        this.r = radius;
        // uses global reference to p5 instance 
        this.x = x || Math.random() * $p5.width;
        this.y = y || Math.random() * $p5.height;
    }
    
    Ball.prototype.draw = function() {
        $p5.fill("#f90");
        $p5.noStroke();
        $p5.ellipse(this.x, this.y, this.r, this.r);
    };
    

    The obvious reason the above strategy would be problematic with multiple sketches is that any classes referencing the global variable could only realistically be used in that particular sketch instance. The 'advantage' of this pattern when working with a single sketch is that you don't have to explicitly pass a reference into object instances.

  • edited June 2016

    Passing a p5 reference to external objects

    Storing a reference on the object

    Here a reference to the p5 instance is stored in the object on instantiation. The advantage is you don't have to pass additional parameters to class methods; but referencing the p5 instance does require you to use this.p instead of simply p. You can of course store the reference in a function level variable called p...

    //No need to store a global reference
    new p5(function (p) {
      "use strict";
    
      var balls = [];
      var numBalls = 20;
    
      p.setup = function () {
        p.createCanvas(600, 400);
        p.background(0);  
        for(var i=0; i<numBalls; i++) {
            // pass a reference to our p5 instance into the pseudo class
            balls[i] = new Ball(p, 20);
            balls[i].draw();
        }
    
      };
    
    });
    
    
    // pseudo class
    function Ball(p, radius, x, y) {
        this.p = p;
        this.r = radius;
        this.x = x || Math.random() * p.width;
        this.y = y || Math.random() * p.height;
    }
    
    Ball.prototype.draw = function() {
        // avoid having to repeat this.p
        var p = this.p;
        p.fill("#f90");
        p.noStroke();
        p.ellipse(this.x, this.y, this.r, this.r);
    };
    

    This pattern might be useful if you need to call methods on all instances of a pseudo-class independently from the individual sketches they reference; since you only have to pass a reference when first initialised... You may also want to take this approach if methods are called internally but need to reference the sketch instance; otherwise you may find yourself passing a reference to the sketch instance between internal methods.


    Passing a reference to all object methods

    In this pattern a reference to the p5 instance is passed in whenever it is needed:

    //No need to store a global reference
    new p5(function (p) {
      "use strict";
    
      var balls = [];
      var numBalls = 20;
    
      p.setup = function () {
        p.createCanvas(600, 400);
        p.background(0);  
        for(var i=0; i<numBalls; i++) {
            // pass a reference to our p5 instance into the pseudo class
            balls[i] = new Ball(p, 20);
            // and also into the draw method
            balls[i].draw(p);
        }
    
      };
    
    });
    
    
    // pseudo class
    function Ball(p, radius, x, y) {
        this.r = radius;
        // we still need to accept a reference into the constructor
        // since it's used here:
        this.x = x || Math.random() * p.width;
        this.y = y || Math.random() * p.height;
    }
    
    // We now also have to accept a reference in here:
    Ball.prototype.draw = function(p) {
        p.fill("#f90");
        p.noStroke();
        p.ellipse(this.x, this.y, this.r, this.r);
    };
    

    This avoids the need for aliasing this.p but means you have to remember to pass a reference to all methods that require it; including those called internally... That means object methods dependent on the p5 instance can only be called from inside the sketch code or from methods themselves called from the sketch code.

Sign In or Register to comment.