How to Paint with Code: Creating Your Brushes

Published

June 27, 2021

Author

Arianna Ninh

Illustrator

Arianna Ninh

There's a world of possibility in designing our own digital painting tools that we can't easily recreate with analog tools like a normal pencil. In this guide, we'll explore the many ways we can code our own paintbrushes using a Javascript creative coding library called p5.js.

We'll start by creating a basic line drawing application, which will be our template going forward to create a whole toolkit of distinct paintbrushes. We'll mix and match a variety of painting techniques, along with a bit of math, and see what textures emerge in the process.

To get the most out of this tutorial, you should be familiar with the fundamental concepts of Javascript, such as functions, for loops and arrays. However, you won't need any previous experience with the p5.js library so beginners are welcome!

Okay, enough chitchat — let's start painting!

Drawing a simple line

Let's start off with the basics by creating a simple line-drawing tool with p5.js. You can use this code as a starter template and testing playground for any other paintbrushes you make with p5.js.

First, try it out yourself! Click and drag on the canvas below to draw a line.

A brief breakdown of our starter template

Let's take a look at the HTML, CSS, and Javascript code in our template and see how each piece works together. You could test this code in the SuperHi Editor, a CodePen, or the p5.js editor.

Throughout this guide, I'll introduce many built-in functions from the p5.js library, and I'll provide reference links to the p5.js documentation for each one. I encourage you to explore the p5.js documentation yourself as you follow along.

Setting up our HTML and CSS

We'll use minimal HTML and CSS in this guide so that we can focus on painting with Javascript.

First in our index.html file, we'll link the p5.js library to our project's HTML with a <script> tag:

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

Then in CSS, we'll make sure our canvas fills the browser window by setting the body tag's margin to 0 and hiding the scrollbars with overflow: hidden;.

body {
  margin: 0;
  overflow: hidden;
}

Setting up our Javascript with p5.js

Let's shift our focus to writing Javascript with the p5.js library! We'll create two key functions that drive every p5.js sketch, setup() and draw(), and also a third function that will act as our painting tool, pen().

From a high level, this is how our 3 Javascript functions work together:

  • setup() sets up our painting canvas. It runs only once at the start of the program.

  • draw() is like the hand that moves the paintbrush around the canvas. It runs repeatedly.

  • pen() is the painting tool we use to paint on the canvas. It gets called via the draw() function.

Initializing our canvas with setup()

The setup() function is where we'll create our canvas and do any initialization tasks. This is also where you'd put any code that only needs to run once when the program first starts on page load.

function setup() {
  createCanvas(windowWidth, windowHeight)
  background('#fbf8f3')
}

Inside our setup() function, we'll initialize our canvas with two functions:

Painting on our canvas with draw()

The second key function, draw(), is where we can incorporate interactivity and animation into our p5.js sketches. It loops continuously as long as the program is running. In our case, the draw() function is where we'll decide whether or not we should paint on the canvas.

function draw() {
  if (mouseIsPressed) {
    pen()
  }
}

Inside our draw() function, we'll check if the mouse button is pressed with the built-in p5.js boolean variable mouseIsPressed, which returns true if the mouse button is pressed or false if not. If mouseIsPressed is true, then we'll paint on the canvas by calling our "paintbrush" function, pen() .

Styling our paint strokes with a custom paintbrush function

Our very first "paintbrush" is the pen() function, which draws a basic line:

function pen() {
  // set the color and weight of the stroke 
  stroke(0, 0, 0, 255)
  strokeWeight(2)

  // draw a line from current mouse point to previous mouse point
  line(mouseX, mouseY, pmouseX, pmouseY)
}

To understand how this draws to the canvas, let's break this function down step-by-step.

Inside our pen() function, first we set the color of the stroke to a solid black color using the stroke() function. We also set the stroke weight, which is the line thickness, to 2 pixels:

// set the color and weight of the stroke 
stroke(0, 0, 0, 255)
strokeWeight(2)

We're defining our color using the RGBA color mode here, which takes in 4 numbers between 0-255 as parameters: red, green, blue, and alpha (which is the color's opacity). If you'd like, you can try out a different RGBA color, such as stroke(33, 225, 70, 80) which creates a light green and slightly transparent color. To learn more about how the RGBA color mode works, along with other color modes, check out the p5.js Color guide.

Next, we draw a line with the line() function, which draws a line between two points defined by their X and Y coordinates:

// draw a line from current mouse point to previous mouse point
line(mouseX, mouseY, pmouseX, pmouseY)

