Tony Lukasavage

Caffeine. Whiskey. Code. Mostly the last one.

Box2D JS - Physics in HTML5 & Javascript Guide

I hate to do it, but I highly recommend the Google Chrome browser for this demo. In other browsers it may take a while to load and runs slower.

→ Click on the demo above or here, then right click to view source → Download the Box2D JS library → Download the concatenated version (~350KB) → Download the minified version (~170 KB)

The Overview

Some of you might remember the Box2DFlashAS3 demo I did a while ago. Well here’s its HTML5 counter-part, with the help of Box2D JS. Box2D is the Javascript port of the Box2DFlashAS3 library, which in turn is a port of the Box2D C++ library. Put simply, this library allows you to apply 2 dimensional physics to objects on your HTML5 canvas element. Just click anywhere on the demo above to see what I mean.

The code behind this is a condensed, easier-to-follow version of the demo code available by viewing the page source at the Box2D JS site. I personally found it a little tough to follow initially and really wanted to avoid the large amount of individual includes necessary to get it working. To that end I created the concatenated and minified versions downloadable above. So basically the includes necessary go from this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!--[if IE]><script type="text/javascript" src="lib/excanvas.js"></script><![endif]-->
<script src="lib/prototype-1.6.0.2.js"></script>

<!-- box2djs -->
<script src='js/box2d/common/b2Settings.js'></script>
<script src='js/box2d/common/math/b2Vec2.js'></script>
<script src='js/box2d/common/math/b2Mat22.js'></script>
<script src='js/box2d/common/math/b2Math.js'></script>
<script src='js/box2d/collision/b2AABB.js'></script>
<script src='js/box2d/collision/b2Bound.js'></script>
<script src='js/box2d/collision/b2BoundValues.js'></script>
<script src='js/box2d/collision/b2Pair.js'></script>
<script src='js/box2d/collision/b2PairCallback.js'></script>
<script src='js/box2d/collision/b2BufferedPair.js'></script>
<script src='js/box2d/collision/b2PairManager.js'></script>
<script src='js/box2d/collision/b2BroadPhase.js'></script>
<script src='js/box2d/collision/b2Collision.js'></script>
<script src='js/box2d/collision/Features.js'></script>
<script src='js/box2d/collision/b2ContactID.js'></script>
<script src='js/box2d/collision/b2ContactPoint.js'></script>
<script src='js/box2d/collision/b2Distance.js'></script>
<script src='js/box2d/collision/b2Manifold.js'></script>
<script src='js/box2d/collision/b2OBB.js'></script>
<script src='js/box2d/collision/b2Proxy.js'></script>
<script src='js/box2d/collision/ClipVertex.js'></script>
<script src='js/box2d/collision/shapes/b2Shape.js'></script>
<script src='js/box2d/collision/shapes/b2ShapeDef.js'></script>
<script src='js/box2d/collision/shapes/b2BoxDef.js'></script>
<script src='js/box2d/collision/shapes/b2CircleDef.js'></script>
<script src='js/box2d/collision/shapes/b2CircleShape.js'></script>
<script src='js/box2d/collision/shapes/b2MassData.js'></script>
<script src='js/box2d/collision/shapes/b2PolyDef.js'></script>
<script src='js/box2d/collision/shapes/b2PolyShape.js'></script>
<script src='js/box2d/dynamics/b2Body.js'></script>
<script src='js/box2d/dynamics/b2BodyDef.js'></script>
<script src='js/box2d/dynamics/b2CollisionFilter.js'></script>
<script src='js/box2d/dynamics/b2Island.js'></script>
<script src='js/box2d/dynamics/b2TimeStep.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactNode.js'></script>
<script src='js/box2d/dynamics/contacts/b2Contact.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactConstraint.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactConstraintPoint.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactRegister.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactSolver.js'></script>
<script src='js/box2d/dynamics/contacts/b2CircleContact.js'></script>
<script src='js/box2d/dynamics/contacts/b2Conservative.js'></script>
<script src='js/box2d/dynamics/contacts/b2NullContact.js'></script>
<script src='js/box2d/dynamics/contacts/b2PolyAndCircleContact.js'></script>
<script src='js/box2d/dynamics/contacts/b2PolyContact.js'></script>
<script src='js/box2d/dynamics/b2ContactManager.js'></script>
<script src='js/box2d/dynamics/b2World.js'></script>
<script src='js/box2d/dynamics/b2WorldListener.js'></script>
<script src='js/box2d/dynamics/joints/b2JointNode.js'></script>
<script src='js/box2d/dynamics/joints/b2Joint.js'></script>
<script src='js/box2d/dynamics/joints/b2JointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2DistanceJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2DistanceJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2Jacobian.js'></script>
<script src='js/box2d/dynamics/joints/b2GearJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2GearJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2MouseJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2MouseJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2PrismaticJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2PrismaticJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2PulleyJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2PulleyJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2RevoluteJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2RevoluteJointDef.js'></script>

