Physics
Point and spring simulation
right click to select a point
select multiple points to connect them
hold down "d" and move mouse to drag
Introduction
A few years ago I wrote a little website called chubmarine.com. It was a physics simulation type thing, much like the one that's on this page. It's no longer active, but I thought it would be interesting to write about anyway.
While it was cool to get attention online, I think the event that had the most impact on me relating to the site was a conversation I had with my mom right after I launched it. Apparently, what I thought was a clever portmanteau of my last name and the word submarine was actually a euphemism for something or other that is perhaps not the most appropriate name for a physics simulation. My mom found this out when she googled "chubmarine", and the first result was a link to urbandictionary.com. You can probably guess how that discussion went.
The Nerdy Stuff
Simulating points and springs may seem super complicated and math/physics intensive, however it's actually quite simple to get up and running in very few lines of code. I won't go into a detailed explanation of each and every single feature, but I'll try to highlight the most important bits.
Vectors
You might be familiar with vectors from math class, but just as a refresher here is a short video which explains the concept suitably well.
In essence, a vector is an object that represents both direction and magnitude. In our simulation,
vectors are instances of a class that has two properties:
x
and y
.
Vectors are a very convenient way to represent the positions and velocities of our points.
The Vector
class in this simulation contains a few helper
functions, for example a length
function to get the length of the vector, or
a normalize
function which creates a vector in the same direction as the original but with a length of 1.
Game Loop
In video game programming there exists the concept of the
game loop, which is the infinitely repeating
sequence of
recieving input, changing the game state, and drawing the game on the screen. As a consequence
of this particular simulation being written in JavaScript, we can't actually have a real infinite loop,
since that would freeze the page
(I found this out the hard way), so instead we have to emulate an infinite loop using
callbacks and the requestAnimationFrame
function.
The simulation is started by calling the following start
function, which runs a single step
of the game, and then schedules itself to be run again.
Simulation.prototype.start = function () {
this.lastFrameTime = this.lastFrameTime || new Date();
var currentTime = new Date();
// difference between the time that the last frame
// was run and the current time in milliseconds
var delta = currentTime - this.lastFrameTime;
// number that we multiply displacements by to
// compensate for going faster/slower than 60fps
var adjust = (1000 / 60) / delta;
console.log(adjust);
this.tick(adjust);
this.lastFrameTime = currentTime;
var that = this;
requestAnimationFrame(function () {
that.start();
});
};
Notice how we can compensate for the app running slower or faster than we want by calculating the difference between the times at which the current and last frames were run.
The tick
function is as follows:
Simulation.prototype.tick = function (adjust) {
this.clear();
// first we update the game state
for (var i = 0; i < this.springs.length; i++) {
this.springs[i].update(this.context, adjust);
}
for (var i = 0; i < this.points.length; i++) {
this.points[i].update(this.context, adjust);
}
// then we draw it to the screen
for (var i = 0; i < this.springs.length; i++) {
this.springs[i].draw(this.context);
}
for (var i = 0; i < this.points.length; i++) {
this.points[i].draw(this.context);
}
};
The arrays springs
and points
are defined elsewhere in the program.
context
is an instance of the
CanvasRenderingContext2D
class, which allows us to draw graphics on an HTML canvas. The clear
function
clears the canvas so that it is empty before the game draws another frame.
Points
The point class is defined as follows:
function Point(x, y) {
this.pos = new Vector(x, y);
this.vel = new Vector(0, 0);
this.highlighted = false;
this.dragged = false;
}
Point.size = 10;
Point.friction = 0.01;
Point.bounceResistance = 0.5;
Point.airResistance = 1.025;
Point.mass = 0.2;
Probably the most important part of the Point
class is the update
function,
which, as you can probably infer from the name, updates the state of the point.
Point.prototype.update = function (context, adjust) {
if (!this.dragged) {
this.pos = this.pos.add(this.vel.multiply(adjust));
this.vel = this.vel.add(new phys.Vector(0, Point.mass).multiply(adjust));
this.vel = this.vel.divide(Point.airResistance);
}
this.collideWithWalls(context);
};
You may be wondering why we check for whether or not the point is dragged. In order for the user to be able to drag a point, it has to be at the position of the mouse exactly. Therefore, while it's being dragged, it does not follow the laws of physics.
Collisions
Implementing collisions was a tricky problem. One might be tempted to simply reverse the velocity of a point when it "hits" a wall, however that may cause the point to get stuck, as its velocity is switched back and forth forever. After a few iterations, the following algorithm seemed to be the most effective at modeling collisions. In order to collide the points with the walls, or edges of the canvas, the following steps are taken for each point:
- Check if the point is outside the canvas
- If it is, then:
- Move it back in the canvas via the closest wall
- Set the relevant component of its velocity to point towards the inside of the canvas
- Dampen the relevant component of its velocity to simulate loss of energy
- Dampen the other component of its velocity to simulate friction
Here's the implementation:
Point.prototype.collideWithWalls = function (context) {
if (this.pos.x < Point.size / 2) { // if too far left.
this.pos.x = Point.size / 2;
this.vel.x = Math.abs(this.vel.x) * (1 - Point.bounceResistance);
this.vel.y *= 1 - Point.friction;
}
if (this.pos.y < Point.size / 2) { // if too far up.
this.pos.y = Point.size / 2;
this.vel.y = Math.abs(this.vel.y) * (1 - Point.bounceResistance);
this.vel.x *= 1 - Point.friction;
}
if (this.pos.x > context.canvas.width - Point.size / 2) { // if too far to the right.
this.pos.x = context.canvas.width - Point.size / 2;
this.vel.x = Math.abs(this.vel.x) * -1 * (1 - Point.bounceResistance);
this.vel.y *= 1 - Point.friction;
}
if (this.pos.y > context.canvas.height - Point.size / 2) { // if too far to the bottom.
this.pos.y = context.canvas.height - Point.size / 2;
this.vel.y = Math.abs(this.vel.y) * -1 * (1 - Point.bounceResistance);
this.vel.x *= 1 - Point.friction;
}
}
As you can see it's not the most elegant implementation with a lot of repetition, but it gets the job done.
Springs
In actual real physics the motion of a spring is modeled using Hooke's Law. Essentially what it boils down to is that the force that the spring exerts on an object is proportional to its extension from rest. If a spring is squished or stretched more, it should exert proportionally more force.
The Spring
class, which attempts to model the action of a real spring, is
defined as follows:
function Spring(firstPoint, secondPoint) {
this.first = firstPoint;
this.second = secondPoint;
this.restLength = 100;
this.springyness = 16;
}
The update
function of the Spring
class handles the implementation
details of Hooke's Law.
Spring.prototype.update = function (context, adjust) {
var difference = this.second.pos.subtract(this.first.pos);
var distance = this.length();
var extension = distance - this.restLength;
var vel = difference.normalize().divide(this.springyness).multiply(extension).multiply(adjust);
this.first.vel = this.first.vel.add(vel);
this.second.vel = this.second.vel.subtract(vel);
};
Conclusion
I really enjoyed recreating this old project, especially since it has become my own sort of "Hello World" app. Every time I rewrite it I discover something new or figure out some clever way to improve something. This is probably the last time I'll make it, which is why I've decided to document how it works. I hope you've had as much fun playing around with the sim as I did making it.
Finally, if you have any questions or comments, feel free to reach out to me!