p5.js gives us four handy built-in variables that change with each call to draw():

  • mouseX and mouseY: the X and Y coordinates of the mouse's current location

  • pmouseX and pmouseY: the X and Y coordinates of the mouse's previous location

We pass these four variables into line() to create a continuous path that follows the mouse.

Putting it all together, this is the Javascript code for our starter template:

function setup() {
  createCanvas(windowWidth, windowHeight);
  background("#fbf8f3");
}

function draw() {
  if (mouseIsPressed) {
    pen();
  }
}

function pen() {
  // set the color and weight of the stroke
  stroke(0, 0, 0, 255);
  strokeWeight(2);

  // draw a line from current mouse point to previous mouse point
  line(mouseX, mouseY, pmouseX, pmouseY);
}

Voilà — you've just made your very first line drawing application!

For your convenience, here's the template code all in one place:

HTML

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

CSS

body {
  margin: 0;
  overflow: hidden;
}

Javascript

function setup() {
  createCanvas(windowWidth, windowHeight);
  background("#fbf8f3");
}

function draw() {
  if (mouseIsPressed) {
    pen();
  }
}

function pen() {
  // set the color and weight of the stroke
  stroke(0, 0, 0, 255);
  strokeWeight(2);

  // draw a line from current mouse point to previous mouse point
  line(mouseX, mouseY, pmouseX, pmouseY);
}

Let's create more paintbrushes!

For the rest of this guide, we'll focus on creating different paintbrushes with p5.js. As you follow along, you can use this same exact template code provided above. To switch the paintbrush, simply swap pen() for a different paintbrush inside of the draw() function.

Also, we'll use a variety of math techniques to define our brush strokes throughout this guide. To make it easy for you to experiment with these techniques and create your own brushes, I've compiled all of these techniques as code snippets, which you'll find at the very bottom of this guide for future reference.

Now, let's explore more p5.js painting techniques and see what fun brushes emerge in the process!

Mimicking a felt-tip marker pen

Instead of drawing a continuous pen line, let's switch things up. We'll draw a translucent circle wherever the mouse is currently located, which paints a line that feels similar to a felt-tip marker.

First, try it out for yourself:

To help you get a sense of how this paintbrush function will work before we start writing the actual code, here is a visual diagram:

Now let's start writing the code. In your Javascript file, create a new function called marker() and fill it out with the steps below.

First, we'll define how we want our circles to look at the beginning of our new paintbrush function. We can set the fill color to a translucent yellow by calling the fill() function with the RGBA color mode, and we can also disable the outline of the shape by calling noStroke().

// set the color and brush style
fill(255, 200, 103, 40)
noStroke()

Then, we'll draw a circle with the p5.js circle() function:

// draw a circle at the current mouse point, with diameter of 50 pixels
circle(mouseX, mouseY, 50)

Similar to how we created the pen() paintbrush function, we pass in mouseX and mouseY as parameters to draw the circle's center at the mouse's current location. To create a thick marker line, we set the circle diameter to 50 pixels, but you can change the diameter to whatever size you'd like.

Putting it altogether, here's our marker() function:

function marker() {
	// set the color and brush style
  fill(255, 200, 103, 40)
  noStroke()
  
	// draw a circle at the current mouse point, with diameter of 50 pixels
  circle(mouseX, mouseY, 50)
}

Since you already have the template code set up, all you'd need to do to use this marker() function is to call it within the draw() function:

function draw() {
  if (mouseIsPressed) {
    marker()
  }
}

Making better brushes and painting smoother strokes

You may have noticed that your paint strokes with the marker() brush don't look quite continuous. Gaps appear between the circles when you draw too quickly. Why is that?

Keep in mind that although we define how our strokes look with paintbrushes like pen() and marker(), it is the draw() function that drives the act of painting on the canvas. Since our marker() can only create one circle per call to draw(), the frequency that the browser can call draw() determines how smooth our marker() strokes look. In this way, each call to the draw() function is like a single animation frame, and repeating draw() over time creates a coherent picture.

Here's another way of looking at it: the draw() function is like the hand that moves our paintbrush around the canvas, but our hand can only move so fast. So, given these constraints, we have to be smart about the way we paint in order for our strokes to look good.

Stringing circles together like beads

One way we can create more continuous paint strokes out of circles is by connecting them together in a sequence without gaps in between, resulting in what looks like a string of beads():

Each call to the draw() function doesn't have to be totally independent – instead, each frame can inform the next one. We'll make each call to draw() dependent on the previous call by centering the circle at the midpoint between the current and previous mouse points. The circle diameter will be the distance between the two mouse points.