to this:

1
2
3
<!--[if IE]><script type="text/javascript" src="lib/excanvas.js"></script><![endif]-->
<script src="lib/prototype-1.6.0.2.js"></script>
<script src="box2djs.min.js"></script>

You still need to download Box2D JS for its dependencies on Prototype and excanvas, but using my single file versions of the library will make getting started much cleaner and easier. OK, enough of the back story, on to the code.

The Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>SavageLook.com - Box2D JS Hello World</title>
  <!--[if IE]><script type="text/javascript" src="lib/excanvas.js"></script><![endif]-->
  <script src="lib/prototype-1.6.0.2.js"></script>
  <script src="box2djs.min.js"></script>

  <script type="text/javascript">
  var world;
  var ctx;
  var canvasWidth;
  var canvasHeight;
  var canvasTop;
  var canvasLeft;

  function drawWorld(world, context) {
      for (var j = world.m_jointList; j; j = j.m_next) {
          drawJoint(j, context);
      }
      for (var b = world.m_bodyList; b; b = b.m_next) {
          for (var s = b.GetShapeList(); s != null; s = s.GetNext()) {
              drawShape(s, context);
          }
      }

      ctx.font = 'bold 18px arial';
      ctx.textAlign = 'center';
      ctx.fillStyle = '#000000';
      ctx.fillText("Click the screen to add more objects", 400, 20);
      ctx.font = 'bold 14px arial';
      ctx.fillText("Performance will vary by browser", 400, 40);

  }

  function drawJoint(joint, context) {
      var b1 = joint.m_body1;
      var b2 = joint.m_body2;
      var x1 = b1.m_position;
      var x2 = b2.m_position;
      var p1 = joint.GetAnchor1();
      var p2 = joint.GetAnchor2();
      context.strokeStyle = '#00eeee';
      context.beginPath();
      switch (joint.m_type) {
      case b2Joint.e_distanceJoint:
          context.moveTo(p1.x, p1.y);
          context.lineTo(p2.x, p2.y);
          break;

      case b2Joint.e_pulleyJoint:
          // TODO
          break;

      default:
          if (b1 == world.m_groundBody) {
              context.moveTo(p1.x, p1.y);
              context.lineTo(x2.x, x2.y);
          }
          else if (b2 == world.m_groundBody) {
              context.moveTo(p1.x, p1.y);
              context.lineTo(x1.x, x1.y);
          }
          else {
              context.moveTo(x1.x, x1.y);
              context.lineTo(p1.x, p1.y);
              context.lineTo(x2.x, x2.y);
              context.lineTo(p2.x, p2.y);
          }
          break;
      }
      context.stroke();
  }

  function drawShape(shape, context) {
      context.strokeStyle = '#ffffff';
      if (shape.density == 1.0) {
          context.fillStyle = "red";
      } else {
          context.fillStyle = "black";
      }
      context.beginPath();
      switch (shape.m_type) {
      case b2Shape.e_circleShape:
          {
              var circle = shape;
              var pos = circle.m_position;
              var r = circle.m_radius;
              var segments = 16.0;
              var theta = 0.0;
              var dtheta = 2.0 * Math.PI / segments;

              // draw circle
              context.moveTo(pos.x + r, pos.y);
              for (var i = 0; i < segments; i++) {
                  var d = new b2Vec2(r * Math.cos(theta), r * Math.sin(theta));
                  var v = b2Math.AddVV(pos, d);
                  context.lineTo(v.x, v.y);
                  theta += dtheta;
              }
              context.lineTo(pos.x + r, pos.y);

              // draw radius
              context.moveTo(pos.x, pos.y);
              var ax = circle.m_R.col1;
              var pos2 = new b2Vec2(pos.x + r * ax.x, pos.y + r * ax.y);
              context.lineTo(pos2.x, pos2.y);
          }
          break;
      case b2Shape.e_polyShape:
          {
              var poly = shape;
              var tV = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[0]));
              context.moveTo(tV.x, tV.y);
              for (var i = 0; i < poly.m_vertexCount; i++) {
                  var v = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[i]));
                  context.lineTo(v.x, v.y);
              }
              context.lineTo(tV.x, tV.y);
          }
          break;
      }
      context.fill();
      context.stroke();
  }

  function createWorld() {
      var worldAABB = new b2AABB();
      worldAABB.minVertex.Set(-1000, -1000);
      worldAABB.maxVertex.Set(1000, 1000);
      var gravity = new b2Vec2(0, 300);
      var doSleep = true;
      world = new b2World(worldAABB, gravity, doSleep);
      createGround(world);
      return world;
  }

  function createGround(world) {
      var groundSd = new b2BoxDef();
      groundSd.extents.Set(400, 30);
      groundSd.restitution = 0.0;
      var groundBd = new b2BodyDef();
      groundBd.AddShape(groundSd);
      groundBd.position.Set(400, 470);
      return world.CreateBody(groundBd);
  }

  function createBall(world, x, y) {
      var ballSd = new b2CircleDef();
      ballSd.density = 1.0;
      ballSd.radius = 20;
      ballSd.restitution = 0.5;
      ballSd.friction = 0.5;
      var ballBd = new b2BodyDef();
      ballBd.AddShape(ballSd);
      ballBd.position.Set(x,y);
      return world.CreateBody(ballBd);
  }

  function createHelloWorld() {
      // H
      createBox(world, 50, 420, 10, 20, false);
      createBox(world, 90, 420, 10, 20, false);
      createBox(world, 70, 395, 30, 5, false);
      createBox(world, 50, 370, 10, 20, false);
      createBox(world, 90, 370, 10, 20, false);

      // E
      createBox(world, 140, 435, 30, 5, false);
      createBox(world, 120, 420, 10, 10, false);
      createBox(world, 130, 405, 20, 5, false);
      createBox(world, 120, 390, 10, 10, false);
      createBox(world, 140, 375, 30, 5, true);

      // L
      createBox(world, 200, 435, 20, 5, false);
      createBox(world, 185, 400, 5, 30, false);

      // L
      createBox(world, 250, 435, 20, 5, false);
      createBox(world, 235, 400, 5, 30, false);

      // O
      createBox(world, 300, 435, 20, 5, false);
      createBox(world, 285, 405, 5, 25, false);
      createBox(world, 315, 405, 5, 25, false);
      createBox(world, 300, 375, 20, 5, false);

      // W
      createBox(world, 390, 435, 40, 5, false);
      createBox(world, 360, 390, 10, 40, false);
      createBox(world, 420, 390, 10, 40, false);
      createBox(world, 390, 415, 5, 15, false);

      // O
      createBox(world, 460, 435, 20, 5, false);
      createBox(world, 445, 405, 5, 25, false);
      createBox(world, 475, 405, 5, 25, false);
      createBox(world, 460, 375, 20, 5, false);

      // R
      createBox(world, 495, 410, 5, 30, false);
      createBox(world, 518, 425, 5, 15, false);
      createBox(world, 515, 405, 15, 5, false);
      createBox(world, 525, 390, 5, 10, false);
      createBox(world, 510, 375, 20, 5, false);

      // L
      createBox(world, 560, 435, 20, 5, false);
      createBox(world, 545, 400, 5, 30, false);

      // D
      createBox(world, 610, 435, 20, 5, false);
      createBox(world, 595, 405, 5, 25, false);
      createBox(world, 625, 405, 5, 25, false);
      createBox(world, 610, 375, 20, 5, false);

      // !
      createBox(world, 650, 430, 10, 10, false);
      createBox(world, 650, 380, 10, 40, false);
  }

  function createBox(world, x, y, width, height, fixed) {
      if (typeof(fixed) == 'undefined') fixed = true;
      var boxSd = new b2BoxDef();
      if (!fixed) boxSd.density = 1.0;
      boxSd.restitution = 0.0;
      boxSd.friction = 1.0;
      boxSd.extents.Set(width, height);
      var boxBd = new b2BodyDef();
      boxBd.AddShape(boxSd);
      boxBd.position.Set(x,y);
      return world.CreateBody(boxBd);
  }

  function step(cnt) {
      var stepping = false;
      var timeStep = 1.0/60;
      var iteration = 1;
      world.Step(timeStep, iteration);
      ctx.clearRect(0, 0, canvasWidth, canvasHeight);
      drawWorld(world, ctx);
      setTimeout('step(' + (cnt || 0) + ')', 10);
  }

  // main entry point
  Event.observe(window, 'load', function() {
      world = createWorld();
      ctx = $('canvas').getContext('2d');
      var canvasElm = $('canvas');
      canvasWidth = parseInt(canvasElm.width);
      canvasHeight = parseInt(canvasElm.height);
      canvasTop = parseInt(canvasElm.style.top);
      canvasLeft = parseInt(canvasElm.style.left);

      createHelloWorld();

      Event.observe('canvas', 'click', function(e) {
              if (Math.random() > 0.5) {
                  //createBox(world, Event.pointerX(e), Event.pointerY(e), 10, 10, false);
                  createBox(world, e.clientX, e.clientY, 10, 10, false);
              } else {
                  createBall(world, Event.pointerX(e), Event.pointerY(e));
              }
      });
      step();
  });
  </script>
    </head>
    <body style="margin:0px;">
        <canvas id="canvas" width='800' height='500' style="background-color:#eeeeee;"></canvas>
    </body>
 </html>

