Skip to content

Instantly share code, notes, and snippets.

@BonsaiDen
Created August 2, 2013 23:18

Revisions

  1. BonsaiDen created this gist Aug 2, 2013.
    459 changes: 459 additions & 0 deletions box.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,459 @@
    (function(exports) {

    // Vector Class -----------------------------------------------------------
    // ------------------------------------------------------------------------
    function Vector2(x, y) {
    this.x = x;
    this.y = y;
    }

    Vector2.prototype = {

    add: function(v) {
    return new Vector2(this.x + v.x, this.y + v.y);
    },

    sub: function(v) {
    return new Vector2(this.x - v.x, this.y - v.y);
    },

    dot: function(v) {
    return this.x * v.x + this.y * v.y;
    },

    cross: function(v) {
    return this.x * v.y - this.y * v.x;
    },

    div: function(s) {
    this.x /= s;
    this.y /= s;
    return this;
    },

    mul: function(s) {
    this.x *= s;
    this.y *= s;
    return this;
    },

    normalize: function() {

    var len = this.length();
    if (this.length > 0.0001) {
    var invLen = 1.0 / len;
    this.x *= invLen;
    this.y *= invLen;
    }

    },

    // Length
    lengthSqr: function() {
    return this.x * this.x + this.y * this.y;
    },

    length: function() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
    }

    };


    // AABB implementation ----------------------------------------------------
    // ------------------------------------------------------------------------
    function AABB(x, y, w, h, mass) {

    mass = mass !== undefined ? mass : 0;

    // Properties
    this.id = ++AABB.id;

    // The is actually the inverse mass.
    // The value 0 is used as a placeholder for infinite mass
    this.im = mass === 0 ? 0 : 1 / mass;

    // How "bouncy" this box is. The higher the value, the more the box will
    // bounce of in case of a collision
    this.restitution = 0.1;

    this.staticFriction = 1;
    this.dynamicFriction = 0.3;

    this.position = new Vector2(x, y);
    this.velocity = new Vector2(0, 0);
    this.size = new Vector2(w, h);

    // Internal, temporary values only used during the collision phase
    this.force = new Vector2(0, 0);
    this.min = new Vector2(0, 0);
    this.max = new Vector2(0, 0);

    }

    AABB.prototype = {

    /**
    * Quick check to see if all of the axis of two AABBs are overlapping.
    */
    isOverlapping: function(other) {
    if (this.max.x < other.min.x || this.min.x > other.max.x) {
    return false;

    } else if (this.max.y < other.min.y || this.min.y > other.max.y) {
    return false;

    } else {
    return true;
    }
    },

    /**
    * Update the boundaries of the AABB
    */
    updateBounds: function() {
    this.min.x = this.position.x - this.size.x;
    this.max.x = this.position.x + this.size.x;
    this.min.y = this.position.y - this.size.y;
    this.max.y = this.position.y + this.size.y;
    },

    /**
    * Integrate force and gravity into the AABB's velocity.
    */
    integrateForces: function(gravity) {
    if (this.im !== 0) {
    this.velocity.x += (this.force.x * this.im + gravity.x) / 2;
    this.velocity.y += (this.force.y * this.im + gravity.y) / 2;
    }
    },

    /**
    * Integrate the velocity into the AABB's position.
    */
    integrateVelocity: function(gravity) {
    if (this.im !== 0) {
    this.position.x += this.velocity.x;
    this.position.y += this.velocity.y;
    this.integrateForces(gravity);
    }
    },

    applyImpulse: function(x, y) {
    this.velocity.x += this.im * x;
    this.velocity.y += this.im * y;
    },

    applyForce: function(x, y) {
    this.force.x += x;
    this.force.y += y;
    },

    clearForces: function() {
    this.force.x = 0;
    this.force.y = 0;
    }

    };


    // Manifold which contains information about a single collision -----------
    // ------------------------------------------------------------------------
    function Manifold(a, b) {

    this.a = a;
    this.b = b;
    this.e = Math.min(a.restitution, b.restitution);
    this.sf = 0;
    this.df = 0;

    this.normal = new Vector2(0, 0);
    this.penetration = 0;

    }

    Manifold.prototype = {

    /**
    * Initialize the manifold for the collision phase.
    */
    init: function(gravity, epsilon) {

    // TODO is this correct?
    this.sf = Math.sqrt(this.a.staticFriction * this.b.staticFriction);
    this.df = Math.sqrt(this.a.dynamicFriction * this.b.dynamicFriction);

    // Figure out whether this is a resting collision, if so do not apply
    // any restitution
    var rx = this.b.velocity.x - this.a.velocity.x,
    ry = this.b.velocity.y - this.a.velocity.y;

    if ((rx * rx + ry * ry) < (gravity.x * gravity.x + gravity.y * gravity.y) + epsilon) {
    this.e = 0.0;
    }

    },

    /**
    * Solve the SAT for two AABBs
    */
    solve: function() {

    // Vector from A to B
    var nx = this.a.position.x - this.b.position.x,
    ny = this.a.position.y - this.b.position.y;

    // Calculate half extends along x axis
    var aex = (this.a.max.x - this.a.min.x) / 2,
    bex = (this.b.max.x - this.b.min.x) / 2;

    // Overlap on x axis
    var xoverlap = aex + bex - Math.abs(nx);
    if (xoverlap > 0) {

    // Calculate half extends along y axis
    var aey = (this.a.max.y - this.a.min.y) / 2,
    bey = (this.b.max.y - this.b.min.y) / 2;

    // Overlap on x axis
    var yoverlap = aey + bey - Math.abs(ny);
    if (yoverlap) {

    // Find out which axis is the axis of least penetration
    if (xoverlap < yoverlap) {

    // Point towards B knowing that n points from A to B
    this.normal.x = nx < 0 ? 1 : -1;
    this.normal.y = 0;
    this.penetration = xoverlap;
    return true;

    } else {

    // Point towards B knowing that n points from A to B
    this.normal.x = 0;
    this.normal.y = ny < 0 ? 1 : -1;
    this.penetration = yoverlap;
    return true;

    }

    }

    }

    return false;

    },

    /**
    * Resolves a collision by applying a impulse to each of the AABB's
    * involved.
    */
    resolve: function(epsilon) {

    var a = this.a,
    b = this.b,

    // Relative velocity from a to b
    rx = b.velocity.x - a.velocity.x,
    ry = b.velocity.y - a.velocity.y,
    velAlongNormal = rx * this.normal.x + ry * this.normal.y;

    // If the velocities are separating do nothing
    if (velAlongNormal > 0 ) {
    return;

    } else {

    // Correct penetration
    var j = -(1.0 + this.e) * velAlongNormal;
    j /= (a.im + b.im);

    // Apply the impulse each box gets a impulse based on its mass
    // ratio
    a.applyImpulse(-j * this.normal.x, -j * this.normal.y);
    b.applyImpulse(j * this.normal.x, j * this.normal.y);

    // Apply Friction
    var tx = rx - (this.normal.x * velAlongNormal),
    ty = ry - (this.normal.y * velAlongNormal),
    tl = Math.sqrt(tx * tx + ty * ty);

    if (tl > epsilon) {
    tx /= tl;
    ty /= tl;
    }

    var jt = -(rx * tx + ry * ty);
    jt /= (a.im + b.im);

    // Don't apply tiny friction impulses
    if (Math.abs(jt) < epsilon) {
    return;
    }

    if (Math.abs(jt) < j * this.sf) {
    tx = tx * jt;
    ty = ty * jt;

    } else {
    tx = tx * -j * this.df;
    ty = ty * -j * this.df;
    }

    a.applyImpulse(-tx, -ty);
    b.applyImpulse(tx, ty);

    }

    },

    /**
    * This will prevent objects from sinking into each other when they're
    * resting.
    */
    positionalCorrection: function() {

    var a = this.a,
    b = this.b;

    var percent = 0.7,
    slop = 0.05,
    m = Math.max(this.penetration - slop, 0.0) / (a.im + b.im);

    // Apply correctional impulse
    var cx = m * this.normal.x * percent,
    cy = m * this.normal.y * percent;

    a.position.x -= cx * a.im;
    a.position.y -= cy * a.im;

    b.position.x += cx * b.im;
    b.position.y += cy * b.im;

    }

    };


    // AABB collision engine --------------------------------------------------
    // ------------------------------------------------------------------------
    function Engine() {
    this.iterations = 10;
    this.gravity = new Vector2(0, 1);
    this.contacts = [];
    this.boxes = [];
    this.length = 0;
    }

    Engine.EPSILON = 0.0001;

    Engine.prototype = {

    findCollisions: function() {

    this.contacts.length = 0;

    for(var i = 0; i < this.length; i++) {

    var a = this.boxes[i];
    for(var j = i + 1; j < this.length; j++) {

    var b = this.boxes[j];

    // Ignore collisions between objects with infinite mass
    if (a.im === 0 && b.im === 0) {
    continue;

    } else if (a.isOverlapping(b)) {
    var c = new Manifold(a, b);
    if (c.solve()) {
    this.contacts.push(c);
    }
    }

    }

    }

    },

    integrateForces: function() {
    for(var i = 0; i < this.length; i++) {
    this.boxes[i].integrateForces(this.gravity);
    }
    },

    initializeCollisions: function() {
    for(var i = 0, l = this.contacts.length; i < l; i++) {
    this.contacts[i].init(this.gravity, Engine.EPSILON);
    }
    },

    solveCollisions: function() {
    for(var i = 0; i < this.iterations; i++) {
    for(var c = 0, l = this.contacts.length; c < l; c++) {
    this.contacts[c].resolve(Engine.EPSILON);
    }
    }
    },

    integrateVelocities: function() {
    for(var i = 0; i < this.length; i++) {
    this.boxes[i].integrateVelocity(this.gravity);
    this.boxes[i].clearForces();
    this.boxes[i].updateBounds();
    }
    },

    correctPositions: function() {
    for(var i = 0, l = this.contacts.length; i < l; i++) {
    this.contacts[i].positionalCorrection();
    }
    },

    tick: function() {
    this.findCollisions();
    this.integrateForces();
    this.initializeCollisions();
    this.solveCollisions();
    this.integrateVelocities();
    this.correctPositions();
    },

    addBox: function(box) {
    this.boxes.push(box);
    this.length++;
    }

    };

    exports.Engine = Engine;
    exports.Vector2 = Vector2;
    exports.Box = AABB;

    })(typeof module === 'undefined' ? window : module.exports);


    var box = exports;

    var ground = new box.Box(0, 20, 100, 10);
    //ground.restitution = 1;
    var object = new box.Box(0, -20, 10, 10, 1);
    //var two = new box.Box(20, -40, 10, 10, 1);
    //two.restitution = 0.1;

    var w = new box.Engine();
    w.addBox(ground);
    w.addBox(object);
    //w.addBox(two);

    //object.applyImpulse(4, 0);

    setInterval(function() {
    //object.applyForce(0.5, 0);
    w.tick();
    console.log(Math.round(object.position.x), object.position.y);

    }, 33);