To recreate this paintbrush, let's create a new Javascript function called beads() and fill it out with the below steps:

First, we'll set the color of the brush and disable the stroke at the beginning of beads(). Do you notice a pattern forming here? We'll always set the brush styles at the start of all of our paintbrush functions.

// set the color and brush style
fill(185, 83, 213, 180)
noStroke()

Then, we'll find the distance between the current and previous mouse points by passing their X and Y coordinates into the p5.js dist() function:

// find the distance between the current and previous mouse points
const distance = dist(mouseX, mouseY, pmouseX, pmouseY)

Next, we'll find the midpoint between the current and previous mouse points:

// find the midpoint between the current and previous mouse points
const midX = (mouseX + pmouseX) / 2
const midY = (mouseY + pmouseY) / 2

And finally, we'll use these values to draw a circle with the p5.js circle() function, where the circle is centered at the midpoint, and the circle's diameter is the distance:

// draw a circle at the midpoint, with distance as its diameter
circle(midX, midY, distance)

Putting it altogether, here's our final paintbrush function:

function beads() {
  // set the color and brush style
  fill(185, 83, 213, 180)
  noStroke()

  // find the distance between the current and previous mouse points
  const distance = dist(mouseX, mouseY, pmouseX, pmouseY)

  // find the midpoint between the current and previous mouse points
  const midX = (mouseX + pmouseX) / 2
  const midY = (mouseY + pmouseY) / 2

  // draw a circle at the midpoint, with distance as its diameter
  circle(midX, midY, distance)
}

Shifting the colors across the rainbow

Let's switch things up a little bit. We'll use the same beads() function, but instead, we'll gradually change the color in each circle to create a string of rainbow beads.

To create this rainbow effect across the sequence of circles, we'll shift the hue of each circle across the color spectrum. This would be annoying to do in the RGBA color mode, where the color is represented with 3 values for red, green, and blue. However, we can use a different color mode called HSBA, which stands for Hue, Saturation, Brightness, and Alpha. This allows us to represent the hue as a single number that ranges from 0 to 360, as illustrated here:

Let's create a new paintbrush function which we'll call rainbowBeads() . Inside that function at the top, first we need to find the hue of the circle.

p5.js gives us a variable called frameCount, which tells us how many frames have passed so far, or in other words, the total number of calls to draw(). If we divide frameCount by 360, then the remainder will always be some number from 0 to 360.

We can find the hue this way by using the modulo operator, % , which returns the remainder after dividing by some number. To make the color change across the rainbow even faster, we'll multiply frameCount by 10 as well.

// find the hue, which is a number from 0 to 360
const hue = (frameCount * 10) % 360

You can try experimenting with other values besides 10 and 360 as well, and see what happens.

And now that we have the hue, we can create the color in the HSBA color mode by using the p5.js color() function, and then set the brush style as usual:

// set the color and brush style
const hsbaColor = color(`hsba(${hue}, 100%, 100%, 0.6)`)
fill(hsbaColor)
noStroke()

The rest of the code is the same as the beads() function. Putting it all together, here's our final paintbrush function, rainbowBeads():

function rainbowBeads() {
  // find the hue, which is a number from 0 to 360
  const hue = (frameCount * 10) % 360;

  // set the color and brush style
  const hsbaColor = color(`hsba(${hue}, 100%, 100%, 0.6)`);
  fill(hsbaColor);
  noStroke();

  // find the distance between the current and previous mouse points
  const distance = dist(mouseX, mouseY, pmouseX, pmouseY);

  // find the midpoint between the current and previous mouse points
  const midX = (mouseX + pmouseX) / 2;
  const midY = (mouseY + pmouseY) / 2;

  // draw a circle at the midpoint, with distance as its diameter
  circle(midX, midY, distance);
}

Flipping between two states to paint a wiggle

Let's use the same tools we used in our rainbowBeads() paintbrush to create a totally different visual effect: a wiggly line!

This wiggly line is essentially a sequence of alternating semicircles. We'll use the midpoint, distance, direction of the mouse movement, and the frameCount to draw each semicircle as an arc.

First, let's create a new wiggle() function and set the brush styles at the top. By default, p5.js fills in any shape with white, but we can disable this by calling noFill() :

// set the color and brush style
stroke(255, 120, 0, 255)
strokeWeight(2)
noFill()

Similar to how we created the beads() brush, we'll find the distance between the current and previous mouse points, which will become the arc's diameter, as well as the midpoint, which will become the arc's center point.

// find the distance between the current and previous mouse points
const distance = dist(mouseX, mouseY, pmouseX, pmouseY)

