Testing With RST

This is a re-do of an old post I made to my main site about making a p5 restructured-text post in nikola based on the p5 get started tutorial. I'm repeating it to make sure I can get it to work here, since it's been so long since I've done it.

First the link to the p5 code:

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.10/p5.min.js"></script>

Note

On the old post I had included the html tag directly in the post (using the .. raw:: directive), but I thought that would be wasteful since you can have multiple posts on one page so I moved it the the BODY_END variable in the conf.py file which adds it to the end of the HTML body. This seemed to work for the first sketch but when I moved to the instance-mode version (see below) it broke (probably because p5 wasn't loaded when I was trying to create an instance of it) so I moved it to EXTRA_HEAD_DATA which so far seems to work. I also experimented with the meta-template plugin but since this repository is all about using P5, that didn't seem the way to go, since it would basically have the same problem as using the .. raw:: directive (which is really doing the same thing as the meta_template approach, meta_template just makes it a little more convenient to do it).

A Version That Worked Once

Here's the javascript for the broken version.

get_started.js (Source)

function setup() {
  var parent_div_id = "get_started";
  // this sets the width of the canvas to match the div (center column)
  canvas = createCanvas($("#" + parent_div_id).outerWidth(true), 300);
  canvas.parent(parent_div_id);
  background(255);
  strokeWeight(3);
  stroke(0, 0, 255);
  fill(255);
}

function mousePressed() {
  /* set background to blue */
  background(0, 0, 255);
}

function mouseReleased() {
  /* set background to white */
  background(255);
}

function draw() {
  var diameter;
  /* Draw circles that change diameter based on mouse speed */
  /* and color based on if mouse-pressed (or not pressed)   */
  if (mouseIsPressed) {
    fill(0, 0, 255);
    stroke(255);
  } else {
    fill(255);
    stroke(0, 0, 255);
  }
  diameter = pow(dist(pmouseX, pmouseY, mouseX, mouseY), 1.5);
  ellipse(mouseX, mouseY, diameter, diameter);
}

This next bit shows how to include the sketch in the post. The naming of the div is important ("get_started" in this case) - the javascript has to reference it (i.e. canvas.parent("get_started")) or the sketch will not appear right here (see the notes below). I'm also using it to get the width for the image. As you can see from the src='get_started.js part, this loads a file called 'get_started.js' that I created earlier and put in the files/posts/testing-with-rst folder (the directory name matches the name of this blog-post file - testing-with-rst.rst).

<div id="get_started">
    <script language="javascript" type="text/javascript" src='get_started.js'></script>
</div>

This at first worked to create a processing sketch in that blank box above this line, but once I added another sketch it broke. Originally this sketch seemed to break the second one, but then I fixed the second sketch and it broke this one. I realized that I was defining the same function names (e.g. mousePressed) in both so one of them was overwriting the other. Now it seems to work again, kind of... If you move your mouse cursor over fast enough that the circle is large while the sketch below is visible you'll see that the two of them have created some kind of super-canvas where they events over one also effects the other. So leaving this in has broken both of them to some degree. I think this function-based approach just won't work.

The Class-Based Version

My solution was to use the instance mode concept (which was surprisingly hard to find using google), basically wrapping everything in a class rather than using the global namespace.

If you can't tell, there's a processing sketch in that blank box above this line. If you move your mouse over it p5 will draw some circles with the radii based on how fast your mouse is moving. If you click and drag it inverts the colors.

Here's the javascript that created it.

get_started_2.js (Source)

/**
 * Instance mode implementation of a simple sketch that draws blue circles
 * on a white background when the user moves the mouse cursor over it, basing
 * the size of the circles on how fast the cursor is moving. If the user clicks
 * and drags the mouse it draws white circles on a blue background instead.
 **/
class GetStarted2 {
  /**
   * The constructor sets up the p5 functions
   *
   * @param {object} p: the `p5` object
   * @param {string} parent_div_id: ID of the div to attach the canvas to
   * @param {integer} height: Pixel-heght for the canvas
   **/
  constructor(p, parent_div_id, height){
    /**
     * Creates the canvas using ``height`` and the width of the parent div
     * Also sets up some coloring
     **/
    p.setup = function() {
      var canvas = p.createCanvas($("#" + parent_div_id).outerWidth(true),
                                  height);
      p.background(255);
      p.strokeWeight(3);
      p.stroke(0, 0, 255);
      p.fill(255);
    };

    /**
     * sets the background and fill to blue
     * and the stroke to white
     **/
    p.mousePressed = function() {
      p.background(0, 0, 255);
      p.fill(0, 0, 255);
      p.stroke(255);
    };

    /**
     * sets the background and fill to white
     * and the stroke to blue
     **/
    p.mouseReleased = function() {
      p.background(255);
      p.fill(255);
      p.stroke(0, 0, 255);
    };

    /**
     * Draws circles that change diameter based on mouse speed
     **/
    p.draw = function() {
      var diameter;
      diameter = p.pow(p.dist(p.pmouseX, p.mouseY, p.mouseX, p.mouseY), 1.5);
      p.ellipse(p.mouseX, p.mouseY, diameter, diameter);
    };
  }; // end constructor
};// end GetStarted2

/**
 * Builds the GetStarted2 sketch
 *
 * p5 thinks it's getting a function, not a class
 * so it doesn't use the `new` statement. This function
 * works around that and passes in some parameters
 *
 * @param {object} p: a ``p5`` instance
 **/
function build_get_started_2(p){
  return new GetStarted2(p, "get_started_2", 300);
};

var p5_retest = new p5(build_get_started_2, "get_started_2");

A Little Explanation

So, for my future self who will likely forget what this is doing, here's some notes.

var p5_retest = new p5(build_get_started_2, "get_started_2");

The last line creates a p5 object which calls the build_get_started_2 function, passing the instance into it. In addition, if you pass in the ID of the container, it will attach the sketch to it. So the second argument "get_started_2" does what you would otherwise do in the constructor like this:

p.canvas.parent("get_started_2");

If you omit the name of the parent ID when creating the p5 object then this line would need to go back into the constructor. I kind of like using the explicit p.canvas.parent("get_started_2"); call, but I already did something similar to it in the previous (broken) sketch so I thought I'd pass it in to the p5 constructor here so I'd have it documented.

The build_get_started_2 function:

function build_get_started_2(p){
  return new GetStarted2(p, "get_started_2", 300);
};

This works around a problem that I've often frequently with callback-functions - you have no control over how the function is going to be called, and without the code or explicit documentation you have to work with it as a black-box (I'm sure it's documented somewhere but I find the processing documentation for things other than the sketching to be a little hard to find). In this case I didn't really plan on it being a function, which probably wouldn't be a problem for someone who works with javascript a lot, but I'm not one of them. I'm not really taking advantage of any class-based features in my sketch so I could have converted it to a function, but I prefer classes (probably because I mostly code in python) so I'll probably be using them in the future, so it was good to get this figured out now. Also, since I had to make a builder anyway, this allowed me to put the constants (like the height) outside of the class and pass them in as parameters.

Other than that, the code is pretty close to the function-based version, except using th. p5 instance instead of the global functions.

Notes

  • In order to get the javascript into the final HTML you need to put in the listings folder at the root of the nikola folder and use the listing reStructuredText directive instead of include (this is a special nikola directive). This not only shows it but creates two links - the one with the file name links to a formatted version of the code and the (Source) link lets you download it as plain-text. Showing the code and creating another page seemed kind of redundant to me, but I think the purpose is to provide a link you can refer to outside of this post. You can't see the directive but it says:

.. listing:: get_started.js javascript
  • Another thing I forgot to mention in the other post was that nikola looks for the javascript (when it includes it in the html, not when it wants to show it as in the first note) in the files/posts/<post-name-slugified>/ directory, so in this case, besides putting the javascript in the listings folder, I also had to put another copy of it in files/posts/testing-with-rst/. There must be an easier way. Well, I guess it's not hard, it just seems inefficient.