Building a Plexus
In this post we’ll look at how to build a Plexus in JavaScript using the p5.js library.
What we are Making
Our goal is to build something that looks like this:
How we will make it
- Generate random points all over the screen.
- Draw lines between points that are close to each other. Closer points will have a brighter coloured line.
- Move the points slightly each frame.
- Repeat.
Setup p5.js
We’ll start off by setting up our p5.js project. Boilerplate code for p5.js can be found here on my GitHub. Some custom functions are included in this boilerplate to deal with the inverted y-axis when using browsers. These functions make the coordinate (0,0) draw at the bottom-left instead of the top-left.
We’ll only need to modify the script.js
file inside the boilerplate code.
Points
Generating Points
Our first step will be to generate a bunch of random points on the screen that will slowly move in a random direction.
Let’s start off by creating a Point
class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Point {
constructor(backgroundSizeX, backgroundSizeY, moveSpeed, pointSize) {
// Store properties
this.moveSpeed = moveSpeed;
this.pointSize = pointSize;
this.backgroundSize = { x: backgroundSizeX, y: backgroundSizeY };
// Generate random starting position
this.position = {
x: Math.floor(Math.random() * (this.backgroundSize.x - 1)),
y: Math.floor(Math.random() * (this.backgroundSize.y - 1))
};
// Generate random starting direction
let xDir = Math.random()*2 - 1;
let yDir = Math.random()*2 - 1;
// Convert to a unit vector
let length = Math.sqrt(xDir**2 + yDir**2);
this.direction = { x: xDir/length, y: yDir/length };
}
}
In the setup
function of p5.js we’ll now create a bunch of instances of this class. We’ll also need to create an array to store the points, as well as some variables for MOVE_SPEED
, POINT_SIZE
, and TOTAL_POINTS
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const MOVE_SPEED = 0.2;
const POINT_SIZE = 5;
let TOTAL_POINTS = 300;
let SCREEN_WIDTH = 0;
let SCREEN_HEIGHT = 0;
let POINT_LIST = [];
// Initial Setup
function setup() {
SCREEN_WIDTH = window.innerWidth - 20;
SCREEN_HEIGHT = window.innerHeight - 20
createCanvas(SCREEN_WIDTH, SCREEN_HEIGHT);
// Create random Points all over the screen
for (let i = 0; i < TOTAL_POINTS; i++) {
POINT_LIST.push(new Point(SCREEN_WIDTH, SCREEN_HEIGHT, MOVE_SPEED, POINT_SIZE));
}
frameRate(30);
}
Drawing Points
Now that we have a list of Point objects, we can draw them to the screen. We’ll do this by adding some code to the draw
function that loops over all the points and draws them every frame:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// To be called each frame
function draw() {
// Draw background & set Rectangle draw mode
background(255);
rectMode(CENTER);
// Draw scene rectangle
fill(30,30,30);
stroke(30,30,30);
drawRect(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT)
// Draw Points
POINT_LIST.forEach(currentPoint => {
drawPoint(currentPoint);
});
}
const drawPoint = (pointObject) => {
stroke(200,200,200);
fill(200,200,200);
drawCircle(pointObject.position.x, pointObject.position.y, pointObject.pointSize);
}
If we open the index.html
file, we should see the following:
Lines
Draw Lines to Nearby Points
We want our points to have lines drawn to other nearby points.
At the top of our code let’s add the following constant:
1
const DISTANCE_TO_START_DRAWING_LINES = 150;
- 2 Points that are further away than this will not have a line between them.
- NOTE: you may want to adjust this value based on your screen size.
For each point we need to find all the other points within this radius and draw a line to them.
We’ll add a function called drawLineToNearby
which will do this for us. This function will be called every frame before each point is drawn.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function draw() {
// Draw background & set Rectangle draw mode
background(255);
rectMode(CENTER);
// Draw scene rectangle
fill(30,30,30);
stroke(30,30,30);
drawRect(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT)
// Draw Lines to Nearby Points
POINT_LIST.forEach(currentPoint => {
drawLineToNearby(currentPoint);
});
// Draw Points
POINT_LIST.forEach(currentPoint => {
drawPoint(currentPoint);
});
}
const drawLineToNearby = (targetPoint) => {
stroke(200,200,200);
POINT_LIST.forEach(point => {
let distanceBetween = distanceBetweenPoints(targetPoint, point);
// Only draw the line if it's close enough
if (distanceBetween < DISTANCE_TO_START_DRAWING_LINES) {
drawLine(targetPoint.position.x, targetPoint.position.y, point.position.x, point.position.y);
}
})
}
// Calculate Euclidean distance between 2 points
const distanceBetweenPoints = (pointA, pointB) => {
return Math.sqrt(Math.pow(pointA.position.x - pointB.position.x, 2) + Math.pow(pointA.position.y - pointB.position.y, 2));
}
Here’s what we have so far:
Fading Line Colour
This is looking better, but we want the lines that connect points that are closer together to be brighter.
We’ll do this by adjusting the Alpha for each line based off the distance. Far away points will be closer to Alpha 0 and nearby points will be closer to Alpha 255.
We can achieve this using the map
function in p5.js. This function takes 5 arguments and maps an input from one range to another range:
1
map(50, 0, 100, 1, 6) // returns 3.5
- 50 is halfway between 0 and 100. The output will be the number halfway between 1 and 6, which is 3.5.
Here’s the code to implement this in our Plexus:
1
2
3
4
5
6
7
8
9
10
11
12
13
const drawLineToNearby = (targetPoint) => {
POINT_LIST.forEach(point => {
let distanceBetween = distanceBetweenPoints(targetPoint, point);
// Only draw the line if it's close enough
if (distanceBetween < DISTANCE_TO_START_DRAWING_LINES) {
let alpha = map(distanceBetween, 0, DISTANCE_TO_START_DRAWING_LINES, 255, 0);
stroke(200, 200, 200, alpha);
drawLine(targetPoint.position.x, targetPoint.position.y, point.position.x, point.position.y);
}
})
}
What we have currently:
Moving Points
As a final touch let’s make the points move around the screen slowly.
We’ll do this by adding an update
function to our Point
class that will be called every frame. This will change the points position over time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Point {
constructor(backgroundSizeX, backgroundSizeY, moveSpeed, pointSize) {
// Store properties
this.moveSpeed = moveSpeed;
this.pointSize = pointSize;
this.backgroundSize = { x: backgroundSizeX, y: backgroundSizeY };
// Generate random starting position
this.position = {
x: Math.floor(Math.random() * (this.backgroundSize.x - 1)),
y: Math.floor(Math.random() * (this.backgroundSize.y - 1))
};
// Generate random starting direction
let xDir = Math.random()*2 - 1;
let yDir = Math.random()*2 - 1;
// Convert to a unit vector
let length = Math.sqrt(xDir**2 + yDir**2);
this.direction = { x: xDir/length, y: yDir/length };
}
update() {
// Update the position of the object
this.position.x += this.direction.x * this.moveSpeed;
this.position.y += this.direction.y * this.moveSpeed;
// Handle going off the left & right sides of the screen
if (this.position.x < 1) this.direction.x = Math.abs(this.direction.x);
else if (this.position.x > this.backgroundSize.x-2) this.direction.x = -Math.abs(this.direction.x);
// Handle going off the top and bottom sides of the screen
if (this.position.y < 1) this.direction.y = Math.abs(this.direction.y);
else if (this.position.y > this.backgroundSize.y-2) this.direction.y = -Math.abs(this.direction.y);
}
}
We’ll call this update
function every frame after we have drawn all the points:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function draw() {
// Draw background & set Rectangle draw mode
background(255);
rectMode(CENTER);
// Draw scene rectangle
fill(30,30,30);
stroke(30,30,30);
drawRect(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT)
// Draw Lines to Nearby Points
POINT_LIST.forEach(currentPoint => {
drawLineToNearby(currentPoint);
});
// Draw Points & Update their positions
POINT_LIST.forEach(currentPoint => {
drawPoint(currentPoint);
currentPoint.update();
});
}
Conclusion
- We now have an animated 2D plexus effect!
- I encourage you to play around with the
MOVE_SPEED
,POINT_SIZE
,DISTANCE_TO_START_DRAWING_LINES
andTOTAL_POINTS
variables as well as thedrawLineToNearby
function to see what you like best.
More Ideas
Colour Change
- Demo available here
- This effect is done by changing the colour hue based off the x-coordinate using the
map
function. Full code below.
Horizontal Only
- Demo available here
Vertical Only
Example 1
- Demo available here
Example 2
Horizontal & Vertical
- Demo available here
Full Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
const MOVE_SPEED = 0.2;
const POINT_SIZE = 5;
const DISTANCE_TO_START_DRAWING_LINES = 150;
let TOTAL_POINTS = 300;
let SCREEN_WIDTH = 0;
let SCREEN_HEIGHT = 0;
let POINT_LIST = [];
// Drawing functions to handled inverted Y-Axis of the browser
const drawRect = (x, y, w, h) => rect(x, SCREEN_HEIGHT-y, w, h);
const drawLine = (x1, y1, x2, y2) => line(x1, SCREEN_HEIGHT-y1, x2, SCREEN_HEIGHT-y2);
const drawCircle = (x, y, d) => circle(x, SCREEN_HEIGHT-y, d);
const drawArc = (x, y, w, h, startAngle, stopAngle) => arc(x, SCREEN_HEIGHT-y, w, h, 2*Math.PI-stopAngle, 2*Math.PI-startAngle);
const drawTri = (x1, y1, x2, y2, x3, y3) => triangle(x1, SCREEN_HEIGHT-y1, x2, SCREEN_HEIGHT-y2, x3, SCREEN_HEIGHT-y3);
// Initial Setup
function setup() {
SCREEN_WIDTH = window.innerWidth - 20;
SCREEN_HEIGHT = window.innerHeight - 20
createCanvas(SCREEN_WIDTH, SCREEN_HEIGHT);
// Create random Points all over the screen
for (let i = 0; i < TOTAL_POINTS; i++) {
POINT_LIST.push(new Point(SCREEN_WIDTH, SCREEN_HEIGHT, MOVE_SPEED, POINT_SIZE));
}
frameRate(30);
}
function draw() {
// Draw background & set Rectangle draw mode
background(255);
rectMode(CENTER);
// Draw scene rectangle
fill(30,30,30);
stroke(30,30,30);
drawRect(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT)
// Draw Lines to Nearby Points
POINT_LIST.forEach(currentPoint => {
drawLineToNearby(currentPoint);
});
// Draw Points & Update their positions
POINT_LIST.forEach(currentPoint => {
drawPoint(currentPoint);
currentPoint.update();
});
}
const drawPoint = (pointObject) => {
stroke(200,200,200);
fill(200,200,200);
drawCircle(pointObject.position.x, pointObject.position.y, pointObject.pointSize);
}
const drawLineToNearby = (targetPoint) => {
colorMode(HSB, 360, 100, 100, 100);
POINT_LIST.forEach(point => {
let distanceBetween = distanceBetweenPoints(targetPoint, point);
// Only draw the line if it's close enough
if (distanceBetween < DISTANCE_TO_START_DRAWING_LINES) {
let colour = map(targetPoint.position.x, 0, targetPoint.backgroundSize.x, 0, 300);
let brightness = map(distanceBetween, 0, DISTANCE_TO_START_DRAWING_LINES, 100, 0);
stroke(colour, 100, 100, brightness);
drawLine(targetPoint.position.x, targetPoint.position.y, point.position.x, point.position.y);
}
})
colorMode(RGB, 255);
}
// Calculate Euclidean distance between 2 points
const distanceBetweenPoints = (pointA, pointB) => {
return Math.sqrt(Math.pow(pointA.position.x - pointB.position.x, 2) + Math.pow(pointA.position.y - pointB.position.y, 2));
}
class Point {
constructor(backgroundSizeX, backgroundSizeY, moveSpeed, pointSize) {
// Store properties
this.moveSpeed = moveSpeed;
this.pointSize = pointSize;
this.backgroundSize = { x: backgroundSizeX, y: backgroundSizeY };
// Generate random starting position
this.position = {
x: Math.floor(Math.random() * (this.backgroundSize.x - 1)),
y: Math.floor(Math.random() * (this.backgroundSize.y - 1))
};
// Generate random starting direction
let xDir = Math.random()*2 - 1;
let yDir = Math.random()*2 - 1;
// Convert to a unit vector
let length = Math.sqrt(xDir**2 + yDir**2);
this.direction = { x: xDir/length, y: yDir/length };
}
update() {
// Update the position of the object
this.position.x += this.direction.x * this.moveSpeed;
this.position.y += this.direction.y * this.moveSpeed;
// Handle going off the left & right sides of the screen
if (this.position.x < 1) this.direction.x = Math.abs(this.direction.x);
else if (this.position.x > this.backgroundSize.x-2) this.direction.x = -Math.abs(this.direction.x);
// Handle going off the top and bottom sides of the screen
if (this.position.y < 1) this.direction.y = Math.abs(this.direction.y);
else if (this.position.y > this.backgroundSize.y-2) this.direction.y = -Math.abs(this.direction.y);
}
}