Canvas with P5
Setup
The easiest way to get set up is using the p5 cdn, but if you want intellisense and other features, you can use the npm package.
NPM
npm i p5
npm i -D @types/p5
Then you can setup the animation loop and canvas like so:
import p5 from "p5";
const p5Manager = new p5((p: p5) => {
// 1. runs first before setup(), use to download files
p.preload = () => {};
// 2. runs once before draw(), use to initialize variables and setup canvas
p.setup = () => {
p.createCanvas(400, 400);
};
// 3. draw() runs each frame, use to update variables and draw to canvas
p.draw = () => {
p.background(220);
};
});
There are three special functions that p5 understands and automatically creates a game loop from that. In these methods, do the canvas setup and call other p5 specific methods to draw to the canvas.
preload()
: runs first beforesetup()
, use to download filessetup()
: runs once beforedraw()
, use to initialize variables and setup canvasdraw()
: runs each frame, use to update variables and draw to canvas. The code inside this function runs each frame.
CDN
Use the p5 CDN:
<script
src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.8.0/p5.min.js"
integrity="sha512-pgK6Wo8doipc/IPQ0ilH3b47ww01345nR9ud1/6Qp0n+lQlEW9zuw6JhysRcUpBY4yKuVZjn1MAkDxbnncuGsQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
All p5 methods and variables are now added to the global scope. You get no intellisense, but development is easier. Here is how the setup looks like:
function setup() {
// create canvas, initialize variables
createCanvas(400, 400);
}
function draw() {
// animation loop code
background(220);
}
P5 special methods
P5 has special methods that when implemented, p5 will recognize them immediately as a part of the framework. For example, any function you name draw()
will draw to the canvas every frame. No need for an animation loop.
If you're using p5 with npm, you don't have to describe these functions globally but rather on the p5 instance.
Basic special methods
preload()
: runs first beforesetup()
, use to download filessetup()
: runs once beforedraw()
, use to initialize variables and setup canvasdraw()
: runs each frame, use to update variables and draw to canvas. The code inside this function runs each frame.
Event methods
These are methods that p5 uses to hijack the original DOM events listeners. You can insert your own callback and run code to execute when these events are triggered:
mousePressed(() => {})
: runs when the mouse is pressed, when the user clicksmouseReleased(() => {})
: runs when the mouse is releasedmouseDragged(() => {})
: runs when the mouse is dragged, when the user holds down the mouse and drags across the screen.
P5 methods and properties
In our npm setup, these are all the things we have available on the p
variable:
Properties
These are builtin properties that you can access on the p5
instance.
Canvas properties:
width
: the canvas widthheight
: the canvas height
Dom related properties:
mouseIsPressed
: boolean, true if the user is holding down the click buttonmouseX
: the x coordinate of the mousemouseY
: the y coordinate of the mousepmouseX
: the previous x coordinate of the mouse (the last frame)pmouseY
: the previous y coordinate of the mouse (the last frame)
Basic methods
background(r, g, b, a?)
: paints the canvas with the given color, based on rgba. If only one argument is given, it is the grayscale value.createCanvas(width, height)
: creates a canvas of the given width and height.fill(r, g, b, a?)
: sets the canvas fill style, based on rgb. If only one argument is given, it is the grayscale value.stroke(r, g, b, a?)
: sets the canvas stroke style, based on rgb. If only one argument is given, it is the grayscale value.translate(x, y)
: moves the origin of the canvas to the given x and y coordinates.noStroke()
: removes the stroke from the canvas by setting strokeStyle to none.strokeWeight(n)
: sets the stroke width to the specified numberframeRate(n)
: sets the fps of the animation loop. Paired nicely with using random elements in your code
Creating shapes
circle(x, y, diameter)
: draws an ellipse at the given x and y coordinates, with the given diameterellipse(x, y, width, height)
: draws an ellipse at the given x and y coordinates, with the given width and heightline(x1, y1, x2, y2)
: draws a line from the first x and y coordinates to the second x and y coordinatesrect(x, y, width, height)
: draws a rectangle at the given x and y coordinates, with the given width and height- Any additional arguments you pass after that relate to the border radius values for the top left, top right, bottom left, and bottom right corners respectively
point(x, y)
: draws a point at the given x and y coordinates. Make sure to change the stroke style and width for this to ensure its visibilitypoint(Vector)
: draws a point at the given vector's x and y coordinates.
square(x, y, size)
: draws a square at the given x and y coordinates, with the given size.- Any additional arguments you pass after that relate to the border radius values for the top left, top right, bottom left, and bottom right corners respectively
triangle(x1, y1, x2, y2, x3, y3)
: draws a triangle with the given x and y coordinates for each point
Images
Text
textFont(fontName)
: sets the font family for text operationstextSize(size)
: sets the font size for text operationstext(text, x, y)
: draws text to the canvas. First argument is the text string, then the second is the x coordinate, and the third is the y coordinate.
p.strokeWeight(1);
p.fill(0);
p.textFont("Arial");
p.textSize(16);
p.text("1", p.width - 15, 15);
p.text("2", p.width - 35, 15);
p.text("4", p.width - 55, 15);
Math methods
random(n)
: returns a random number between 0 and nrandom(min, max)
: returns a random number between min and max
Using map
The map()
method is a way of using semi-reactive programming where we define a range a value can be, and preferred values for when the variable is at the beginning or end of that range.
function update() {
this.position.add(this.velocity);
// 1. establish a range for the y coordinate between 0 and the canvas height.
// 2. when y-coordinate is 0, return 5. when y-coordinate is canvas height, return 20
// 3. by mapping the particle size to the y position, particles grow larger as they move down the screen
// the size will be between 5 and 20
this.size = this.p.map(this.position.y, 0, this.p.height, 5, 20);
// first argument: clamp this.position.y between a range
// second argument: establish minimum value for the range of this.position.y: 0
// third argument: establish maximum value for the range of this.position.y: canvas height
// fourth argument: establish minimum value for the return value: 5
// fifth argument: establish maximum value for the return value: 20
}
map(value, start1, stop1, valWhenStart, valWhenStop)
: maps a value from one range to another range, returning a number in between that clamped range. Let's do an example withmap(star.z, 0, 400, 1, 10)
- The first argument is the value to pass in that want to clamp.
star.z
is a number value, and incrementing or decrementing this value spits out a new number from the range. - The second and third arguments are the range of the value.
0
is the minimum value, and400
is the maximum value.star.z
will range from 0-400 - The fourth and fifth arguments are the range to clamp the value to.
1
is the minimum value, and10
is the maximum value. The returned value will be between 1-10, and will be clamped to that range. Whenstar.z == 0
, 1 will be returned. Whenstar.z == 400
, 10 will be returned. Then it's a linear interpolation for any values between 0-400.
- The first argument is the value to pass in that want to clamp.
Sliders
Using the createSlider()
method, you can add an input range to the DOM and automatically have an event listener binded to it, enabling reactive programming.
let slider = p.createSlider(min, max, default, step);
The 4 arguments passed are the min
, max
, default
value, and the step
, which are all self-explanatory. The slider
variable is an instance of the p5.Element
class, which has the following useful properties and methods:
element.value()
: returns the current value of the elementelement.mouseReleased()
: callback function that runs when the mouse is released on the element. You can do this as a hacky way of lsitening for when the user moves the sliderelement.id(id)
: sets the id of the element to the passed-in id
Here is an example in action:
import p5 from "p5";
import Particle from "./Particle";
let particlesArray: Particle[] = [];
let sizeSlider: p5.Element;
let speedSlider: p5.Element;
const p5Manager = new p5((p: p5) => {
p.setup = () => {
p.createCanvas(400, 400);
// create slider for size. Range between 10-30, with default value of 10, and step of 1
sizeSlider = p.createSlider(10, 30, 10, 1);
// create slider for speed. Range between 1-10, with default value of 2, and step of 1
speedSlider = p.createSlider(1, 10, 2, 1);
particlesArray = Array.from({ length: 100 }, () => new Particle(p));
// user selected value on input and released mouse
speedSlider.mouseReleased(() => {
particlesArray.forEach((particle) => {
particle.setVelocity(Number(speedSlider.value()));
});
});
};
p.draw = () => {
p.background(220, 124, 156, 100);
particlesArray.forEach((particle, index) => {
particle.update();
particle.draw();
particle.setSize(Number(sizeSlider.value()));
});
};
});
Working with vectors
The createVector(x, y)
method creates a vector, which represents an x and y point. This returns a Vector
instance which has the following methods:
add(vector)
: adds the given vector to the current vectorsub(vector)
: subtracts the given vector from the current vectormult(n)
: multiplies the vector by the given numberdiv(n)
: divides the vector by the given numbermag()
: returns the magnitude of the vector
You can also add as many dimensions as you want, doing like createVector(x, y, z)
You can access individual components of the vector through the x
, y
, and z
(if you add a 3rd dimension) properties.
Here is an example of using vectors:
import p5, { Vector } from "p5";
export default class Particle {
private position: Vector; // particle (x, y)
private velocity: Vector; // particle dx and dy
private size: number; // particle size
constructor(private p: p5) {
// random position and velocity
this.position = p.createVector(p.random(p.width), p.random(p.height));
this.velocity = p.createVector(p.random(-2, 2), p.random(-2, 2));
this.size = 10;
}
update() {
this.detectEdges();
// update position by adding velocity vector
this.position.add(this.velocity);
}
draw() {
this.p.noStroke();
this.p.fill(255, 0, 0);
this.p.circle(this.position.x, this.position.y, this.size);
}
// bounce off the edges of the canvas
private detectEdges() {
if (this.position.x < 0 || this.position.x + this.size > this.p.width) {
// flip dx
this.velocity.x *= -1;
}
if (this.position.y < 0 || this.position.y + this.size > this.p.height) {
// flip dy
this.velocity.y *= -1;
}
}
}
Saving colors for later
If you want to save fill styles and stroke styles as variables, you can do that with the color(r, g, b)
builtin method, which returns a canvas style. You can then store it as a variable and pass it to when you want to set the fill or stroke style.
You just pass a color object to a style as fill(color)
const brush = p.color(255, 0, 0);
p.fill(brush);
Canvas State
There are multiple p5 methods we can use to control the state of the canvas, including rotating it, translating the origin, and more.
translate(x, y)
: translates the canvas's origin to the following x and y coordinates.- You could basically turn this into a normal cartesian plane by translating to
translate(width / 2, height / 2)
.
- You could basically turn this into a normal cartesian plane by translating to
rotate(degrees)
: rotates the canvas around the origin by the specified number of degrees/radians. Useful for radial
These other methods help for making some of the canvas state changes easier:
angleMode(DEGREES)
: turns on degree mode for rotation operations, so you can use degrees instead of radians.rectMode(CENTER)
: makes it so that rectangles are built around their center rather than their top-left cornerrectMode(CORNER)
: makes it so that rectangles are built around their top-left corner, which is the default behavior.
push and pop
The push()
and pop()
methods are used to temporarily alter the canvas state, just like how ctx.save()
and ctx.restore()
works.
Let's say you're rotating the canvas around, but you want it to reset to normal without explicitly saying so. Then you would stack all canvas state-changing methods around inside push()
and pop()
, like so:
push()
translate(width / 2, height / 2)
angleMode(DEGREES)
rotate(45)
// draw stuff here
pop()
// canvas state is restored back to normal
Working with images
Now we have a use case for the special preload()
function: to load images. We can load either local or network images and then draw them to the canvas. The general flow is like this:
- In the
preload()
callback, useloadImage(src)
method calls to load the image resources the canvas needs. - Use the
image()
method to draw the image to the canvas.
Here are useful methods for images:
loadImage(src)
: loads the specified image src. This returns ap5.Image
instance. Only run this in the preload function.image(image, x, y, width, height)
: draws the specified image to the canvas. The first argument should be ap5.Image
type, so it should be the value you get back fromloadImage()
.
You can get the individual pixel color value of a pixel from an image using the image.get(x, y)
method, which returns an array of 4 values corresponding to an rgba syntax.
import p5 from "p5";
let catImage: p5.Image;
export const imagePractice = new p5((p: p5) => {
p.preload = () => {
// make sure image is same size as canvas or less
catImage = p.loadImage("https://placekitten.com/400/400");
};
p.setup = () => {
p.createCanvas(400, 400);
};
p.draw = () => {
p.background(220);
p.image(catImage, 0, 0, p.width, p.height);
// get the [r, g, b, a] value of the first pixel in the image
const firstPixelColor = p.color(catImage.get(0, 0));
};
});
P5 project examples
Paint brush app
For this paint brush app, we forego drawing the canvas background each frame so our drawings can stay on the screen. We draw a blue square and a red square, and when they are clicked, we want to change the stroke color.
- Click on blue, change stroke color to blue
- Click on red, change stroke color to red.
Targetable
To detect a click on a canvas element, these are the basic steps to follow:
- Write all click-detection code in the
mousePressed()
callback, which is triggered when the user clicks on the canvas. - See if the
mouseX
andmouseY
are within the bounding box of the canvas element you want to see was clicked on. If so, then the canvas element was clicked.
By defining the coordinate state of a canvas element ahead of time, I made an easier API for dealing with click detection.
import p5 from "p5";
export default class Targetable {
constructor(
private p: p5,
private x: number,
private y: number,
private width: number,
private height: number
) {}
// if user clicks on canvas and the mouseX mouseY is in obj coords, execute callback
onClick(callback: () => void) {
if (!this.p.mouseIsPressed) return;
if (this.checkIfPointInObject(this.p.mouseX, this.p.mouseY)) {
callback();
}
}
private checkIfPointInObject(x: number, y: number) {
return (
x >= this.x &&
x <= this.x + this.width &&
y >= this.y &&
y <= this.y + this.height
);
}
}
Final code
Here is the code for that:
import p5 from "p5";
import Targetable from "./Targetable";
let red: p5.Color;
let blue: p5.Color;
let redBox: Targetable;
let blueBox: Targetable;
let strokeColor: p5.Color;
const p5Manager = new p5((p: p5) => {
p.setup = () => {
// setup colors, background color, etc.
p.createCanvas(400, 400);
red = p.color(255, 0, 0);
blue = p.color(0, 0, 255);
strokeColor = p.color(0, 0, 0);
p.background(220);
// define the bounding box for our buttons
redBox = new Targetable(p, 0, 0, 20, 20);
blueBox = new Targetable(p, 20, 0, 20, 20);
};
p.draw = () => {
p.noStroke();
// draw red and blue squares
p.fill(red);
p.rect(0, 0, 20, 20);
p.fill(blue);
p.rect(20, 0, 20, 20);
};
// when user holds down mouse and moves around screen
p.mouseDragged = () => {
p.stroke(strokeColor);
p.strokeWeight(5);
// draw a line
p.line(p.mouseX, p.mouseY, p.pmouseX, p.pmouseY);
};
// when user click the mouse
p.mousePressed = () => {
// can only set onClick listeners inside this method
redBox.onClick(() => {
// set brush color to red on click
strokeColor = red;
});
blueBox.onClick(() => {
// set brush color to blue on click
strokeColor = blue;
});
};
});
Symmetrical paint brush app
Canvas rotation for radial patterns
10 print + Grid
Basic Grid
function drawGrid(p: p5) {
for (let x = 25; x < p.width; x += 50) {
for (let y = 50; y < p.height; y += 50) {
const size = p.random(10, 30);
p.ellipse(x, y, size);
}
}
}
10 print code
Pointism and images and Linewalk
Pointism
Pointism is a mosaic-like effect where we draw the image with individual circles, using the fill color from each image pixel to resemble the image.
Note that this will only work when we make the image the exact same size as the canvas so their pixel coordinates correspond with each other.
Here are the basic pointism steps done in a draw loop:
- Get a random point on the canvas plane
- Get the pixel color of the image at that random x and y
- Draw a circle and set the fill style to that pixel color
p.draw = () => {
// p.image(catImage, 0, 0, p.width, p.height)
lineWalk();
};
function pointilism() {
// for loop makes operation faster
for (let i = 0; i < 25; i++) {
// get a random point in the canvas, get the pixel color of the point's pixel
const randomX = p.random(p.width);
const randomY = p.random(p.height);
const pixelColor = p.color(catImage.get(randomX, randomY));
// drawing the circle with the image pixel color reveals the image
p.fill(pixelColor);
// stroke adds a mosaic effect
p.stroke(0);
p.strokeWeight(0.3);
p.circle(randomX, randomY, 4);
}
}
Line walk
Line walk is a technique where we draw random connected lines of a small length, where that line has the fill color of a pixel in the image in roughly the same location, for a drawing the image kind of effect.
- Set initial random coordinates to start the walk from
let initX = 0;
let initY = 0;
const stepSize = 10;
- Get the associated image pixel color from those x and y coordinates
const pixelColor = p.color(catImage.get(initX, initY));
- Select another random point a certain small number of units away from the first point, like 10 units away.
const randomX = p.random(-stepSize, stepSize);
const randomY = p.random(-stepSize, stepSize);
- Draw a line with the stroke style set to the image pixel color
p.stroke(pixelColor);
p.strokeWeight(2);
p.line(initX, initY, initX + randomX, initY + randomY);
- Increment the initial random coordinates by the 2nd pair of random coordinates. This makes a "walk" effect
initX += randomX;
initY += randomY;
- If the coordinates end up being out of bounds, reassign them to make sure they get back in the canvas somewhere.
if (initX < 0 || initX > p.width || initY < 0 || initY > p.height) {
initX = p.random(p.width);
initY = p.random(p.height);
}
Here is the complete code:
const stepSize = 10;
let initX = 0;
let initY = 0;
function lineWalk() {
for (let i = 0; i < 25; i++) {
// if step out of bounds, reset to somewhere random
if (initX < 0 || initX > p.width || initY < 0 || initY > p.height) {
initX = p.random(p.width);
initY = p.random(p.height);
}
// radius of randomness is 10. Line width will be 10 in any direction
const randomX = p.random(-stepSize, stepSize);
const randomY = p.random(-stepSize, stepSize);
const pixelColor = p.color(catImage.get(initX, initY));
p.stroke(pixelColor);
p.strokeWeight(2);
// draw line of length 10 with image pixel color from initX, initY
p.line(initX, initY, initX + randomX, initY + randomY);
// incrementing makes it a walk
initX += randomX;
initY += randomY;
}
}