Post

Building a Plexus

In this post we’ll look at how to build a Plexus in JavaScript using the p5.js library.

  1. What we are Making
  2. Setup p5.js
  3. Points
  4. Lines
  5. Moving Points
  6. Conclusion
  7. More Ideas
  8. Full Code

What we are Making

Our goal is to build something that looks like this:

How we will make it

  1. Generate random points all over the screen.
  2. Draw lines between points that are close to each other. Closer points will have a brighter coloured line.
  3. Move the points slightly each frame.
  4. 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 and TOTAL_POINTS variables as well as the drawLineToNearby 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

Vertical Only

Example 1

Example 2

Horizontal & Vertical


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);
	}
}


This post is licensed under CC BY 4.0 by the author.