martes, 9 de noviembre de 2021

P5JS Advanced Book

 P5JS Advanced Book

https://css-tricks.com/creating-book-cover-using-javascript-p5-js/


Creating a Book Cover Using JavaScript and p5.js

Avatar of Engin Arslan
Engin Arslan on 

I recently published a book and an interactive course called Coding for Visual Learners. It teaches coding to beginners from scratch using the widely popular JavaScript programming language and the p5.js programming library. Since p5.js a great and an easy to use drawing library, I wanted to make use of it to create the cover of my book and course as well. This is a tutorial on how to create this particular visual using JavaScript and p5.js.

Book Cover using p5.js (circling 1's and 0's around the title)

p5.js is a drawing & creative coding library that is based on the idea of sketching. Just like how sketching can be thought of as a minimal approach to drawing to quickly prototype an idea, p5.js is built on the concept of writing the minimal amount of code to translate your visual, interaction or animation ideas to the screen. p5.js is a JavaScript implementation of the popular library called Processing which is based on the Java programming language.

The concise nature of p5.js makes it a very easy library to learn. But don’t let this simplicity trick you into believing that p5.js has limited capabilities. p5.js has an impressive amount of functionality, history, and community behind it to make it a valuable learning investment if you ever wanted to create art, design, motion or interactive pieces using code. A p5.js program can be anywhere from a few lines of code to thousands.

You can use p5.js to build intricate data visualizations:

Or, generative art:

In this tutorial, I will be assuming a bit of JavaScript knowledge like familiarity with if-else structures, arrays, etc. If you would like to brush up your JavaScript knowledge or would like to learn this powerful language from scratch I would highly recommend you to check out my book and the course Coding for Visual Learners. It is tailored for the absolute beginner.

I will also assume some level of familiarity with computer graphics so that concepts like coordinate systems or color modes don’t require too much explanation. But in case if anything is not clear, feel free to reach out to me and I will do my best to clarify things.

You can follow along with this write-up either using the p5.js editor on your operating system or use an online code editor. Here is a pen that can serve as a template for your p5.js projects.

Let’s get started!

Part One: p5.js Basics

setup & draw functions

Most p5.js scripts contain two function declarations called setup and draw. You will be writing all of your p5.js related code inside these two functions and p5.js will execute these functions for you in particular ways. It is important to understand how these functions are executed by p5.js since they form the backbone of any p5.js program.

  1. The setup function is where you will be writing code that is related to initialization of your program. Anything that is written inside the setup function gets executed once and it gets executed at the beginning of the program.
  2. The draw function is where you will be writing most of the drawing related functionality. draw function gets executed after the setup function and it gets executed continuously (close to 60 times, frames, a second by default) which lets you create all kinds of interactive and animated visuals using this library.

Given draw and setup functions are pretty essential to the workings of p5.js, this is the boilerplate code that you would need to use for almost every p5.js code you write.

If you don’t explicitly instruct it, p5.js creates a canvas (the drawing area inside the web page that you will be using) using a default size which is probably too small for your purposes. That’s why you are calling the createCanvas function which creates a canvas explicitly using the given x and y dimensions. You are also calling the background function inside the draw function to be able to set a color for the canvas as well. This might not be necessary for boilerplate purposes but it helps with seeing the created canvas.

Colors & Shapes in p5.js

Color setting functions in p5.js, like the background function you are using here, works with either one, two, three or four arguments. A single argument creates a solid color using the given value for the R, G, and B (Red, Green, Blue) components. Calling a color setting function with a single value is same as calling it with the same value as three separate arguments. These two declarations below would create the same result:

background(150);
background(150, 150, 150);

A fourth argument, when provided, sets the alpha (transparency) for the color. A second argument, when is used as the last argument also controls the alpha of the color and hence the alpha of the shapes that you draw after it. Talking of shapes, let’s create a shape. Here is a rectangle, drawn by using the rect function.

