p5.js With Org-Mode and Nikola
Table of Contents
What Is This, Then?
This is a re-working of a p5js post I made a while ago to see if I could remember how to use it with nikola, my static site generator of choice. Upon revisiting it again I realized that the text was describing how to do it with posts written in restructuredText
even though I re-wrote the post in org-mode, and haven't used restructuredText
in a while so it didn't really make sense. Additionally, I had assumed since I have the original org source, whenever I want to remember how I had used p5js in org-mode, I could just read it and figure out what I had done - but while that was sort of true, it was harder than I would have liked, so this is going to be a post showing:
- How to put a p5js sketch into an org-mode post for HTML exporting.
- How to put multiple p5js sketches on the same page without them conflicting with each other.
- How to display the javascript for the p5js sketch when it's in a separate file.
This is nikola specific, but I'm assuming it'd be similar if I switched again to another system.
The Short Version
To put a p5.js sketch into an org-mode document:
- Include the p5 library in a script tag
- Create an Instance Container to place the sketch somewhere on the page.
- Create a javascript sketch using Instantiation and associate it with the Instance Container
- For Nikola, put the javascript sketch in a folder named after the slug of the post (
files/posts/<SLUG>/
).
The First Sketch
The sketches I'll be using came from Getting Started With Processing. This first one creates a circle that follows your mouse as you move it around.
The HTML Elements
To start we'll create two HTML elements that we want to export as HTML from the org-mode post. One is a script
element that imports the javascript for the p5 sketch, the other is a div
element which the javascript puts the sketch into (the p5js documentation calls this an Instance Container). Since we want the HTML elements themselves to be in our final HTML document we put in an org-mode export block.
#+begin_export html
<script language="javascript" type="text/javascript" src='get_started.js'></script>
<div id="get-started"></div>
#+end_export
Wherever you put that block in the org-mode post is where the p5 sketch will be. I created the sketch in a file called get_started.js
so I tell the the script tag to import it with src='get_started.js'.
The div id
is how the javascript knows where to put the sketch, which I'll go over when we get to the second sketch. The big white space above this paragraph is created because that's where I put the real export block (the one shown above the white space is just for documentation, the real block is hidden) so if you move your cursor over it you should see something happen.
The Javascript
When nikola creates the page from this post it's going to copy over the javascript file ("get_started.js") and put it next to the HTML file for the post, which is why we can just put the file name in the script
tag and don't need a path. In order for nikola to find the original file to copy over we need to put it in a folder whose path looks like files/posts/<SLUG>/get_started.js
(starting from the root of the nikola repository). You can probably change it but this is the way it works by default.
The <SLUG>
has to be the slugified name for the post (for nikola this is set in the slug
metadata at the top of the post). In this case I used the much too long slug p5-js-with-org-mode-and-nikola
so the path to the javascript file is files/posts/p5-js-with-org-mode-and-nikola/get_started.js
.
We need to know this for two reasons:
- One is that it's where you have to put the file or it won't get copied over when nikola builds the site (you will have to make the folder if you didn't do it previously).
- The other is that I want to show the javascript itself in this post so we'll put in an org-mode
include
directive with the path to the file.
The thing that I tend to stumble over when embedding other files into an org-mode post is that the path for the include
directive is relative to the location of the org-mode post, not the root of the nikola repository. Since the org-mode file for this post is in a sub-folder named posts
, our path needs to go up one directory first, then back down, like this:
#+include: ../files/posts/p5-js-with-org-mode-and-nikola/get_started.js src js
Wherever you put the include directive in the post is where org-mode will insert the contents of the file. The arguments after the path tell org that it's a source block (and not, say, an example) and that it's in javascript so org will apply the right syntax highlighting to it, like so:
function get_started(p5js){
const WHITE = 255;
const COLOR = p5js.color(0, 0, 255);
let diameter;
p5js.setup = function() {
let canvas = p5js.createCanvas(0.8 * p5js.windowWidth, 200);
p5js.background(WHITE);
p5js.strokeWeight(3);
p5js.stroke(COLOR);
p5js.fill(WHITE);
}; // setup
p5js.mousePressed = function() {
p5js.background(COLOR);
}; // mouse_pressed
p5js.mouseReleased = function() {
p5js.background(WHITE);
}; // mouse_released
p5js.draw = function() {
/* Draw circles that change diameter based on mouse speed */
/* and color based on if mouse-pressed (or not pressed) */
if (p5js.mouseIsPressed) {
p5js.fill(COLOR);
p5js.stroke(WHITE);
} else {
p5js.fill(WHITE);
p5js.stroke(COLOR);
}
diameter = p5js.pow(p5js.dist(p5js.pmouseX, p5js.pmouseY,
p5js.mouseX, p5js.mouseY), 1.5);
p5js.ellipse(p5js.mouseX, p5js.mouseY, diameter, diameter);
}
}; // get started
new p5(get_started, "get-started");
You don't need to actually show the javascript, and I would normally do it by putting the code in the post itself and then tangling it out, the way I do it for the next sketch, but this is how I did it originally and so this is for the future me that might want to but will probably forget how to do it.
The Second Sketch
This second sketch is also from the "Getting Started With Processing" book and I'm including it to show that the way we're creating the p5 sketches not only specifies the location of the sketch (via the Instance Container) but we're also using what the p5js documentation calls Instance Mode Instantiation to keep the sketches from stomping on each other's variables.
The Sketch Function
If you look at the p5js.org documentation you'll notice that they generally create them using global variables. This works okay if you only have one sketch on your page, but if you have more than one the code from the different sketches might interfere with each other (if you have things with the same name, e.g. setup
then the later definitions will replace the earlier ones as the browser loads the page). The way that we keep our sketches from interfering with each other is to create a function for each sketch that takes a p5 object as its argument and then do all the other defining within that function. This first block that we're looking at gives the function declaration for our second sketch and also defines some variables.
let rotate_translate_sketch = function(p5js) {
const WHITE = 255;
const COLOR = p5js.color(255, 0, 0);
let angle = 0.0;
let side;
Normally I would have called the COLOR
constant RED
(or BLUE
in the case of the first sketch) but I thought it'd be a simple way to show that they're not conflicting with each other - if they were declared globally you would get a syntax error and the sketch wouldn't run, since you're re-declaring a constant. If they were variables instead of constants then you wouldn't get an error but the sketches would end up the same color, with one definition of COLOR
overriding the other. This seems like a trivial thing, but it can sometimes create unpredictable conflicts that are hard to troubleshoot and tedious to work around.
Mouse Pressed
I'm not really sure what's going on under the hood, but in order to define behaviors for the p5js methods we have to do what looks like monkey-patching on the p5js object that gets passed in to the sketch function. In this case I want to white-out the sketch whenever the mouse is pressed so I define/re-define the p5js mousePressed
function and assign it to the p5js object.
p5js.mousePressed = function() {
p5js.background(WHITE);
}; // mouse_pressed
Setup and Draw
As with mousePressed
, the way to define the setup
and draw
behaviors for p5js is to create new functions and assign them to the p5js object. Note that in most of the documentation examples the p5js methods are global, but in this case I'm using the methods and attributes associated with the p5js object (e.g. p5js.createCanvas
, instead of createCanvas
).
p5js.setup = function(){
p5js.createCanvas(0.8 * p5js.windowWidth, 200);
p5js.strokeWeight(3);
p5js.smooth();
}; //setup
p5js.draw = function(){
p5js.stroke(COLOR);
p5js.translate(p5js.mouseX, p5js.mouseY);
side = p5js.pow(p5js.dist(p5js.pmouseX, p5js.pmouseY,
p5js.mouseX, p5js.mouseY), 1.5);
p5js.rotate(angle);
p5js.square(-15, -15, side);
angle += 0.1;
} // draw
}; // end of rotate_translate_sketch
And that's the end of our sketch function.
Attaching Our Sketch Definition
So now we have our sketch definition, but how do we pass in the p5js object so we can call it? Well, we don't. Instead of calling the function we create a new p5 object, passing in the function in as the first argument and the ID of the div
block that we want to put it in as the second argument (I'll create the div afterwards).
new p5(sketch=rotate_translate_sketch, node="rotate-translate");
Now our sketch will get attached to the HTML div
block whose ID is "rotate-translate" and run.
The HTML
As with the first sketch, we need to include the javascript file in a script
tag (in this case it's rotate_translate.js
) and create the div
using the ID we passed to the p5 object.
#+begin_export html
<script language="javascript" type="text/javascript" src="rotate_translate.js"></script>
<div id="rotate-translate"></div>
#+end_export
And there you go, two p5js sketches put in an org-mode post and exported to HTML using nikola.
One More Thing
Something I didn't go over is that the p5js library itself has to be pulled into the page so somewhere you need to put an element that looks something like this (assuming it's coming from the CDN):
<script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js"></script>
In my case I made a template for nikola to do it, but I haven't worked with mako for a while so maybe I'll have to look at that again later.
Sources
- p5js.org: The site for p5.js.
- Instantiation: The p5.js example page for keeping variables local to a sketch so sketches don't collide with each other.
- Instance Container: The p5.js example page for putting a sketch in a specific location (using
div
tags). It gives several variations of how to do it, I used what I thought was the easiest way. - Reas C, Fry B. Getting started with Processing. 1st ed. Beijing ; Sebastopol, [CA]: O’Reilly; 2010. 194 p. (Make: projects).