// find the midpoint between the current and previous mouse points
const midX = (mouseX + pmouseX) / 2
const midY = (mouseY + pmouseY) / 2

We also want to rotate each arc towards the direction of the mouse. To figure out how much to rotate the arc, we'll calculate the angle of the mouse direction with a bit of trigonometry, by finding the inverse tangent between the current and previous mouse points:

// find the angle of the direction the mouse is moving in
const angle = Math.atan2(mouseY - pmouseY, mouseX - pmouseX)

Before we draw anything, let's take a look the geometry of circles and arcs. We can define a circle as an angle of 360 degrees or 2π radians.

Since an arc is essentially a portion of a circle's circumference, we can define an arc as some angle in degrees or radians as well. A semicircle is half a circle, so it would be 180 degrees or π radians.

To define an arc in p5.js, we must choose a starting and ending angle in radians. Then, p5.js will create the arc by drawing it clockwise around a circle, starting and ending at the specified points. If we use the angle of the mouse direction as the semicircle's starting angle, then since a semicircle is π radians, it would end at angle plus π.

To create the wiggly line, we also need to flip each arc so that they alternate. We can flip any arc by adding π to its starting and ending angles, which will rotate the arc clockwise halfway around the circle. So, if we add π to every other arc, then we will have a wiggly line.

First, we'll figure out how we should flip the current arc by checking whether frameCount is an even or odd number using frameCount % 2 , then multiply that by PI. This would result in either 0 when frameCount is an even number or PI when frameCount is an odd number. Adding this to the arc's starting and ending angle will flip every other arc accordingly.

// find which way to flip the arc
const flip = (frameCount % 2) * PI

And finally, with all the parameters we've gathered, we'll draw an arc with the arc() function:

// draw the arc as a half circle
arc(midX, midY, distance, distance, angle + flip, angle + PI + flip)

Putting it all together, here's our final wiggle() paintbrush:

function wiggle() {
  // set the color and brush style
  stroke(255, 120, 0, 255)
  strokeWeight(2)
  noFill()

  // find the distance between the current and previous mouse points
  const distance = dist(mouseX, mouseY, pmouseX, pmouseY)

  // find the midpoint between the current and previous mouse points
  const midX = (mouseX + pmouseX) / 2
  const midY = (mouseY + pmouseY) / 2

  // find the angle of the direction the mouse is moving in
  const angle = Math.atan2(mouseY - pmouseY, mouseX - pmouseX)

  // find which way to flip the arc
  const flip = (frameCount % 2) * PI

  // draw the arc as a half circle 
  arc(midX, midY, distance, distance, angle + flip, angle + PI + flip)
}

Amplifying the tangents around the mouse path

We've drawn mostly curves and circles so far. To mix up the texture, let's explore sharper lines.

In this next paintbrush function, toothpick() , we'll create toothpick-like shapes that hug the curved path of the mouse. We'll create each toothpick as an ellipse with a very large width and a very small height, and we'll rotate them towards the direction of the mouse.

As you may predict, first we'll start by setting the color and brush style at the top of the function:

// set the color and brush style
fill(60, 180, 0, 150)
noStroke()

Before we start setting up the parameters of our ellipse, let's talk about transforming shapes relative to the canvas.

In our previous paintbrush, wiggle(), we drew each semicircle by rotating it in the direction of the mouse path. To draw each toothpick, instead of rotating just the shape itself, we'll first rotate the entire canvas around the current mouse point as the center of rotation. After transforming the whole canvas, anything we do subsequently in the draw() function will be relative to this transformation.

As our first transformation, we'll set the origin of the canvas to the mouse's current location using the translate() function. This means that the mouse's X and Y coordinates will become (0, 0), and the rest of the canvas coordinates will be relative to that point. By default, the origin is at the upper left corner of the browser window, unless we change it with a translation.

// move the origin (0,0) to the current mouse point
translate(mouseX, mouseY)

Then as our second transformation, we'll rotate the canvas by the angle of the mouse direction with the rotate() function. The center of rotation is our mouse, since we already set the mouse location as the canvas origin.

// find the angle of the direction the mouse is moving in
// then rotate the canvas by that angle
const angle = Math.atan2(mouseY - pmouseY, mouseX - pmouseX)
rotate(angle)

To create the toothpick shape, we'll draw an ellipse with a very long width and a very short height using the ellipse() function. This function takes in 4 parameters: the X and Y coordinates of its center point and its width and height.