The first two arguments to the rect function set the position of the rectangle and the last two arguments are for setting the size. Notice how when used with the arguments 0 and 0, the rectangle is drawn to the top-left of the screen. 0, 0 is the origin point for the canvas and it resides at the top-left corner. Rectangle shapes are drawn from their top-left corner by default in p5.js. If you wanted to change this behavior, you can declare a function called rectMode with the p5.js variable CENTER to change the rectangle drawing mode. It makes sense to place this declaration inside the setup function since you will only have to do it once for the lifetime of a program.

If you wanted to draw this rectangle right in the middle of the screen, you have a couple of options. You could provide it with the value 700/2 so that it would be using half of the width and half of the height. But hard-coding values like this is not a good programming practice. You could try sharing variables among the rect function and the setup and draw functions. But luckily, since placing things relative to the width and height of the canvas is a common operation, p5.js provides you with a shortcut. You can use the p5.js variables width and height to be able to get the current width and height of the canvas. Divide these variables by two to use them for the placement of the rectangle.

So far, you know how to set the color for the background but haven’t seen how to set the color for the shapes yet. There are two functions that allow you to do so. One of them is the fill function that sets the fill color for the shapes that comes after this function declaration and then there is the stroke function that sets the color for the stroke.

How color functions work might feel bit unintuitive since they affect the shapes that come after the declaration and not before. To understand how they operate, you can think of calling these functions as setting the active color for the consecutive drawing operations.

Another related function is strokeWidth, which sets the width of the stroke of the shapes. You will be using all these three functions to create a transparent rectangle with white borders.

You will also create a text in the middle of the screen. For this purpose, you will be using the text function. text function takes 3 inputs, the text to be drawn to the screen and the x and the y position for the text placement. You can also use the textFont function to set the desired font for the text.

There are a couple of things to fix here. First of all, notice how the text is not center aligned. As you can guess by now, there is a function to set how text is aligned, called the textAlign function. You will be using the textAlign function in the setup function since you won’t be changing this property again. You will provide it with two arguments: CENTER, CENTER. This will make the text drawings to be horizontally and vertically aligned.

The second thing to notice is that the text shape is being affected by the fill and stroke values of the rectangle above but you probably don’t want to use these values when drawing the text. Let’s set new values before drawing the text to get rid of the previous values.

To be able to adjust the stroke, you could have either make the stroke color to be transparent or set the strokeWeight to be 0. There is also a noStroke function that is used here which gets rid of the stroke altogether. It doesn’t really matter which one you choose in my opinion, they all have the same effect in this use case.

You will now adjust the shape of the rectangle to encompass this text a bit better. Also, you will make the background black for legibility purposes.

Currently, the text and the rectangle is too small. You already know how to scale up the rectangle. To be able to change the size of the text you could be using a function called textSize. But you will instead use a p5.js transformation function, scale, to handle the scaling since it will allow you to control the scale for the rectangle and the text together in a more straightforward manner. Unfortunately, transformations are not very intuitive when working with p5.js so they require a further explanation.

p5.js transformation functions

Transformations in p5.js happen relative to the origin point. There is a scale function that you can use which would scale the shapes that come after itself by the given amount, but things will get scaled from the origin point (the top-left corner of the screen). This is rarely desirable. When working with shapes, you would generally want to scale them relative to their center point.

Notice how calling the scale function makes everything bigger but also displaces them since scaling is happening relative to the top-left corner.

If only the shape was at the origin point then this scaling operation would have worked for our purposes. Luckily there is a way to achieve that. For this, you should be using the scale function alongside with the translate function. Here is how it works:

You have set the x and y position for both the rectangle and the text to be 0. This moves the shapes to the origin point. Then you call the translate transformation function to move the shapes to their initial position at width/2 and height/2. This works because translate function actually doesn’t move the shapes, it moves the entire coordinate system. Now that the origin of the coordinate system is in the middle of the screen, you can call the scale function, which would do the scaling from this desired point.

Now if this sounds a bit convoluted to you, let me assure that you are not alone. Moving the entire coordinate system to transform a shape is not only an overkill for what you are trying to achieve but can also be impractical since it means that anything else that comes after these shapes will need to use this newly transformed coordinate system which adds a lot of complexity to the process. Let’s see it in an example by drawing a new shape.