The Breakdown

1
2
3
4
5
6
7
8
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>SavageLook.com - Box2D JS Hello World</title>
  <!--[if IE]><script type="text/javascript" src="lib/excanvas.js"></script><![endif]-->
  <script src="lib/prototype-1.6.0.2.js"></script>
  <script src="box2djs.min.js"></script>

We start by including the scripts necessary to make Box2D JS work. In order, we need excanvas (included in the Box2D JS distribution) in order to account for the fact that all current released version of Internet Explorer do not support the HTML canvas element. Next we include the Prototype Javascript framework, also included with Box2D JS. Finally we include my minified version of the library. Now we can get started building physics into our canvas element.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script type="text/javascript">
  var world;
  var ctx;
  var canvasWidth;
  var canvasHeight;
  var canvasTop;
  var canvasLeft;

  function drawWorld(world, context) {
      for (var j = world.m_jointList; j; j = j.m_next) {
          drawJoint(j, context);
      }
      for (var b = world.m_bodyList; b; b = b.m_next) {
          for (var s = b.GetShapeList(); s != null; s = s.GetNext()) {
              drawShape(s, context);
          }
      }

      ctx.font = 'bold 18px arial';
      ctx.textAlign = 'center';
      ctx.fillStyle = '#000000';
      ctx.fillText("Click the screen to add more objects", 400, 20);
      ctx.font = 'bold 14px arial';
      ctx.fillText("Performance will vary by browser", 400, 40);

  }