The ellipse() function takes in 4 parameters: the X and Y coordinates of its center point and its width and height.

  • We'll set the ellipse width based on the distance between the current and previous mouse points, and extend it a little further by multiplying by 2.

  • We'll also set the minimum size of the ellipse to 4 pixels by adding minSize to the ellipse width and height.

  • Since we've already set the canvas origin and rotation based on the mouse path, we can simply draw the ellipse horizontally, where its center point's X and Y coordinates are (0, 0).

// set minumum width and height of the toothpick-shaped ellipse
const minSize = 4

// find the distance between current mouse point and previous mouse point
const distance = dist(mouseX, mouseY, pmouseX, pmouseY)

// draw the toothpick-shaped ellipse
ellipse(0, 0, distance * 2 + minSize, minSize)

Putting it all together, here's the code for our toothpick() paintbrush:

function toothpick() {
  // set the color and brush style
  fill(60, 180, 0, 150)
  noStroke()

  // move the origin (0,0) to the current mouse point
  translate(mouseX, mouseY)

  // find the angle of the direction the mouse is moving in
  // then rotate the canvas by that angle
  const angle = Math.atan2(mouseY - pmouseY, mouseX - pmouseX)
  rotate(angle)

  // set minumum width and height of the toothpick-shaped ellipse
	const minSize = 4
	
	// find the distance between current mouse point and previous mouse point
	const distance = dist(mouseX, mouseY, pmouseX, pmouseY)
	
	// draw the toothpick-shaped ellipse
	ellipse(0, 0, distance * 2 + minSize, minSize)
}

Simply by extending the width of the ellipse even further, we can create a totally different look and feel of this paintbrush function. Here, we've changed color and the ellipse's width multiplier from 2 to 18. What other variations can you create?

Filling in the gaps between points

Another way we can smoothly connect the points along the mouse path is by repeating a point, line or shape to fill in the gaps. We can achieve this with linear interpolation – often called lerping in shorthand – which is a fancy math term for finding a number in between two numbers.

Let's use lerping to create a paintbrush function that mimics a fountain pen:

First, we'll create a new fountainPen() function and set the brush style, as well as the width of the fountain pen line.

// set the color and brush style
stroke(0, 0, 0, 255)
strokeWeight(1)
const width = 5

Technically, we could stop here and draw a slanted line right away, resulting in the following fountainPen() function:

function fountainPen() {
  // set the color and brush style
	stroke(0, 0, 0, 255)
	strokeWeight(1)
	const width = 5

  // draw a slanted line 
  line(mouseX - width, mouseY - width, mouseX + width, mouseY + width)
}

However, since the draw() function can only run at a limited frequency, this function alone would leave large gaps in between each slanted line. See for yourself:

Instead of drawing a slanted line only once per call to draw(), we can repeat that slanted line multiple times with the lerp() function to close the gaps.

To do this, let's create a variable called lerps, which will become the number of times we repeat a for loop:

// set the number of times we repeat the line
const lerps = 16

// repeat the slanted line with lerping
for (let i = 0; i < lerps; i++) {
  
}

Next, we'll use the for loop to repeatedly draw the slanted line at regular intervals between the current and previous mouse points.

Inside the for loop, first we need to figure out where to draw each line by finding its lerped x and y coordinates with the lerp() function. Given any two numbers and some ratio between 0.0 to 1.0, lerp() will return a number between the 2 numbers at the given ratio.

  • To find the x coordinate, we'll lerp between mouseX and pmouseX , and to find the y coordinate, we'll lerp between mouseY and pmouseY.

  • To create even spacing between each line, we'll divide the loop index i by lerps, the total number of loop iterations, to get the lerping ratio i / lerps .

This is the resulting code for our for loop where we repeatedly draw the slanted line:

// set how many times to lerp the line 
const lerps = 16

// repeat the slanted line with lerping
for (let i = 0; i < lerps; i++) {
  const x = lerp(mouseX, pmouseX, i / lerps)
  const y = lerp(mouseY, pmouseY, i / lerps)

  line(x - width, y - width, x + width, y + width)
}

Finally, putting it all together, here's the code for our fountainPen() paintbrush:

function fountainPen() {
  // set the color and brush style
  stroke(0, 0, 0, 255)
  strokeWeight(1)
	const width = 5

  // set the number of times we lerp the line in the for loop
  const lerps = 16

	// repeat the slanted line with lerping
  for (let i = 0; i <= lerps - 1; i++) {

		// find the lerped x and y coordinates between the mouse points
    const x = lerp(mouseX, pmouseX, i / lerps)
    const y = lerp(mouseY, pmouseY, i / lerps)

		// draw a slanted line
    line(x - width, y - width, x + width, y + width)
  }
}