As you can see, you are now drawing a circle using the ellipse function at the x and y coordinates of 120 and 0. But the problem is that even though the provided y value is at 0, you can clearly see that the circle is not at top of the screen but in the middle, which happens to be the new 0 value for the y-axis. This would be highly undesirable for most circumstances but luckily there is a way in p5.js to deal with it. Enter the push and pop functions!

push & pop

The p5.js push function allows you to create a new state and pop function restores the state to the previous conditions. This allows you to have completely different settings applied to individual objects without worrying those settings to affect the shapes that come after as long as you do everything in between a push and a pop call. Here is how it works:

Perfect! All that transformation changes you are doing right now remains localized in between the push and pop function calls. It is important to note that you should always call push and pop functions together. Using one but not the other doesn’t make any sense. Now that you fixed the problems regarding transformations, you can get rid of the circle shape. One final thing you will be adding before finalizing this text on the screen is to add a bit of an animation to it. Let’s scale it up a little bit for the first 200 frames of the animation to add a bit more dynamism to the visual. For this purpose, you can use the frameCount p5.js variable. frameCount p5.js variable keeps track of the number of times the draw function is called during the lifetime of a p5.js program.

The following code snippet is added to the program. If the frameCount is less than 200 it adds 1/400 of the current frameCount value to the current scale. The reason why you are using the number 400 is that when the frameCount is 200 you would like to have 0.5 added to the total scale. But this is not the most elegant or scalable way of tackling this problem.

if (frameCount < 200) {
  scale(1.5 + frameCount/400);
} else {
  scale(2);
}

One p5.js function that you can use to derive this incremental scaling value is the map function. map function maps the given value that is expected to be in between the given minimum and maximum range to a number in the desired minimum and maximum range. The following declaration:

map(frameCount, 0, 200, 0, 0.5);

will map the frameCount variable that is in between 0 and 200 to a value in between 0 and 0.5. Let’s make use of it inside the code:

This is so much better. And the code is very scalable as well. You could just be changing the scaleFactor and maxLimit variables to adjust how much and how fast the text grows. In the next part of this walkthrough, you will be building the background effect of the final visual, which is arguably the more exciting part of this visual. But the parts you have seen so far should be enough to get you familiarized with the basics of p5.js.

Part Two: Creating the Ring Shapes

In this second part of the tutorial, you will learn about creating the animated, ring-like shapes in the background. But first, let’s move the text drawing code to its own function so that it doesn’t occupy so much space inside the draw function.

This update didn’t introduce any functionality change but is only a structural update to the code to make it more legible. You won’t need the drawTitleText function for now, so you can comment it out to be able to focus on the background shapes.

You will use a JavaScript object to represent the numbers for the final visual on the screen. Using an object to create as shape makes it easier to think about the shape as an entity of its own with a certain set of properties and behaviors. It also helps with encapsulating the functionality that is relevant to the shape, under the shape itself, which is good for code organization.

Here, you are creating a JavaScript constructor function called Num to represent the numbers that are used in the ring. It takes a couple of arguments; a message for the shape to display, the x, y position, the rotation and the color values for the shape. You haven’t seen the rotate function before but it is pretty similar to scale or translate functions. As the name implies, it transforms the shape by rotating it.

function Num(msg, x, y, rot, clr) {
    this.x = x;
    this.y = y;
    this.rot = rot;
    this.msg = msg;
    this.color = clr;

    this.render = function() {
        push();
        fill(this.color);
        translate(this.x, this.y);
        rotate(this.rot);
        text(this.msg, 0, 0);
        pop();
    }
}

The constructor function has a render method that handles the drawing of the shape to the screen. You can now use this constructor function to initialize an instance of this shape object and draw it on the screen.

A couple of things that are worth mentioning.

First of all, notice how the Num function is pretty generic. It could be used to render any text to the screen in a flexible manner. It is also easy to use since you can simply control the rotation by defining the desired rotation value instead of using push and pop functions externally.