Here we declare our global variables that define the “world” the physics exist in and the context, dimensions, and position of the canvas element.

Also we have our drawWorld() function that will, as the name implies, draw the shapes and joints that compose the Box2D JS world. Each of these objects is iterated through and drawn individually. They are added to the world with the createBody() function. An important thing to note is that in the case of this demo, drawWorld() will be called with each “step”. Think of your canvas as an animation and each call to drawWorld() as a frame. Should be a simple concept for you Flash devs out there ;)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
function drawJoint(joint, context) {
  var b1 = joint.m_body1;
  var b2 = joint.m_body2;
  var x1 = b1.m_position;
  var x2 = b2.m_position;
  var p1 = joint.GetAnchor1();
  var p2 = joint.GetAnchor2();
  context.strokeStyle = '#00eeee';
  context.beginPath();
  switch (joint.m_type) {
  case b2Joint.e_distanceJoint:
      context.moveTo(p1.x, p1.y);
      context.lineTo(p2.x, p2.y);
      break;

  case b2Joint.e_pulleyJoint:
      // TODO
      break;

  default:
      if (b1 == world.m_groundBody) {
          context.moveTo(p1.x, p1.y);
          context.lineTo(x2.x, x2.y);
      }
      else if (b2 == world.m_groundBody) {
          context.moveTo(p1.x, p1.y);
          context.lineTo(x1.x, x1.y);
      }
      else {
          context.moveTo(x1.x, x1.y);
          context.lineTo(p1.x, p1.y);
          context.lineTo(x2.x, x2.y);
          context.lineTo(p2.x, p2.y);
      }
      break;
  }
  context.stroke();
}