Lerping points to create a splatter effect

The fun thing about creative coding is that sometimes silly mistakes turn into spontaneous surprises! While creating the fountainPen() paintbrush, I accidentally made a mistake with the ratio in the lerp() function. Lo and behold, this splatter() paintbrush was born:

The splatter() and fountainPen() functions are quite similar in code. So instead of walking you through how to code the splatter() paintbrush function step-by-step, I'll briefly outline the key differences:

  • The third parameter to lerp() , which is the lerp ratio, is i / lerps + lerps . This not only spaces each lerped point apart from each other at regular intervals with i / lerps, but adding lerps also slightly shifts the point by the number of pixels.

  • Instead of drawing a line, we draw a point at the lerped X and Y coordinates with the point() function.

  • We also use a few minor stylistic differences in color, stroke weight and number of loop iterations in the lerps variable (8 lerps instead of 16).

Putting it all together, here is the code for the splatter() paintbrush function:

function splatter() {
  // set the color and brush style
  stroke(frameCount % 255, 180, 255, 160)
  strokeWeight(4)

  // set the number of times we lerp the point in the for loop
  const lerps = 8

	// repeat the point with lerping
  for (let i = 0; i < lerps; i++) {

		// find lerped x and y coordinates of the point
    const x = lerp(mouseX, pmouseX, i / lerps + lerps)
    const y = lerp(mouseY, pmouseY, i / lerps + lerps)

		// draw a point
    point(x, y)
  }
}

Hatching

Let's explore this lerping technique even further. Instead of repeating the same slanted line as in the fountainPen() brush, how can we create sketchier and messier strokes? This next paintbrush function echoes a traditional sketching technique called hatching, which is used to darken areas of an illustration by drawing parallel lines.

Let's create a new paintbrush function called hatching(). At the top, we'll set the brush style and also calculate the speed of the mouse movement.

// set the color and brush style
stroke(15, 15, 255, 220)
strokeWeight(2)

// calculate the speed of the mouse
let speed = abs(mouseX - pmouseX) + abs(mouseY - pmouseY)

So far in this guide, we've drawn shapes and lines based on the relationship between the current mouse point (mouseX and mouseY) and the previous mouse point (pmouseX and pmouseY). One way we can more easily manipulate this relationship is with a vector, which is an object that has both a direction and magnitude. You can think of a vector as an arrow of some length that points towards some direction.

To create a vector, we can pass in an X value and a Y value into the createVector() function. This will create a p5.Vector object with the following properties:

  • The vector direction is the direction from the origin (0, 0) to the point at X and Y. In p5.js, this is an angle in radians.

  • The vector magnitude is the distance between the origin and the point at X and Y.

For example, if we create a vector with the current mouse point and another vector with the previous mouse point, the below diagram shows how we can visualize them:

However, instead of creating vectors out of the current and previous mouse points, let's combine them: we'll find the Y value by subtracting pmouseX from mouseX, and the X value by subtracting pmouseY from mouseY.

// make a vector by inverting X and Y values
const vector = createVector(mouseY - pmouseY, mouseX - pmouseX)

Swapping the X and Y values inverts the direction of our resulting vector. To get a better sense of how this swap results in each line of the brush stroke, try hovering your mouse over the interactive diagram below and observe how the orientation of the line changes:

Next, we'll set the vector magnitude to depend on the speed of the mouse with the setMag() vector function, which is one of many p5.Vector methods that can be used to manipulate vectors. This determines the length of our line.

// set the vector magnitude (the line length) based on the mouse speed
vector.setMag(speed / 2)

Technically, this entire hatching() function could be created without using any vectors. However, having access to vector functions like setMag() enables us to quickly and conveniently manipulate the relationships between points, lines, and shapes like this with very little code, which spares us from doing the extra math ourselves. You can check out the p5.Vector object documentation to see the full list of p5.js vector functions.

And finally, just as we lerped the lines of the fountainPen() paintbrush, we'll do the same with hatching() to make our brush strokes smoother and denser:

// set the number of times we lerp the line
const lerps = 3

// repeat the line with lerping
for (let i = 0; i < lerps; i++) {

	// find the lerped X and Y coordinates
  const x = lerp(mouseX, pmouseX, i / lerps)
  const y = lerp(mouseY, pmouseY, i / lerps)

	// draw a line
  line(x - vector.x, y - vector.y, x + vector.x, y + vector.y)
}

Putting it all together, here's the code for our hatching() paintbrush:

