Project Details

Status
ongoing
Started
yesterday
Last worked on
today, "A bit more p5 details, it is too addicting"
Total
4.50 h in 2 sessions

This project is not really needed or important, it is just something I want to do. So I saved it for a beautiful snowy saturday, when I am free of the usual work todos during the week.



What is p5.js?

Well, p5.js is a project I have been interested in and playing around with since years. It is the kind of project that comes out when two beautiful worlds collide: coding and art. It is a very visual way of programming, where you can make algorithmic art, visualize data, make little games, all while being super simple to learn and with a vibrant community. It is a bit like in drawing, every project is called a “sketch” and usually you always see something. Here are many examples of the cool things you can do with it. And it is just a single p5.js file. Amazing 🤯.

It is so versatile and playful, I would like to have p5.js inside the toolbelt for this blog for the future. So whenever I want to visualize or animate something, or just have some fun, I can just pull it out and use it.

So here is what I set out to do:

🎯
Definition of Done
Have p5.js implemented in minimal-paper theme, as a shortcode that can be called from within the markdown files. Multiple sketches should be able to run in a page, ideally in p5 global mode (normal coding like in the examples, no instances). The sketch files should be able to live in the bage bundles. And the sketch should elegantly be able to pick up site theme colors and the width of the parent content, to seamlessly adjust to the site style.

Prototyping

As a first step, I was discussing with ChatGPT how a good implementation in Hugo could look. There is a lot I find critical about LLMs, but the explorative use of going through options and learning about best practises is really a strength in my opinion. After some discussion, I started testing out a first approach: I downloaded p5.js and put it in the assets/js/ directory of my theme. I also created a p5.css in assets/css for future styling. I figured out the most elegant way would be to store the sketch inside the page bundle, e.g. as sketch.js and then call it via shortcode. Something like {{< p5 src="sketch.js" >}}.

Going through some options with ChatGPT, I quickly realized that if I want multiple sketches to coexist in a single page, I have think about how to isolate the sketches. This is how a normal sketch usually looks (global mode):


function setup() {
  createCanvas(720, 400);
}

function draw() {
  circle(mouseX, mouseY, 20);
}

And here is how an isolated / instanciated sketch would look like:

const sketch = (p) => {
  p.setup = () => {
    p.createCanvas(720, 400);
  };

  p.draw = () => {
    p.circle(p.mouseX, p.mouseY, 20);
  };
};

new p5(sketch);

As I significantly prefer the first option, I have to find another way to isolate the sketches. ChatGPT suggested to use iframes as a way of isolation. Inside an iframe, each sketch could live seperatedly and use the more elegant global mode. I do not have enough experience to judge how good / bad it is to encapsule the sketches in iframes, but I gave it a try.

Some back and forth with ChatGPT and changing things myself lead to the following p5.html shortcode:

{{- $sketch := .Page.Resources.GetMatch (.Get "src") -}}
{{- $radius := .Get "radius" | default "12px" -}}
{{- $bgvar := .Get "bg" | default "--border" -}}


<div class="p5-wrapper" style="border-radius:{{ $radius }}; width:100%;">
  <iframe
    class="p5-frame"
    scrolling="no"
    style="border:0; border-radius:{{ $radius }}; width:100%; height:400px;"
    srcdoc='
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <style>
        html, body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background: transparent;
        }
        
        canvas {
            display: block;
        }
    </style>
    <script src="/js/main.js"></script>
</head>

<body>
    <script>

        window.addEventListener("load", () => {
            // After iframe has loaded and sized, run the sketch
            {{ $sketch.Content | safeJS }}
        });

        const bgColor = getComputedStyle(parent.document.documentElement)
            .getPropertyValue("{{ $bgvar }}")
            .trim();

        window.__P5_BG__ = bgColor;

        {{ $sketch.Content | safeJS }}
    </script>
</body>
</html>'
  ></iframe>
</div>

With this, it needs a minimum of {{< p5 src="sketch.js" >}} to render a sketch. Let’s take the following code as a minimal example, that draws circles under the mouse (this is kind of the ‘hello world’ of p5).