Secondly, see how certain words are being avoided when declaring functions. You could have called the constructor function Number instead of Num but Number is an already existing function in JavaScript so you should try to avoid it. Things wouldn’t necessarily explode if you were to use Number but it is generally a good idea to not cause name conflicts. Also for the arguments, you are not using the rotate or color as parameter names as these are the names for built-in p5.js functions. If you are to use these names for arguments then you would not be able to use the functions that are using the same name in the context of this function. Learning how to avoid these names comes with practice.

Finally, notice how you are providing a rotation value of 90 to the number object, but the number doesn’t seem to have rotated 90 degrees. That’s because p5.js uses radians by default when performing rotations instead of degrees. You can set it to use degrees as the default rotation unit by calling the p5.js angleMode function with the DEGREES variable inside the setup function.

angleMode(DEGREES);

It is great that there is now a single number on the screen but the aim is to have several numbers in a ring formation. Let’s figure out how to do this for a single number and then you can scale the operation to do placement for an arbitrary amount of numbers.

The code is growing in size! This is the code that is added for creating the ring layout.

var radius = 100;
var amount = 15;

push();
translate(width / 2, height / 2);

for (var i = 0; i < amount; i++) {
    rotate(i);
    var num = new Num(1, 0 + radius, 0, 90, 255);
    num.render();
}

pop();

Here we have a for-loop that creates a given amount of Num objects that are away from the current origin, by the given radius amount. This is currently only creating 15 objects. This is to illustrate a point. See how the spacing around the shapes is not even. This happens because transformation calls in p5.js (like translaterotatescale) have a cumulative effect. For example, these two calls that are listed below would result in a total translation of 20 pixels in the x-axis.

translate(10, 0);
translate(10, 0);

To remedy this you can use the push and pop so that the transformation effects remain localized and don’t accumulate. That should take care of the uneven spacing of shapes. You should also make it so that the shapes are evenly distributed around the imaginary circle. To do this you can just divide 360 (the total angle of a circle) with the number of shapes you have to derive how much you should rotate each shape to achieve an even distribution.

Perfect, in the next step, you will be creating a function out of this ring drawing operation. You will make the function to accept radius and amount as arguments. Also inside the function, you will be generating an array of random numbers to be fed into the Num object so that you can pass a random value of 0 or 1 to the object, instead of always using number 1.

For this purpose, you will use the p5.js random function. Here is a snippet that generates a random floating point number in between 0 and 2 (excluding 2) that gets converted to an integer using the JavaScript parseInt function and gets pushed into an array.

var randomNumbers = [];
for (var i = 0; i <= amount; i++) {
    randomNumbers.push(parseInt(random(2), 10));
}

Now you can pass an item from this randomNumbers collection to the Num object instead of always using the number 1. Here is the entire code:

The problem right now is that the p5.js random function is generating a random value for each draw function call but for our purposes, we would like these random values to remain the same throughout the execution of the program. To be able to do so you should use the p5.js randomSeed function. The randomSeed makes sure you are getting the same “random” values for the given seed value. You will make it so that the seed should be configurable from outside the function.

The numbers are not flickering anymore. Since you created the function that draws a single ring, it is not hard at all at this point to draw multiple rings with varying amounts of Num objects using a loop. To keep the example simple, we are using a numeric constant to keep the density of the rings similar to each other.

You are almost done! In fact, I am going to just show the final code at this point, as it doesn’t contain anything you haven’t learned about.

Here are a couple of things that are updated in this version.

  • Updated the background for the Coding for Visual Learners Text so that it’s semi-transparent.
  • Introduced rotation to the rings.
  • Introduced randomness to the color of the Num objects as well.

In case you didn’t want the shape to be animated, you can just call a function called noLoop inside the setup function to only display the first frame of the animation. If you wanted to adjust the shape you can start off by tweaking the amountspacingradius and rotSpeed parameters.

I used the static image for my book cover but the animated shape to create animations for the course. To be fair, I also used Photoshop a bit for the placement of some of the elements but the base shape is created with JavaScript and p5.js which makes me happy as a programmer.

No hay comentarios:

Publicar un comentario