function drawShape(shape, context) {
  context.strokeStyle = '#ffffff';
  if (shape.density == 1.0) {
      context.fillStyle = "red";
  } else {
      context.fillStyle = "black";
  }
  context.beginPath();
  switch (shape.m_type) {
  case b2Shape.e_circleShape:
      {
          var circle = shape;
          var pos = circle.m_position;
          var r = circle.m_radius;
          var segments = 16.0;
          var theta = 0.0;
          var dtheta = 2.0 * Math.PI / segments;

          // draw circle
          context.moveTo(pos.x + r, pos.y);
          for (var i = 0; i < segments; i++) {
              var d = new b2Vec2(r * Math.cos(theta), r * Math.sin(theta));
              var v = b2Math.AddVV(pos, d);
              context.lineTo(v.x, v.y);
              theta += dtheta;
          }
          context.lineTo(pos.x + r, pos.y);

          // draw radius
          context.moveTo(pos.x, pos.y);
          var ax = circle.m_R.col1;
          var pos2 = new b2Vec2(pos.x + r * ax.x, pos.y + r * ax.y);
          context.lineTo(pos2.x, pos2.y);
      }
      break;
  case b2Shape.e_polyShape:
      {
          var poly = shape;
          var tV = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[0]));
          context.moveTo(tV.x, tV.y);
          for (var i = 0; i < poly.m_vertexCount; i++) {
              var v = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[i]));
              context.lineTo(v.x, v.y);
          }
          context.lineTo(tV.x, tV.y);
      }
      break;
  }
  context.fill();
  context.stroke();
}

Above are the body drawing functions: drawJoint() and drawShape(). I’m not going to get into much detail here, but just know that these functions are responsible for taking the physics bodies and giving them a visual representation. They make calls to the canvas context 2D drawing API to create the shapes that give us our falling rectangles and circles. This is the simplest case and requires no external dependencies. In practical cases though, you will more likely find images or other more clever uses like this one:


1
2
3
4
5
6
7
8
9
10
function createWorld() {
  var worldAABB = new b2AABB();
  worldAABB.minVertex.Set(-1000, -1000);
  worldAABB.maxVertex.Set(1000, 1000);
  var gravity = new b2Vec2(0, 300);
  var doSleep = true;
  world = new b2World(worldAABB, gravity, doSleep);
  createGround(world);
  return world;
}