function hatching() {
  // set the color and brush style
  stroke(15, 15, 255, 220)
  strokeWeight(1)

  // calculate the speed of the mouse
  let speed = abs(mouseX - pmouseX) + abs(mouseY - pmouseY)

  // make a vector by inverting X and Y values
  const vector = createVector(mouseY - pmouseY, mouseX - pmouseX)
  
  // set the vector magnitude (the line length) based on the mouse speed
  vector.setMag(speed / 2)
  
  // set the number of times we lerp the line
  const lerps = 3

  // repeat the line with lerping
  for (let i = 0; i < lerps; i++) {

		// find the lerped X and Y coordinates
    const x = lerp(mouseX, pmouseX, i / lerps)
    const y = lerp(mouseY, pmouseY, i / lerps)

		// draw a line
    line(x - vector.x, y - vector.y, x + vector.x, y + vector.y)
  }
}

Spray painting with randomness

As our final paintbrush in this guide, we'll mimic the look and feel of painting with a can of sprayPaint(), and introduce randomness into the process. Before we start building it, try it out yourself:

First, we'll create a new sprayPaint() function. Then at the top, we'll set the brush styles:

// set the color and brush style
stroke(0, 0, 0, 255)
strokeWeight(1)

Each call to this paintbrush function will draw a bunch of random points within a circle, which will get bigger as the mouse moves faster.

To define the dimensions, we'll set up some initial variables:

  • minRadius as the minimum radius of the brush stroke, even when the mouse speed is 0

  • sprayDensity as the number of random points to draw within the circle

  • speed of the mouse

  • r as the radius of the circle, which is the minRadius at its smallest size, and increases with the speed of the mouse

  • rSquared, which we'll later use to calculate the positions of the random points

// set minimum radius and spray density of spray paint brush
const minRadius = 10
const sprayDensity = 80

// find the speed of the mouse movement
const speed = abs(mouseX - pmouseX) + abs(mouseY - pmouseY)

// find the radius of the spray paint brush, and also the radius squared
const r = speed + minRadius
const rSquared = r * r

We can draw the random points by first creating a for loop, which runs as many times as the sprayDensity . You can create darker, denser paint strokes by increasing the sprayDensity.

// draw random points repeatedly within the circle with radius r
for (let j = 0; j < sprayDensity; j++) {

}

Each time we run through this for loop, we'll draw a random point within the circle by using the random() function. The random() function takes in two numbers – a minimum and maximum number – and returns a random number within that range.

In order to make sure that each random point falls inside the circle, we'll need to use a bit of math. So, let's break down the geometry of circles!

We can define any circle with the equation r^2 = x^2 + y^2, where r is the radius, and x and y are any point on the circumference of the circle. In case this equation looks familiar to you, this circle formula is based on the Pythagorean theorem. If we isolate the y variable on one side of the equals sign, we get y = sqrt(r^2 - x^2).

So, let's say we have a circle with some radius r . Our goal is to find the x and y coordinates of some random point within that circle.

  • If we choose any x that is between -r and r, then plugging those x and r values into the circle equation will give us some y that equals sqrt(r^2 - x^2) . These two x and y coordinates together would make a point that sits on the circumference of the circle.

  • Then, as long as we choose some x that is between -r and r, and also some y that is between -sqrt(r^2 - x^2) and sqrt(r^2 - x^2), then the random point will fall somewhere inside the circle.

Using these rules, we can now use the random() function to draw a bunch of random points within the circle:

// draw random points within a circle
for (let j = 0; j < sprayDensity; j++) {

  // pick a random position within the circle
  const randX = random(-r, r)
  const randY = random(-1, 1) * sqrt(rSquared - randX * randX)

  // draw the random point
  point(mouseX + randX, mouseY + randY)
}

As the final step, to create even denser and smoother spray paint strokes, we'll add lerping. We can do this by nesting our first for loop inside of another for loop.

  • In the outer for loop, we'll calculate the lerped X and Y coordinates with the lerp() function, in the same way we used lerping to create the previous three paintbrush functions.

  • In the inner for loop, we'll draw the random points by combining the lerped X and Y with the random X and Y coordinates.

// set the number of times we lerp the points in the for loop
const lerps = 10

// repeat the random points with lerping
for (let i = 0; i < lerps; i++) {
  
  // find the lerped X and Y coordinates
  const lerpX = lerp(mouseX, pmouseX, i / lerps)
  const lerpY = lerp(mouseY, pmouseY, i / lerps)
  
  // draw random points within a circle
  for (let j = 0; j < sprayDensity; j++) {

    // pick a random position within the circle
    const randX = random(-r, r)
    const randY = random(-1, 1) * sqrt(rSquared - randX * randX)

    // draw the random point
    point(lerpX + randX, lerpY + randY)
  }
}

