Trying Generative Art with P5.js

In this post I explore the world of generative art with P5.js

Trying Generative Art with P5.js

Hello, people! After a long time without writing any technical article in any of my blogs, I decided to create this space to share with you some of my experiments and also to talk about the things I'm studying and creating. Since this is a new blog, I decided to start writing about something new (to me): generative art. This is a topic that I just started exploring and I think it will suit very well with this moment in my life. So, grab your cup of coffee and have a nice reading!

A First Contact

Generative Art With P5.js

In a Sunday afternoon in 2019, I was very bored and decided to read random articles on the Internet. I wanted to try different aspects of programming. Sometimes you want to code, but you want to try something new, to see the act of coding through a different perspective. I don't know if this happens to you so often but, with me, it is very frequent.

For those who know me very well, I am an arts person. By that I mean: I like not only to read about poetry, painting, music and literature, but I also like to create it. Given the fact that art is a very important subject to me, it is surprising that I never tried generative art before. It is a shame, to be honest!

So, on that day, I decided to search on Google about two subjects that caught my attention for quite some time: maze generation (I will talk about this subject in a future post!) and generative art. I remember that some minutes before I was scrolling through my instagram and I saw a colleague of mine from the university, Bernardo Fontes, posting a series of sketches on generative art. I was amazed by it! "That is it!", I thought. "I need to give it a try!"

As usual, when you are trying to start a new thing, the first thing you do it to search for it like crazy on the internet. I do the same and, on that day, I remember I was not only mesmerized by some of the works I've seen, but also by the question: How should I start? Should I try to do it in Python, C++ or JavaScript? What are the available libraries out there? And what if I want to start with JS: should I draw direct on canvas or using something else?

Well, this is what is known as paralysis analysis. You have so much data available on your hands and so many options that you simply can't decide. You read, read and read and never start, never create. I know it happens A LOT with me and it would not be different with generative art.

But, while I was scrolling through Google's results, I found an amazing website: Inconvergent. It simply caught my attention. I couldn't stop reading his posts. They are simply amazing! So, at some point, I was so excited that I decided to stop and try to reproduce one of his ideas. He had a post about some dots bouncing to and fro, and I said: "ok, let's try to create something similar!"

But how to start?

P5.js to the rescue!

At some point, I simply decided to try P5.js. Sometimes it is better to make a decision and start than to simply over-analyse and do nothing!

I decided to try p5.js because I wanted to draw on the browser and it seems to be a tool that is easy to use. In this way you can focus on the creative process and on the algorithms itself.

As they say on their website:

to make code accessible to artists, designers, educators, and beginners...

So, why not give it a try?

On the second image of the post "shepherding random numbers", he draws some random dots that bounces in a canvas and the movement restart whenever you click on it. That's the example I decided to create alone. I think he even shares the code on how he implemented it, but I wanted to do it by myself using P5.js.

The code for this post is available on my github.

Setting Up

In the P5.js website, you can download the library itself or you can use it through a CDN. In this example, I've used the minified version of the library which I downloaded. But you can also use the CDN version of it. Just add the script tag bellow on your html file:

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

On the body of my index.html, I've added a div with the id "dots-container" and loaded the script that does the magic itself:

<body>
    <div id="dots-container"></div>
    <script src="dots-container.js"></script> 
</body>

The Goal

Our goal in this algorithm is to:

1 - draw a specified number of dots (circles) equaly spaced on the width of the container. It should be placed randomly on the vertical (Y) axis;

2 - for each dot, on the axis Y, draw a "target" circle that is also placed randomly on the same axis.

3 - move the dot to the target spot with an easing effect.

Implementation

I wanted P5.js to draw my canvas in a specific div (container). So, my dots-container.js file have the structure:

const sketch = function(p) {
  // ... all logic here...
}
new p5(sketch, document.getElementById("dots-container"))

Here we specify to p5 that it should draw everything inside the "dots-container" div.

The sketch function is called with an argument "p". I use this object to call all the methods exposed by the P5.js library. The most important methods to notice on the code are the setup, draw and mouseClicked.

The "setup" method is used to create the canvas and spawn the initial dots. This function is called only one time, as the initial setup of our canvas.

p.setup = function() {
  p.createCanvas(WIDTH, HEIGHT)
  dots = generateDots(DOTS_COUNT)
  drawDots()
}

The "draw" method is called over and over by P5 to redraw the canvas. We need to insert our logic of redrawing the dots here. Notice that, if we don't clean the canvas before drawing the dots on each frame, P5 will not do it for us. So before recalculating the dots position, I need to clean it.

// Setup: draws the area (canvas) where we wanna draw
p.draw = function() {
  p.clear()
  drawDots()
}

Lastly, the "mouseClicked". Any time the user clicks in the canvas, I recreate the dots and force a "redraw".

// Redraw the dots every time the user clicks on the canavs
p.mouseClicked = function() {
  dots = generateDots(DOTS_COUNT)
  p.redraw()
}

Calculating and Drawing

Each dot is, in fact, a circle. In this way, before drawing each one of them, I define some constants to reuse in our functions:

const RADIUS = 5,
    EASING = 0.05,
    DOTS_COUNT = 250,
    DOT_MARGIN = 10,
    WIDTH = dotsContainer.offsetWidth,
    HEIGHT = dotsContainer.offsetHeight

For the "dot", I simply draw a circle on the "x" and "y" position and with a diamater calculated during the dot generation function.

function drawDot(dot) {
  p.noStroke()
  p.fill("rgba(0, 0, 0, 0.5)")
  p.ellipse(dot.x, dot.y, dot.diameter, dot.diameter)
}

Notice that, before drawing the elipse itself, we need to setup the brush. This is usually how things works in many different libraries (like Cairo in Python and C++, for instance). The nostroke defined that we just wanna draw an elipse without any border. The fill basically indicates P5 to fill whatever area we draw with a given color. In the example, we draw a gray circle with 50% opacity.

function drawTarget(target) {
  p.push()
  p.stroke(30)
  p.fill(255)
  p.strokeWeight(2)
  p.ellipse(target.x, target.y, target.diameter, target.diameter)
  p.pop()
}

We follow the same approach to draw the targets. First we set the brush and draw an ellipse. But, in this case, you can notice that I've used a "push" before modifying the brush configuration and a "pop" straight after I finish drawing the target. You can think in this push and pop operations like this: you are normally using your main brush. But imagine now that you want to draw another details with different colors and thickness. You have two approaches: you can clean the current brush or pisk a new one. So basically what the push means is "pick a new and clean brush for me to use" After you finish using it, you give it back and pick the main one.

In technical terms, you can think of it as an stack:: the configuration that is in the top is the one that will be used for drawing. So, for the drawTarget, I simply want to draw the target with a new brush, without changing the "global" configuration. In this way, when I call pop, the brush returns to its original configuration.

In the generateDots and in the drawDots I create each dot, each target and do the logic to "move" the dots on the direction of the target with an easing effect. The easing basically means that, the closer it gets to the target, the slower the dot will be. It is kind of an "break" to make the dot stop.

You can check this code on this GitHub repo and also take a look on the CodeSandbox to see it in action.

Conclusions

I know this is nothing impressive, but it is a nice introduction to the P5.js library for me and also my first steps in the world of generative art.

You can expect more posts on this subject in the future.

If you have any doubts, feel free to contact me or add a comment bellow.

See you on the next post!

Comments