Let's be honest! I am falling in love with the p5.js framework. It makes my algorithms and simulations so beautifully presentable! And that too with so minimum amount of boilerplate.
After building a Mandelbrot set simulator, I thought, why to stop here, let's build something related to Physics, which I loved very much during my +2 days. Then the idea of creating the Gravitation simulator came into my mind.
The Physics Involved
Gravitation comes under (classically) Central forces.
The magnitude of gravitational force between 2 point masses
where
Combining this with Newton's famous 2nd law,
the magnitude of acceleration
Now, actually in space, every celestial body moves towards every other due to the Gravitation.
That'd indeed be an incredible multi-body dynamics problem to solve.
But our scope is pretty simple.
We'll just consider
Gravitation is always attractive (Shhhh! GTR fans).
What that means is, if, by some magic,
Note that the acceleration is independent of
Approximations and rescalings
The Gravitation is very weak as a force.
Using
Another cheating that I do in the simulation,
is that I keep all object's mass as 1.
For the simulator,
I also work in 2D plane and decompose the accelerations in X and Y axes using the slope of the line joining 2 bodies.
So, if
I also clip
The total acceleration of the mass
I'll drop the sum sign from here on.
The last approximation has to do with how I calculate the velocity and position in the next timestep.
In reality, velocity would be a nice and smooth time integral of the acceleration and the displacement would be a time integral of the velocity.
Here I approximate integral by sum.
I take a small timestep
Of course, the calculations are done component-wise.
The code, finally
With all the above considerations in mind, let's jump right into the code.
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Gravity</title> | |
<meta name="charset" content="utf-8"> | |
<meta name="viewport" content="width=device-width"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/p5.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/addons/p5.sound.min.js"></script> | |
</head> | |
<body style="display: flex; flex-direction: row;"> | |
<div style="display:flex; flex-direction:column;"> | |
<input type="number" max="600" min="0" id="posX" placeholder="posX"> | |
<input type="number" max="600" min="0" id="posY" placeholder="posY"> | |
<input type="number" max="1000" min="-1000" id="velX" placeholder="velX"> | |
<input type="number" max="1000" min="-1000" id="velY" placeholder="velY"> | |
<button onclick="start()">Start</button> | |
<button onclick="reset()">Reset</button> | |
</div> | |
<script src="sketch.js"></script> | |
</body> | |
</html> |
The index.html
loads the p5.js
library as usual.
Notice that I have created fields for initial position and velocity
as well as buttons for starting and resetting the simulation.
The main juice of the code is sketch.js
:
let particles = []; | |
let position = [100, 100]; | |
let velocity = [0, 0]; | |
let started = false; | |
let stuck = false; | |
let lines = []; | |
function start() { | |
started = true; | |
} | |
function reset(){ | |
started = false; | |
stuck = false; | |
particles = []; | |
lines = []; | |
} | |
function setup(){ | |
createCanvas(600, 600); | |
} | |
const timestep = 1; | |
function draw(){ | |
let mindist = 100000000; | |
background(200); | |
if (!started){ | |
if (mouseIsPressed){ | |
if (mouseX >= 0 && mouseX <= 600) | |
particles.push([mouseX, mouseY]); | |
console.log(particles) | |
} | |
position[0] = Number(document.getElementById("posX").value); | |
position[1] = Number(document.getElementById("posY").value); | |
velocity[0] = Number(document.getElementById("velX").value); | |
velocity[1] = Number(document.getElementById("velY").value); | |
}else{ | |
let acc = [0, 0]; | |
particles.forEach(elt => { | |
let dist_sqr = (elt[0] - position[0]) * (elt[0] - position[0]) + (elt[1] - position[1]) * (elt[1] - position[1]); | |
if (dist_sqr / (velocity[0] * velocity[0] + velocity[1] * velocity[1]) < 0.1) stuck = true; | |
if (dist_sqr < mindist) mindist = dist_sqr; | |
acc[0] += constrain(10 * (elt[0] - position[0]) / (dist_sqr * Math.sqrt(dist_sqr)), -10, 10); | |
acc[1] += constrain(10 * (elt[1] - position[1]) / (dist_sqr * Math.sqrt(dist_sqr)), -10, 10); | |
}); | |
if (!stuck){ | |
velocity[0] += acc[0] * timestep; | |
velocity[1] += acc[1] * timestep; | |
position[0] += velocity[0] * timestep; | |
position[1] += velocity[1] * timestep; | |
lines.push([...position]); | |
} | |
} | |
particles.forEach(elt => { | |
fill(100, 200, 100); | |
ellipse(elt[0], elt[1], 10, 10); | |
}); | |
for (let i = 0; i < lines.length - 1; i++){ | |
stroke(255, 0, 0); | |
line(lines[i][0], lines[i][1], lines[i+1][0], lines[i+1][1]); | |
} | |
stroke(0, 0, 0); | |
fill(255, 0, 0); | |
rect(position[0], position[1], 5, 5); | |
} | |
particles
array contains the location of the fixed unit masses;position
andvelocity
correspond to the moving mass.started
andstuck
are simulation states.lines
contains the set of points along the path traced by the moving mass.
In the draw
function, I check if the mouse is pressed WITHIN THE CANVAS,
I add that location to the particles
array.
However if started
is set to true
, I no longer add bodies,
rather I proceed on calculating the next position at every frame.
Finally, I draw the fixed particles as green circles and the moving particle as a red body which leaves a red trace.
This logic works fine, until the red body from very far off comes
very near a green body.
In this process it gains tremendous velocity and shoots
out of the screen.
To prevent that from happening,
the stuck
variable is set to true
whenever such stuff happens.
And once set to true
, the particle's position is not updated,
it remains stuck.
This sticking behaviour is controlled by the metric in Line 40. The value to which it is set in the code hardly has any effect. Change the threshold to 1 or 10 to see the sticking effect.