This is where the Box2D JS physics world is created. We define the bounds for the AABB physics with the minVertex and maxVertex properties and set the vector of gravity. After we create those we apply them to a newly created world and create the ground that will be the base of our scene.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function createGround(world) {
  var groundSd = new b2BoxDef();
  groundSd.extents.Set(400, 30);
  groundSd.restitution = 0.0;
  var groundBd = new b2BodyDef();
  groundBd.AddShape(groundSd);
  groundBd.position.Set(400, 470);
  return world.CreateBody(groundBd);
}

function createBall(world, x, y) {
  var ballSd = new b2CircleDef();
  ballSd.density = 1.0;
  ballSd.radius = 20;
  ballSd.restitution = 0.5;
  ballSd.friction = 0.5;
  var ballBd = new b2BodyDef();
  ballBd.AddShape(ballSd);
  ballBd.position.Set(x,y);
  return world.CreateBody(ballBd);
}

    function createBox(world, x, y, width, height, fixed) {
  if (typeof(fixed) == 'undefined') fixed = true;
  var boxSd = new b2BoxDef();
  if (!fixed) boxSd.density = 1.0;
  boxSd.restitution = 0.0;
  boxSd.friction = 1.0;
  boxSd.extents.Set(width, height);
  var boxBd = new b2BodyDef();
  boxBd.AddShape(boxSd);
  boxBd.position.Set(x,y);
  return world.CreateBody(boxBd);
}

Above we have the body create functions: createGround(), createBall(), and createBox(). Other than the obvious, let’s talk about a few things going on here. Each body is defined by a shape definition, and each shape definition has a number of properties that dictate how it will behave in the Box2D JS world (see the Box2D docs for details). Restitution, friction, and density affect how the shapes fall, move, and react.

The extents define the dimensions of the shapes, but probably not how you are accustomed. Extents represent the distance from one corner of the shape to its center. So a 100x100 box is actually defined by the extents shapeDef.extents.Set(50,50).

The shape definition is then used to define a body definition. The body is then positioned in the world. The positioning, like extents, is also based on the center of the body, not its corner. Finally, the newly defined body, based on the shape definition, is added the world with the CreateBody() function.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
function createHelloWorld() {
  // H
  createBox(world, 50, 420, 10, 20, false);
  createBox(world, 90, 420, 10, 20, false);
  createBox(world, 70, 395, 30, 5, false);
  createBox(world, 50, 370, 10, 20, false);
  createBox(world, 90, 370, 10, 20, false);

  // E
  createBox(world, 140, 435, 30, 5, false);
  createBox(world, 120, 420, 10, 10, false);
  createBox(world, 130, 405, 20, 5, false);
  createBox(world, 120, 390, 10, 10, false);
  createBox(world, 140, 375, 30, 5, true);

  // L
  createBox(world, 200, 435, 20, 5, false);
  createBox(world, 185, 400, 5, 30, false);

  // L
  createBox(world, 250, 435, 20, 5, false);
  createBox(world, 235, 400, 5, 30, false);

  // O
  createBox(world, 300, 435, 20, 5, false);
  createBox(world, 285, 405, 5, 25, false);
  createBox(world, 315, 405, 5, 25, false);
  createBox(world, 300, 375, 20, 5, false);

  // W
  createBox(world, 390, 435, 40, 5, false);
  createBox(world, 360, 390, 10, 40, false);
  createBox(world, 420, 390, 10, 40, false);
  createBox(world, 390, 415, 5, 15, false);

  // O
  createBox(world, 460, 435, 20, 5, false);
  createBox(world, 445, 405, 5, 25, false);
  createBox(world, 475, 405, 5, 25, false);
  createBox(world, 460, 375, 20, 5, false);

  // R
  createBox(world, 495, 410, 5, 30, false);
  createBox(world, 518, 425, 5, 15, false);
  createBox(world, 515, 405, 15, 5, false);
  createBox(world, 525, 390, 5, 10, false);
  createBox(world, 510, 375, 20, 5, false);

  // L
  createBox(world, 560, 435, 20, 5, false);
  createBox(world, 545, 400, 5, 30, false);

  // D
  createBox(world, 610, 435, 20, 5, false);
  createBox(world, 595, 405, 5, 25, false);
  createBox(world, 625, 405, 5, 25, false);
  createBox(world, 610, 375, 20, 5, false);

  // !
  createBox(world, 650, 430, 10, 10, false);
  createBox(world, 650, 380, 10, 40, false);
}