And finally, putting it all together, here's our sprayPaint() function:

function sprayPaint() {
	// set the color and brush style
  stroke(0, 0, 0, 255)
  strokeWeight(1)

	// find the speed of the mouse movement
  const speed = abs(mouseX - pmouseX) + abs(mouseY - pmouseY)

	// set minimum radius and spray density of spraypaint brush
	const minRadius = 10
	const sprayDensity = 80
  
  // find radius of the spray paint brush and radius squared
  const r = speed + minRadius
  const rSquared = r * r

	// set the number of times we lerp the points in the for loop
	const lerps = 10

  // repeat the random points with lerping
  for (let i = 0; i < lerps; i++) {
    
    // find the lerped X and Y coordinates
    const lerpX = lerp(mouseX, pmouseX, i / lerps)
    const lerpY = lerp(mouseY, pmouseY, i / lerps)
    
    // draw a bunch of random points within a circle
    for (let j = 0; j < sprayDensity; j++) {

      // pick a random position within the circle
      const randX = random(-r, r)
      const randY = random(-1, 1) * sqrt(rSquared - randX * randX)

      // draw the random point
      point(lerpX + randX, lerpY + randY)
    }
  }
}

Conclusion

At this point, you've created 10 distinct paintbrushes! But this is just a starting point – there are endless ways you can mix and match the techniques used throughout this guide to create your own unique painting tools.

As you keep going, do explore the p5.js library documentation and see the breadth of p5.js functions and creative coding tools you have at your disposal. I'll also include some links to more p5.js learning resources below, as well as a compilation of the mathy code snippets that we used throughout this guide for your future reference.

In the next guide of this Painting with Code series, we'll combine all of these paintbrushes into a single painting application, so that you can paint with all of your brushes on a single canvas. And in the process, we'll learn how to prototype user interfaces for painting applications with p5.js, so that you can customize the entire painting experience within the browser.

Have fun with it and we'd love to see what you create!

Additional learning resources

Code snippets

Throughout this guide, we used a variety of mini math techniques to stylize our paintbrushes. To make it easy for you to experiment with these techniques and create your own brushes, this is a compilation of these code snippets for your future reference.

Speed of the mouse movement

// find the speed of the mouse movement
let speed = abs(mouseX - pmouseX) + abs(mouseY - pmouseY)

Distance between current and previous mouse points

// find the distance between the current and previous mouse points
const distance = dist(mouseX, mouseY, pmouseX, pmouseY)

Midpoint between current and previous mouse points

// find the midpoint between the current and previous mouse points
const midX = (mouseX + pmouseX) / 2
const midY = (mouseY + pmouseY) / 2

Angle of mouse direction

// find the angle of the direction the mouse is moving in 
const angle = Math.atan2(mouseY-pmouseY, mouseX-pmouseX)

Looping between x number of states in each call to draw()

const state = frameCount % x

Filling in the gaps with lerp() inside a for loop

	// set the number of times we lerp the point
  const lerps = 16

  // repeat the point with lerping 
  for (let i = 0; i <= lerps; i++) {

		// find x and y coordinates of the point
    const x = lerp(mouseX, pmouseX, i / lerps)
    const y = lerp(mouseY, pmouseY, i / lerps)

		// draw the point
    point(x - width, y - width, x + width, y + width)
  }

Picking a random position inside of a circle with radius r

// set radius of the circle and calculate the radius squared 
const r = 10
const rSquared = r * r

// pick a random position within the circle
const randX = random(-r, r)
const randY = random(-1, 1) * sqrt(rSquared - randX * randX)

Share this post
About the author

Arianna Ninh is an Educator at SuperHi, where she supports students in embracing the messy magic of learning something new. She hails from the San Francisco Bay Area and loves hiking outdoors in the SF fog (aka Karl). Outside of SuperHi, you can find her attempting to make food or weird sculptures, or daydreaming in a park somewhere.

Published

June 27, 2021

Author

Arianna Ninh

Illustrator

Arianna Ninh

Related posts

INTERVIEW

Catching Up With... Kelsey Gilbert-Kreiling

ADVICE

Ask a Designer #17: How do I communicate design decisions?

ARTICLE

How to Land Your First (or Next) Remote Job

Want more? Sign up for our newsletter for more articles, resources, and fresh inspiration!