Illustration
This article explains how to integrate a p5.js animation into a web page. It includes a series of questions and answers to test readers' understanding of basic p5.js concepts. The accompanying animation demonstrates both the integration process and the concepts in action. The Actor metaphor is used twice, first to introduce the concepts and second for the implementation.

The objective is to provide basic p5.js understanding, sufficient to integrate simple animations into a web page.

  1. The section Experiment demonstrates the integration of p5.js animation into this web page.
  2. The section QA tests the readers understanding of p5.js through a series of questions and answers. It provides:
    1. a mental model to think about p5.js ;
    2. a trivial example ;
    3. the starting point that leads to the animation in the Experiment section.
  3. Finally, the complete code of the article is available here: GitHub.

The objective of the experiment is to display a grid of interconnected nodes. An animation will demonstrate how probes propagate from one node to another by sensing the neighbors of each node and choosing to jump to the next node in a given direction.

A grid of nodes is displayed below. Three probes are started at the top left corner, each with a given direction.

What is p5.js built for?

  1. p5.js is a tool for learning to code and make art.
  2. p5.js allows to draw images on a web page using JavaScript.

Provide a way to think about p5.js.

  1. One way to think about p5.js is in terms of Actors.
  2. Providen an HTML Canvas canvas, p5 ≡ p5.js(canvas) is the Actor we study.
  3. p5 sends itself messages:
    1. setup().
    2. draw() 60 times per seconds.
  4. By default, when p5 receives the setup() message, it does nothing.
  5. By default, when p5 receives the draw() message, it does nothing.

Provide a way to think about the p5.js programmer.

  1. One way to think about the programmer is in terms of Actors.
  2. The programmer tells p5 what to do when it receives the messages setup() and draw()
  3. Given the programmer instructions, p5 does something instead of nothing.

The programmer wants p5 to:

  1. Create a canvas ;
  2. Paint it in red.

What could be the dialogue between the programmer actor and the p5 actor?

Here a possible dialogue:

  1. programmer to p5:
    • When you receive the setup() message, then: create your canvas. It should be 600 pixels wide and 300 pixels high.
    • When you receive the draw() message, then: paint your canvas red.

That is how programmer and p5 interact. As a programmer, the objective is to add more and more things to setup and draw to make p5draw what is desired on the canvas.

The programmer wants p5 to:

  1. Create a canvas ;
  2. Paint it in red.

What could be the code written by the programmer and received by the p5 actor?

  1. Given the vocabulary understood by p5 ;
  2. the code might be: function setup() { createCanvas(600, 300); } function draw() { background("red"); }
  3. The result can be tested using: p5.js editor.

Provide a way to think about p5.js.

  1. One way to think about p5.js is in terms of Actors.
  2. A programmer tells p5 what to do when it receives setup() and draw() messages.
  3. The vocabulary that programmer can use to influence p5 behavior is given by p5 documentation.

How might this code be interpreted?

function setup() { createCanvas(600, 400) } function draw() { background("blue"); fill("yellow"); stroke("orange"); strokeWeight(20); circle(300, 200, 100); }

The programmer tells p5:

  1. When you receive setup(): create a canvas, 600px wide and 400px high.
  2. When you receive draw():
    1. Draw the background blue.
    2. The next shape will be filled with yellow.
    3. The stroke of the next shape will be orange.
    4. The stroke weight of the next shape will be 20.
    5. Draw a circle shape with a center at (300px, 200px) and a diameter of 100px.

We call grid the grid shown in the Experiment section. What does it mean to map the grid to actors?

  1. We define a state as the data that gives one frame of the grid.
  2. We define the computation as a function that transforms one frame to the next.
  3. The state of the grid is partitioned into a set of states, each encapsulated into an Actor.
  4. The computation is partitioned into all the functions of all actors.

Describe the grid.

A possible description is:

  1. There is a square grid of n positions.
  2. At each position, there is a node.
  3. Each node is connected to its neighbors.
  4. Given a start node, probes have propagated in every direction possible.
  5. Each direction has a color.

Describe the grid state in terms of actors.