Here’s my addition to the Box2D JS code. By using a series of stacked boxes I create the infamous programmer’s first message, “Hello World!” (minus the comma, sorry). No science or mystery here, just a lot of extents and positions for the boxes that compose my message. And yes, I did cheat on the “E” and make it fixed, but not even I can defy the laws of physics for the sake of a code demo.


1
2
3
4
5
6
7
8
9
function step(cnt) {
  var stepping = false;
  var timeStep = 1.0/60;
  var iteration = 1;
  world.Step(timeStep, iteration);
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  drawWorld(world, ctx);
  setTimeout('step(' + (cnt || 0) + ')', 10);
}

The step() function is the what makes the whole thing work. step() is called over and over, at specified intervals, to create the animation of our scene on the canvas. The world’s Step() function is first called to apply one iteration of physics to our world’s bodies. Next we clear the visual representation of the scene so that it can be redrawn by our drawWorld() function. Finally we set the interval timer so that step() will be called again. Again, Flash devs will recognize this as a similar methodology as using the ENTER_FRAME event.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main entry point
Event.observe(window, 'load', function() {
  world = createWorld();
  ctx = $('canvas').getContext('2d');
  var canvasElm = $('canvas');
  canvasWidth = parseInt(canvasElm.width);
  canvasHeight = parseInt(canvasElm.height);
  canvasTop = parseInt(canvasElm.style.top);
  canvasLeft = parseInt(canvasElm.style.left);

  createHelloWorld();

  Event.observe('canvas', 'click', function(e) {
          if (Math.random() > 0.5) {
              //createBox(world, Event.pointerX(e), Event.pointerY(e), 10, 10, false);
              createBox(world, e.clientX, e.clientY, 10, 10, false);
          } else {
              createBall(world, Event.pointerX(e), Event.pointerY(e));
          }
  });
  step();
});
</script>

And this is where we kick everything off. We use the Prototype event handling mechanism to wait until the window is loaded to start our code. We first create the Box2D JS world and get the 2D context, dimensions, and position of our canvas. After that I create the boxes that make up the Hello World message. To finish up we listen for mouse clicks so that we can add more falling objects to the scene, since what good is a meticulously stacked Hello World if you can’t turn it into a pile of rubble? The first step() is kicked off and our scene is ready to go!


1
2
3
4
5
</head>
<body style="margin:0px;">
    <canvas id="canvas" width='800' height='500' style="background-color:#eeeeee;"></canvas>
</body>
 </html>

And to round out the breakdown, here’s the actual instance of the canvas element. Its a very simple container and allows for all the heavy lifting to be done in the Javascript. Just a reminder, the canvas element will NOT work in Internet Explorer unless you have the conditional check that includes excanvas if necessary, or if you happen to be test driving the IE9 beta.

The Summary

OK, well that turned out a lot longer and wordier than I was expecting, but 2D physics being utilized in HTML5 and Javascript is a simple topic. Hopefully, though, if you made it all the way through you have a much better understanding of how they all play together and how you will create Newtonian worlds of your own.

The difference in performance between different browsers is enough to drive you crazy. In my personal experience, Chrome > Firefox > IE. I know its early in the game and that IE 9 and Firefox 4 will likely be right up to par with Chrome, but its this lag and inconsistency across the board that has me spending most of my web development time in Flash and AS3.

Also, I had trouble making this work in all versions of IE, so if you run into any problems with a particular browser, please let me know.

NOTE: The concatenated version of Box2D JS is a convenience I offer so that you don’t have to do all the individual includes shown on the Box2D JS site’s demos. The minified version is that same concatenated version run through JSMin. Both versions are based on version 0.1.0 of Box2D JS. I am not a contributor to the Box2D JS project, just someone making it simpler to get into.