// This function is run just once
function setup() {
  // Create a canvas that is 400px wide and 300px high
  createCanvas(400, 300);
  // Color the background with a light gray
  background(220);
}

// This function is run around 30 times a second
function draw() {
  // Every second, draw a circle with 20px diameter under your mouse
  circle(mouseX, mouseY, 20);
}

Here it is in action. Make sure to move your mouse or finger over it.

As you can see, it does not fill too well into the site design. Also, there is always one circle drawn in the top left corner. Lets fix this with a bit more elegant version of this sketch:


function setup() {
  // Create a canvas with the dimensions of the parent element (iframe)
  createCanvas(windowWidth, windowHeight);
  // Take the background color if present, otherwise 220 (light gray)
  background(window.__P5_BG__ || 220);

  // Set fill color to very transparent black
  fill(0, 0, 0, 30)
  // Remove stroke / borders
  noStroke();

  // Print text in the middle
  textSize(30);
  textAlign(CENTER);
  text('Move the mouse\n or touch screen', windowWidth / 2, windowHeight / 2);
}

function draw() {
  // Prevent circle to be drawn in top left corner on page load
  if (mouseX === 0 && mouseY === 0) return;

  // Draw circle under mouse
  circle(mouseX, mouseY, 20);
}

And here is the sketch in the site. Make sure to move over it with your mouse or finger.

The code is far from perfect and still has many issues, but the sketch looks already quite good and fits inside the page well. This is achieved on one side due to using windowWidth and windowHeight while creating. Here, the sketch picks up the dimensions of the iframe automatically and gets the right size.

Also, the shortcode is getting one of the theme colors (--border) and exposing it into the iframe as window.__P5_BG__. This way, the sketch can pick up always the current background color of the theme and style accordingly.

This prototype shows that is works well and this is a feasible way. In the next steps, I would like to make width and height properly configurable, allow centering for smaller sketches, have sensible defaults, expose all theme color variables to the sketch (but for this I need to define how many theme color variables I need) and so on. I also realized that I currently include p5.js inside the page header and it is always loaded when someone visits the site. Probably only few pages will actually make use of sketches, and I will also explore if I can only load it in those subpages.

To show if p5 is able to load more complex sketches, here is an example from the official website. Make sure to click / touch around!

Only load p5.js when needed

This was actually a matter of 2 minutes. I can actually just load p5.js directly in the head of the iframe. So instead of loading main.js like above, I do it like this:

 1<!-- Load p5 as an asset -->
 2{{ $p5 := resources.Get "js/p5.js" | resources.Fingerprint }}
 3
 4<!-- ... -->
 5<head>
 6    <meta charset="utf-8">
 7    <style>
 8      /* ... */
 9    </style>
10    <script src="{{ $p5.RelPermalink }}"></script>
11</head>
12<!-- ... -->

This way, it is only loaded if a sketch exists. Perfect! I do not know at this moment, if it is more elegant to put it in assets/js and load it like above or put it in static/js and load it conventionally, but I keep it like this for now.

Improving width and height variables

Height was also quite easy. With {{- $w := .Get "width" | default "100%" -}} I am able to get 100% width, except I explicitly say otherwise. The same goes for height: with {{- $h := .Get "height" | default "400px" -}} I have a default height, except I want something else. So with the following shortcode, I can have a sketch with custom dimensions:

{{< p5 src="sketch-v2.js" width="400px" height="300px" >}}

Which looks like this:

⚠️
I realized that taking an absolute width like 400px is not smart (I guess in general), as it can be nice on computers, but too wide on phones and go into the right margin. Therefore it makes more sense to use something like 80% to achieve the same, but make it safe across devices.

A small change was needed to the shortcode and now it works properly. Here is an example with 80% width:

Centering sketches

You cannot see it anymore, but when developing this, the smaller sketches were on the left side of the content window and not centered. I think centering them would look more elegant. This also was very easy. I just had to add the following two lines in my p5.css file:

 1.p5-wrapper {
 2  overflow: hidden;
 3  box-shadow: 0 6px 20px rgba(0,0,0,0.08);
 4  margin: 1.5rem 0;
 5  margin-left: auto;
 6  margin-right: auto;
 7}
 8
 9.p5-frame {
10  display: block;
11}