Simulating Custom Physics: Attraction and Repulsion with p5.Vector
In this post, we’ll explore how to define forces between particles as vectors and animate their interactions using p5.js. Specifically, we'll create a system where a particle responds to the pull and push of a moving reference point.
Exploring Abstract Motion
Here is the result of the simulation.
The red particle moves in a uniform circular motion. The black particle reacts to it: if it’s far away, it feels an attractive force; if it gets too close, it’s repelled.
This isn't gravity. It's an imaginary physical law. There is no friction, inertia, or acceleration here. Furthermore, the interaction is one-way—the red particle is completely unaffected by the black one. It’s a purely mathematical playground where real-world physics don't apply.
The values of attraction and repulsion are the magnitude of the force, including negative values. The direction in which the force acts is the direction of the line connecting the red dot and the black dot. Attraction is from the black dot to the red dot, and repulsion is the opposite.
Vectors have not only the magnitude of force but also the direction, so you can use 'p5.Vector' to neatly translate this physics law into code.
Explanation of Example Code
Here is the p5.js code that draws the animation above.
/**
* Animation of a point moving under attraction and repulsion using 'p5.Vector'
* When only one point receives force
*
* @author @deconbatch
* @version 0.1
* @license GPL3
* p5.js 1.6.0
* created 2023.02.15
*/
const w = 480;
const h = w;
const pullFix = 20;
const pushFix = 30;
const baseDist = w * 0.25;
const moverR = w * 0.2;
const moverT = 0.1;
let node;
function setup() {
createCanvas(w, h);
frameRate(24);
initial();
}
function draw() {
// Calculating the position of a moving reference point
const mover = createVector(
w * 0.5 + moverR * cos(moverT * frameCount),
h * 0.5 + moverR * sin(moverT * frameCount)
);
// Calculating the position of the point receiving the force
let d = (p5.Vector.dist(mover, node) - baseDist) / baseDist;
let g = (d < 0) ? d * pushFix : d * pullFix;
let t = p5.Vector.sub(mover, node).heading();
node.add(
g * cos(t),
g * sin(t),
-node.z + (d + 0.5) * w * 0.1
);
// draw points
background(192);
fill(192, 0, 0);
circle(mover.x, mover.y, 10);
fill(16);
circle(node.x, node.y, node.z);
}
// initialize
function initial() {
frameCount = 0;
node = createVector(
random(w),
random(h),
0.0
);
noStroke();
}
// Redo with mouse click
function mouseClicked() {
initial();
}
Explanation of variables
The variable 'mover' represents a red dot moving in a uniform circle, which is the basis for attraction and repulsion.
const mover = createVector(
w * 0.5 + moverR * cos(moverT * frameCount),
h * 0.5 + moverR * sin(moverT * frameCount)
);
The following two constants determine the radius of the mover's circular motion and its rotation speed.
const moverR = w * 0.2;
const moverT = 0.1;
The variable 'node' represents a black dot that moves under attraction and repulsion.
const pullFix = 20;
const pushFix = 30;
const baseDist = w * 0.25;
Please think of the following three constants as physical constants in the world of this code. Changing the value will change the behavior of the black dot.
const pullFix = 20;
const pushFix = 30;
const baseDist = w * 0.25;
Calculating the Forces
Using p5.Vector.dist(), we calculate the distance between the two particles.
If the distance is less than our threshold (baseDist), we apply a repulsive force (pushFix). If it's greater, we apply an attractive force (pullFix).
let d = (p5.Vector.dist(mover, node) - baseDist) / baseDist;
let g = (d < 0) ? d * pushFix : d * pullFix;
I named it 'g' even though it's not gravity...
The formula below is the part that calculates the direction in which the force acts.
let t = p5.Vector.sub(mover, node).heading();
The direction of the force is determined by the vector pointing from the node to the mover.
p5.Vector.sub(mover, node) returns a vector representing the direction of the mover as seen from the node. By calling .heading(), we extract the angle required to move the node toward (or away from) the reference point.
A Non-standard Use of the Z-Axis
Using the formula below, add the calculated force magnitude and direction to the node position x, y.
node.add(
g * cos(t),
g * sin(t),
However, what is the process for the z part of the node in the code below?
-node.z + (d + 0.5) * w * 0.1
While p5.Vector is designed for 3D coordinates (x, y, z), I’ve used the z attribute here to store the size of the particle.
In the code, circle(node.x, node.y, node.z) looks like a 3D operation, but it's simply a shortcut for a 2D circle with a dynamic radius.
circle(node.x, node.y, node.z);
It’s a bit of a "hacky" implementation—misleading, perhaps, but effective for this experiment!
Play with the Code
In the example code, 'background(192)' in 'draw()' is used to fill in each time. If we move this to 'initial()', we can see that the black dot's movement follows a consistent pattern.
In the video below, white dots are used for filming reasons.
In the example below, we can draw an interesting shape by leaving a trail.
By changing the constant values, the example code will draw a shape like this.
I thought it would be interesting to have multiple nodes instead of just one. However, it was fine when I tried 2 or 3 nodes, but when I increased it to 10 or 20, it lost its visual appeal.
As the number of nodes increased, it became obvious that they were just moving the same distance from the red dot, and he results became predictable.
Conclusion: Becoming a Creator of Laws
Designing fictitious physical laws is one of the joys of creative coding. It allows you to visualize behaviors that couldn't exist in reality.
p5.Vector is an essential tool for this, providing all the methods necessary to translate abstract logic into fluid motion. Why settle for real-world physics when you can invent your own?