A description might be:

  1. There is a grid, an instance of Grid.
    • It has a center position w.r.t the top left corner of the canvas.
    • It has a width.
    • It has a color.
    • It fits several nodes per line.
  2. There are nodes, instances of Node. A node has:
    • a position ;
    • a radius ;
    • a color ;
    • neighbors.
  3. There are probes, instances of Probe. A probe has:
    • a direction ;
    • a color ;
    • nodes that have been visited.

Describe the grid computation in terms of actors.

The computation might be mapped into the following sequence of events:

  1. p5 receives the setup() event.
    1. The canvas is created.
    2. The grid is created.
      1. Nodes are created.
      2. Nodes are interconnected.
    3. Probes are created.
  2. p5 receives the draw() event.
    1. The canvas background is painted
    2. The grid is drawn.
    3. Probes are drawn.
    4. Each probe try to reach a neighbor in its direction if any.
    5. If all probes are immobile, then: stop the computation.

Assume that the HTML canvas may be built like so:

const parent = document.getElementById("p5") const canvas = new Canvas(parent) canvas.start()

Provide an implementation of setup.

An implementation might be:

function setup() { // The canvas is built. const parent = document.getElementById("p5") const canvas = new Canvas(parent) canvas.start() // The grid is built. const parent_width = parent.offsetWidth const grid_center_pos = new Pos(parent_width/2,parent_width/2) grid = new Grid( grid_center_pos, parent_width*3/4, white, 10 ) // The probes are built. start_node = grid.get(0,0); probes = [ new Probe(start_node, red_500, right_dir), new Probe(start_node, blue_500, bottom_right_dir), new Probe(start_node, green_500, bottom_dir), ] }

Provide an implementation of draw.

An implementation might be:

function draw() { background(gray_50.hex) grid.draw() probes.forEach((probe) => probe.draw()) start_node.draw_disk("black") probes.forEach((probe) => probe.next()) probes.every((probe) => !probe.moving) && noLoop() }

Provide an implementation of Node.

An implementation might be:

class Node { #pos #radius #color #neighbors constructor(pos, radius, color) { this.#pos = pos this.#radius = radius this.#color = color this.#neighbors = new Map() } get pos() { return this.#pos } get radius() { return this.#radius } add_neighbor(dir, node) { this.#neighbors.set(dir, node); } get_neighbor(dir) { return this.#neighbors.get(dir) || null; } draw_disk(color = null) { fill(color === null ? this.#color : color) const start = this.#pos circle(start.x, start.y, this.#radius * 2) } draw_edges() { stroke(this.#color) const start = this.#pos this.#neighbors.forEach((neighbor) => { if (neighbor !== null) { const end = neighbor.pos line(start.x, start.y, end.x, end.y) } }) } }

Provide an implementation of Probe.

An implementation might be:

class Probe { #color #dir #moving #nodes #head constructor(node, color, dir) { this.#nodes = [node] this.#head = node this.#color = color this.#dir = dir this.#moving = false; } get moving() { return this.#moving } next() { const node = this.#head.get_neighbor(this.#dir) if (node === null) { this.#moving = false } else { this.#head = node this.#nodes.push(node) this.#moving = true } } draw() { this.#nodes.map((node) => { const pos = node.pos fill(this.#color.hex) circle(pos.x, pos.y, node.radius * 2) }) } }
  • It would be preferable to avoid using the global scope — i.e. to encapsulate setup and draw.
  • A few details have been left out — e.g. the use of frameRate to slow down the animation.
  • The integration has been left out — e.g. the implementation of Canvas. The interested reader may consult the code.
  • A few concepts have been intentionally left out — e.g. class Direction { … }. This omission illustrates two important concepts: Thinking Above The Code (YouTube) and how Whishful Thinking helps programming.

    Wishful Thinking is a very powerful programming practice:

    Before implementing a component you write some of the code that actually uses it. This way you discover what functions with what parameters you really need, which leads to a very good interface. You will also have some good test code for your component.

    The idea is based on the fact that an interface's purpose is to simplify the code that uses the component, not to simplify the code that implements it.

  • One way to think about p5.js using the actor model has been introduced.
  • A way to implement an animation using p5.js and the actor model has been introduced.
  • An animation using p5.js has been integrated into this web page.
  • A way to build non-trivial animation has been introduced.
  • A few Programming principles have been illustrated, in particular: Thinking Above The Code and Whishful Programming.
  • This web page code is available here: GitHub