Physics

Point and spring simulation

left click to create a point
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:

  1. Check if the point is outside the canvas
  2. If it is, then:
    1. Move it back in the canvas via the closest wall
    2. Set the relevant component of its velocity to point towards the inside of the canvas
    3. Dampen the relevant component of its velocity to simulate loss of energy
    4. 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!