A p5.js template for animation

May 27, 2018

While doing a lot of animation with p5.js over the last few months, I’ve slowly developed a template that I now use to start all my projects. It’s designed for my own use but perhaps it could also be useful (in part or as a whole) for other people. So I’m sharing it on GitHub and documenting it below. There’s always ample room for improvement, so if you have any ideas don’t hesite to get in touch.

The main feature of this template is a convenient system to export frames to create videos and gifs. This is accomplished with a small Node.js server, named server.js, that communicates with the p5.js sketch via Socket.io. To export frames, the sketch gets a reference to the web canvas and sends it to server.js, which saves it to disk. The best part is that the exportation can all happen behind the scene, in the command line, using the Node module Puppeteer. Then, when all the frames are exported, they can be turned into videos and gifs using ffmpeg.

Below are all the files included in the template. I will now go through how all the pieces fit together.

frame-export.js
frames
headless.js
index.html
libraries
server.js
sketch.js
style.css

Starting with frame-export.js

After importing p5.js and Socket.io in the file index.html, the template imports frame-export.js. This file has two purposes. Firstly, it establishes whether or not we need to start exporting frames, and secondly, it contains the global function frameExport(), which sends the canvas to server.js. Here is frame-export.js in its entirety:

let GET = {};
let query = window.location.search.substring(1).split("&");

for (let i = 0; i < query.length; i++) {
    if (query[i] === "") // check for trailing & with no param
        continue;
    var param = query[i].split("=");
    GET[param[0]] = param[1];
}

let exporting = (GET["exporting"] && GET["exporting"] == "true") ? true : false;

function frameExport() {
    var formattedFrameCount = "" + frameCount;
    while (formattedFrameCount.length < 5) {
        formattedFrameCount = "0" + formattedFrameCount;
    }
    var dataUrl = canvasDOM.toDataURL();
    var data = {
        dataUrl: dataUrl,
        name: fileName + "-" + formattedFrameCount
    }
    socket.emit('image', data);
}

The exporting boolean will establish whether or not the user intends to export the frames from the sketch. We use Url query strings to set this variable. If the user loads the address http://localhost:8080/, exporting will default to false. To load the sketch with the intent of exporting frames, the user needs to load another address: http://localhost:8080/?exporting=true.

The goal here is to prevent accidental exportation of frames, which can happen quite often if you keep changing and reloading the same p5.js sketch while only sometimes wanting to export frames. Accidental exportations can create large amounts of unwanted files on your hard drive, and can even erase older files with identical names.

Another good side effect of this approach is being able to load a sketch only to see it in the browser, and to load it later to export its frames, all without changing any code in the JavaScript files.

The real magic: exporting frames from the terminal

The best part of this template is the ability to export frames from the terminal using Puppeteer. Puppeteer is a Node.js application which creates a “headless browser”, which is a browser that you can control from the command line.

The code below is what you will find inside headless.js. It’s not a JavaScript file that needs to be linked in index.html, it’s a tiny Node.js application. The only thing that it does is load the address http://localhost:8080/?exporting=true inside Puppeteer’s headless browser, which means that it’ll load your p5.js sketch and trigger the exportation of frames.

const puppeteer = require('puppeteer');

(async() => {
    const browser = await puppeteer.launch();
    // await console.log("Puppeteer launched");
    const page = await browser.newPage();
    await page.setViewport({
        width: 2560 / 2,
        height: 1600 / 2,
        deviceScaleFactor: 2
    });
    // await console.log(page.viewport());
    await page.goto('http://localhost:8080/?exporting=true');
    // await page.screenshot({ path: 'example.png' });
    // await browser.close();
})();

The beauty of exporting frames from the terminal is that you do not need to look at your p5.js sketch as it is exporting (browsers deactivate the p5.js sketch when the page is not displayed). This means that you can start looking at the frames as they appear on your hard drive, and for long sequences, you can do whatever else you want with your computer. No need to wait.

Do note that headless.js contains hard-coded numbers for the width and height of the screen (in the page.setViewport call). This is the size of my screen, so just change these numbers to fit yours.

Global variables in sketch.js

let looping = true;
let socket, cnvs, ctx, canvasDOM;
let fileName = "./frames/sketch";
let maxFrames = 20;

We first declare a looping variable, used to pause and restart the animation by pushing the spacebar. Then, socket, cnvs, ctx, and canvasDOM are all going to be used to

The setup() function

function setup() {
    socket = io.connect('http://localhost:8080');
    cnvs = createCanvas(windowWidth, windowWidth / 16 * 9);
    ctx = cnvs.drawingContext;
    canvasDOM = document.getElementById('defaultCanvas0');
if (exporting) {
    frameRate(2);
} else {
    frameRate(30);
}

And then, the sketch’s frameRate is set according to the exporting variable. This is done because, when exporting simple frames that do not require a lot of computation, I find that exporting too many frames per second ends up clogging the system. I’m still testing this out, but for now, exporting 2 frames per second seems like a good compromise. It’s generally slower but more consistent—it will not slow down over time.

On the other hand, if exporting is false, I set the frameRate to 30. This is just a personal preference—I studied in traditional paper animation and I’m used to thinking in terms of 24 or 30 frames per second (typical frame rates for film and ntsc video, respectively). From what I’ve been able to observe, it doesn’t seem like the web canvas is actually able to dependably refresh at the rate of 24 frames per second. It seems to only refresh correctly at fractions of 60 (so 10, 20, and 30).

The draw() loop

function draw() {
    for (let i = 0; i < 500; i++) {
        let x = random(width);
        let y = random(height);
        ellipse(x, y, 5);
    }
    if (exporting && frameCount <= maxFrames) {
        frameExport();
    }
}

Some convenient hot keys

function keyPressed() {
    if (keyCode === 32) {
        if (looping) {
            noLoop();
            looping = false;
        } else {
            loop();
            looping = true;
        }
    }
    if (key == 'p' || key == 'P') {
        frameExport();
    }
    if (key == 'r' || key == 'R') {
        window.location.reload();
    }
    if (key == 'm' || key == 'M') {
        redraw();
    }
}