Impressing users with animation involves more than knowing how to move objects—you also need to know how to move them in ways that users expect. That requires understanding some common algorithms for mathbased movement and physics interactions. Simple movement based on points and vectors provides a foundation, and then it’s time to create objects that bounce off walls and one another with a bit of friction added to the mix. After that, we will step back and talk about movement that goes beyond straight lines: circles, spirals, and complex Bezier curves. We will then cover how adding gravity can affect movement. Finally, we will finish this chapter by discussing easing and how it can have a positive effect on mathbased animations.
The simplest kinds of animations—moving objects in a straight line up and down the
canvas—can take the form of adding a constant value to the x
or y
position of an object every time it is drawn.
So, to animate graphics, we will need to create an interval and then
call a function that will display our updated graphics on every frame.
Each example in this chapter will be built in a similar way. The first
step is to set up the necessary variables in our canvasApp()
function. For this first, basic
example of movement, we will create a variable named speed
. We will apply this value to the y
position of our object on every call to
drawScreen()
. The x
and y
variables set up the initial position of the object (a filled circle) that
we will move down the canvas:
var
speed
=
5
;
var
y
=
10
;
var
x
=
250
;
After we create the variables, we set up an interval to call the
drawScreen()
function every 20
milliseconds. This is the loop we need to update our objects and move them
around the canvas:
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
In the drawScreen()
function, we
update the value of y
by adding to it
the value of the speed
variable:
y
+=
speed
;
Finally, we draw our circle on the canvas. We position it using the
current values of x
and y
. Because y
is updated every time the function is called, the circle effectively moves
down the canvas:
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
x
,
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
To move the circle up the screen, we would make speed
a negative number. To move it left or
right, we would update the x
instead of
the y
variable. To move the circle
diagonally, we would update both x
and
y
at the same time.
Example 51 shows the complete code needed to create basic movement in a straight line.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX1: Moving In A Straight Line</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
// Create ball
y
+=
speed
;
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
x
,
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
var
speed
=
5
;
var
y
=
10
;
var
x
=
250
;
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
The basic structure of the HTML for all the examples in this chapter will follow these rules. In the interest of saving space, we will refrain from discussing this code further, but it will appear in the examples provided.
Movement based on constant changes to the x
or y
position of an object works well for some applications, but at other
times you will need to be more precise. One such instance is when you
need to move an object from point A to point B at a constant rate of
speed.
In mathematics, a common way to find the length of an unknown oline is to use the Pythagorean theorem:
A^{2} + B^{2} = C^{2} 
In this equation, C is the unknown side of a triangle when A and B are already known. However, we need to translate this equation into something that we can use with the points and pixels we have available on the canvas.
This is a good example of using a mathematical equation in your application. In this case, we want to find the distance of a line, given two points. In English, this equation reads like this:
The distance equals the square root of the square of the difference between the x value of the second point minus the x value of the first point, plus the square of the difference between the y value of the second point minus the y value of the first point.
You can see this in Figure 51. It’s much easier to understand in this format.
In the second example, we need to create some new variables in the
canvasApp()
function. We will still
use a speed
variable, just like in
the first example, but this time we set it to 5
, which means it will move 5
pixels on every call to drawScreen()
:
var
speed
=
5
;
We then create a couple of dynamic objects—each with an x
and a y
property—that will represent the two points we want to move between. For
this example, we will move our circle from 20
,250
to
480
,250
:
var
p1
=
{
x
:
20
,
y
:
250
};
var
p2
=
{
x
:
480
,
y
:
250
};
Now it is time to recreate the distance equation in Figure 51. The first step is to calculate the
differences between the second and first x
and y
points:
var
dx
=
p2
.
x

p1
.
x
;
var
dy
=
p2
.
y

p1
.
y
;
To determine the distance
value, we square both the values we just created, add them, and then use
the Math.sqrt()
function to get the
square root of the number:
var
distance
=
Math
.
sqrt
(
dx
*
dx
+
dy
*
dy
);
Next, we need to use that calculated distance
value in a way that will allow us to
move an object a uniform number of pixels from p1
to p2
.
The first thing we do is calculate how many moves
(calls to drawScreen()
) it will take the object to move
at the given value of speed
. We get
this by dividing the distance
by the
speed
:
var
moves
=
distance
/
speed
;
Then we find the distance to move both x
and y
on
each call to drawScreen()
. We name
these variables xunits
and yunits
:
var
xunits
=
(
p2
.
x

p1
.
x
)
/
moves
;
var
yunits
=
(
p2
.
y

p1
.
y
)
/
moves
;
Finally, we create a dynamic object named ball
that holds the x
and y
value of p1
:
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
};
And create the interval to call drawScreen()
every 33 milliseconds:
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
Let’s draw the ball on the screen. In the drawScreen()
function, we first check to see
whether the moves
variable is
greater than zero. If so, we are still supposed to move the ball
across the screen because we have not yet reached p2
. We decrement moves (moves
) and then update the x
and y
properties of the ball object by adding the xunits
to x
and yunits
to y
:
if
(
moves
>
0
)
{
moves

;
ball
.
x
+=
xunits
;
ball
.
y
+=
yunits
;
}
Now that our values have been updated, we simply draw the ball
at the x
and y
coordinates specified by the x
and y
properties, and we are done—that is, until drawScreen()
is called 33 milliseconds
later:
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
Let’s try the example by executing it in a web browser. You can
find it in the code distribution as CH5EX2.html
, or you can type in Example 52. Watch the ball move from one
point to another. If you update the x and
y values of each point, or change the speed,
watch the results. You can do a lot with this very simple
example.
For many of the examples in this chapter, we will create a way to trace an object’s movement on the canvas by drawing points to show its path. We have done this to help illustrate how objects move. However, in the real world, you would need to remove this functionality so that your application will perform to its potential. This is the only place we will discuss this code, so if you see it listed in any of the later examples in this chapter, refer back to this section to refresh your memory on its functionality.
First, we create an array in canvasApp()
to hold the set of points we will draw on the canvas:
var
points
=
new
Array
();
Next, we load a black 4×4 pixel image, point.png
, which we will use to display the
points on the canvas:
var
pointImage
=
new
Image
();
pointImage
.
src
=
"point.png"
;
Whenever we calculate a point for an object we will move, we
push that point into the points
array:
points
.
push
({
x
:
ball
.
x
,
y
:
ball
.
y
});
On each call to drawScreen()
,
we draw the set of points we have put into the points
array. Remember, we have to redraw
every point each time because the canvas is an immediatemode display
surface that does not retain any information about the images drawn
onto it:
for
(
var
i
=
0
;
i
<
points
.
length
;
i
++
)
{
context
.
drawImage
(
pointImage
,
points
[
i
].
x
,
points
[
i
].
y
,
1
,
1
);
}
In Figure 52,
you can see what the ball looks like when moving on a line from one
point to another and also what the points
path looks like when it is
drawn.
This is the only time in this chapter where we will discuss
the points
path in depth. If you
see the points being drawn, you will know how and why we have added
that functionality. You should also have enough information to
remove the code when necessary.
Example 52 is the full code
listing for CH5EX2.html
.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX2: Moving On A Simple Line</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
var
pointImage
=
new
Image
();
pointImage
.
src
=
"point.png"
;
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
// Create ball
if
(
moves
>
0
)
{
moves

;
ball
.
x
+=
xunits
;
ball
.
y
+=
yunits
;
}
//Draw points to illustrate path
points
.
push
({
x
:
ball
.
x
,
y
:
ball
.
y
});
for
(
var
i
=
0
;
i
<
points
.
length
;
i
++
)
{
context
.
drawImage
(
pointImage
,
points
[
i
].
x
,
points
[
i
].
y
,
1
,
1
);
}
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
var
speed
=
5
;
var
p1
=
{
x
:
20
,
y
:
250
};
var
p2
=
{
x
:
480
,
y
:
250
};
var
dx
=
p2
.
x

p1
.
x
;
var
dy
=
p2
.
y

p1
.
y
;
var
distance
=
Math
.
sqrt
(
dx
*
dx
+
dy
*
dy
);
var
moves
=
distance
/
speed
;
var
xunits
=
(
p2
.
x

p1
.
x
)
/
moves
;
var
yunits
=
(
p2
.
y

p1
.
y
)
/
moves
;
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
};
var
points
=
new
Array
();
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
Moving between two points is handy, but sometimes you don’t have
a point to move to, only a point to start from. In cases like this, it
can be very useful to create a vector
as a means to move your object.
A vector is a quantity in physics that has
both magnitude and direction. For our purposes, the magnitude will be
the speed
value of the moving object,
and the direction will be an angle
value that the object will move upon.
The good news is that moving on a vector is very similar to moving
between two points. In canvasApp()
,
we first set our speed
value
(magnitude). This is the number of pixels the object will move on every
call to drawScreen()
. We will set
this to 5
. We will also set the
starting point (p1
) for the object to
20
,20
:
var
speed
=
5
;
var
p1
=
{
x
:
20
,
y
:
20
};
Now, we will set the angle
value (direction) of movement for our object to 45
degrees. In mathematics, a flat, straight
line usually represents the 0 angle, which means a vector with an angle
of 45 degrees would be down and to the right on the canvas.
With our angle set, we now need to convert it to radians. Radians are a standard unit of angle measurement, and most mathematical calculations require you to convert an angle into radians before you can use it.
So why not just use radians and forget degrees altogether? Because
it is much easier to understand movement in degrees when working with
vectors and moving objects on a 2D surface. While a circle has 360
degrees, it has just about 6 radians, which are calculated
counterclockwise. This might make perfect sense to mathematicians, but
to move objects on a computer screen, angles are much easier. Therefore,
we will work with angles, but we still need to convert our 45degree
angle into radians. We do that with a standard formula: radians = angle * Math.PI/ 180
. And in the code:
var
angle
=
45
;
var
radians
=
angle
*
Math
.
PI
/
180
;
Before we can discuss how we calculate the movement of our object
along our vector, we need to review a couple trigonometric concepts.
These are cosine and sine, and
both relate to the arc created by our angle
(now converted to radians
), if it was drawn outward from the
center of the circle.
cosine
The angle measured counterclockwise from the xaxis
(x
)
sine
The vertical coordinate of the arc endpoint (y
)
You can see how these values relate to a 45degree angle in Figure 53.
This might seem complicated, but there is a very simple way to
think about it: cosine usually deals with the x
value, and sine usually deals with the
y
value. We can use sine and cosine
to help us calculate movement along our vector.
To calculate the number of pixels to move our object on each call
to drawScreen()
(xunits
and yunits
), we use the radians
(direction) we calculated and speed
(magnitude), along with the Math.cos()
(cosine)
and Math.sin()
(sine) functions of
the JavaScript Math
object:
var
xunits
=
Math
.
cos
(
radians
)
*
speed
;
var
yunits
=
Math
.
sin
(
radians
)
*
speed
;
In drawScreen()
, we simply add
xunits
and yunits
to ball.x
and ball.y
:
ball
.
x
+=
xunits
;
ball
.
y
+=
yunits
;
We don’t check to see whether moves
has been exhausted because we are not
moving to a particular point—we are simply moving along the vector,
seemingly forever. In the next section, we will explore what we can do
if we want the moving object to change direction when it hits something,
such as a wall.
Figure 54 shows what Example 53 looks like when it is executed in a web browser. Recall that the points are drawn for illustration only.
Example 53 gives the full code listing.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX3: Moving On A Vector</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
var
pointImage
=
new
Image
();
pointImage
.
src
=
"point.png"
;
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
ball
.
x
+=
xunits
;
ball
.
y
+=
yunits
;
//Draw points to illustrate path
points
.
push
({
x
:
ball
.
x
,
y
:
ball
.
y
});
for
(
var
i
=
0
;
i
<
points
.
length
;
i
++
)
{
context
.
drawImage
(
pointImage
,
points
[
i
].
x
,
points
[
i
].
y
,
1
,
1
);
}
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
var
speed
=
5
;
var
p1
=
{
x
:
20
,
y
:
20
};
var
angle
=
45
;
var
radians
=
angle
*
Math
.
PI
/
180
;
var
xunits
=
Math
.
cos
(
radians
)
*
speed
;
var
yunits
=
Math
.
sin
(
radians
)
*
speed
;
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
};
var
points
=
new
Array
();
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
While it’s neat that we can create a vector with magnitude and direction and then move an object along it infinitely, it’s probably not something you will need to do all that often. Most of the time, you will want to see that object react to the world around it by bouncing off horizontal and vertical walls, for example.
To help you understand how to do this, there is a simple rule in physics. Although this rule is usually applied to rays of light, it can be very useful when animating 2D objects—especially when they are bouncing off horizontal and vertical walls. This rule is known as the angle of reflection:
The angle of incidence is equal to the angle of reflection.
The angle of incidence is the angle an object is traveling when it hits the walls, and the angle of reflection is the angle it travels after it bounces off the wall.
Figure 55 illustrates that when an object hits a wall on a line that forms a 45degree angle with a perpendicular line drawn to the point of impact, it will bounce off (reflect) at a similar 45degree angle.
In the next section, we will create a series of examples using this rule to animate objects. The first, Example 54, will simply allow a single ball to bounce off the edges of the canvas.
In this first example, we will create a ball traveling on a vector. We will set the
speed
(magnitude) to 5
and the angle
(direction) to 35
degrees. The rest of the variables are
identical to those in Example 53. We are
still moving on a vector, but now we will test to see whether the ball
hits a “wall” (the edges of the canvas), in which case it will bounce
off, using the rule of the angle of reflection. One big change from the
previous vector example is the location in which we initialize the
values for radians
, xunits
, and yunits
. Instead of setting them up when we
initialize the application in canvasApp()
, we save that for a call to a new
function named updateBall()
:
var
speed
=
5
;
var
p1
=
{
x
:
20
,
y
:
20
};
var
angle
=
35
;
var
radians
=
0
;
var
xunits
=
0
;
var
yunits
=
0
;
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
};
updateBall
();
The updateBall()
function is
called every time we set a new angle
for the ball, because we need to recalculate the radians
and find new values for xunits
and yunits
. A new angle
value is generated when the app starts,
as well as every time the ball bounces off a wall:
function
updateBall
()
{
radians
=
angle
*
Math
.
PI
/
180
;
xunits
=
Math
.
cos
(
radians
)
*
speed
;
yunits
=
Math
.
sin
(
radians
)
*
speed
;
}
In drawScreen()
, we update the
position of the ball and then draw it on the canvas:
ball
.
x
+=
xunits
;
ball
.
y
+=
yunits
;
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
Next, we test to see whether the ball has hit a wall before we
draw it to the canvas. If the ball hits the right side (ball.x >
theCanvas.width
) or the left side (ball.x < 0
) of the canvas, we set the angle
to 180 degrees minus the angle of the vector on which the ball is
traveling. This gives us the angle of reflection. Alternatively, if the
ball hits the top (ball.y < 0
) or
bottom (ball.y > theCanvas.height
)
of the canvas, we calculate the angle of reflection as 360
degrees minus the angle of the vector on which the ball is
traveling:
if
(
ball
.
x
>
theCanvas
.
width

ball
.
x
<
0
)
{
angle
=
180

angle
;
updateBall
();
}
else
if
(
ball
.
y
>
theCanvas
.
height

ball
.
y
<
0
)
{
angle
=
360

angle
;
updateBall
();
}
That’s it. Example 54 demonstrates a ball that bounces off walls using the rules of physics. Figure 56 illustrates the code.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX4: Ball Bounce</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
ball
.
x
+=
xunits
;
ball
.
y
+=
yunits
;
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
if
(
ball
.
x
>
theCanvas
.
width

ball
.
x
<
0
)
{
angle
=
180

angle
;
updateBall
();
}
else
if
(
ball
.
y
>
theCanvas
.
height

ball
.
y
<
0
)
{
angle
=
360

angle
;
updateBall
();
}
}
function
updateBall
()
{
radians
=
angle
*
Math
.
PI
/
180
;
xunits
=
Math
.
cos
(
radians
)
*
speed
;
yunits
=
Math
.
sin
(
radians
)
*
speed
;
}
var
speed
=
5
;
var
p1
=
{
x
:
20
,
y
:
20
};
var
angle
=
35
;
var
radians
=
0
;
var
xunits
=
0
;
var
yunits
=
0
;
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
};
updateBall
();
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
The points on the line are not drawn when executed in the web browser because they slow down the ball far too much. We left them in Figure 56 to illustrate the angles of incidence and reflection.
One ball is cool, but what about 100? Is the code 100 times more
complicated? No, not at all. In fact, the code is only slightly more
complicated, but it is also more refined. Most programming tasks that
require only a single object of a type tend to allow you to be a bit
lazy. However, when you need to build an application that must support
n
number of objects, you need to make sure
the code will work in many different cases.
In the case of 100 balls bouncing on the canvas, we will need to
create a ball object with a few more properties. Recall that the ball
object we created previously had only x
and y
properties and looked like this:
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
};
All the other variables that represented the ball (speed
, angle
, xunits
, yunits
) were global in scope to the canvasApp()
. We used global variables because
we could get away with it. However, because we need to make sure
everything works the same way in this app, we make all those values
properties of each ball object.
For the multipleballbounce application, we will create an object
that holds all the pertinent information about each bouncing ball:
x
, y
, speed
,
angle
, xunits
, and yunits
.
Because we are going to create 100 balls of various sizes, we also add a
property named radius
, which
represents the size of the ball (well, half the size because it
is a radius):
tempBall
=
{
x
:
tempX
,
y
:
tempY
,
radius
:
tempRadius
,
speed
:
tempSpeed
,
angle
:
tempAngle
,
xunits
:
tempXunits
,
yunits
:
tempYunits
}
Inside canvasApp()
, we define
some new variables to help manage the multiple balls that will bounce
around the canvas:
numBalls
The number of balls to randomly create
maxSize
The maximum radius length for any given ball
minSize
The minimum radius length for any given ball
maxSpeed
The maximum speed any ball can travel
balls
An array to hold all of the ball objects we will create
The following code shows the newly defined variables:
var
numBalls
=
100
;
var
maxSize
=
8
;
var
minSize
=
5
;
var
maxSpeed
=
maxSize
+
5
;
var
balls
=
new
Array
();
We also create a set of temporary variables to hold the values for
each ball before we push it into the balls
array:
var
tempBall
;
var
tempX
;
var
tempY
;
var
tempSpeed
;
var
tempAngle
;
var
tempRadius
;
var
tempRadians
;
var
tempXunits
;
var
tempYunits
;
Next, in canvasApp()
, we
iterate through a loop to create all the ball objects. Notice how
tempX
and tempY
are created below. These values
represent the ball’s starting location on the canvas. We create a random
value for each, but we offset it by the size of the ball (tempRadius*2
). If we did not do that, some of
the balls would get “stuck” in a wall when the app starts because their
x
or y
location would be “through” the wall, but
their speed
would not be enough so
that a “bounce” would get them back on the playfield. They would be
stuck in bouncing limbo forever (which is kind of sad when you think
about it).
When you try this app, you will see that occasionally a ball still gets stuck in a wall. There is a further optimization we need to make to prevent this, but it is a bigger subject than this little iteration. We will talk about it in the section “Multiple Balls Bouncing and Colliding”.
The tempSpeed
variable is
created by subtracting the value of tempRadius
from the value of maxSpeed
, which we created earlier. The
speed
is not random, but it is
inversely proportional to the size (radius) of the ball. A larger ball
has a larger radius, so the value you subtract from tempSpeed
will be larger, thus making the ball
move more slowly.
When you run CH5EX5.html
in
your web browser, you will notice that this little trick makes the
ball appear more “real” because your brain expects larger objects to
move more slowly.
for
(
var
i
=
0
;
i
<
numBalls
;
i
++
)
{
tempRadius
=
Math
.
floor
(
Math
.
random
()
*
maxSize
)
+
minSize
;
tempX
=
tempRadius
*
2
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
width
)

tempRadius
*
2
);
tempY
=
tempRadius
*
2
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
height
)

tempRadius
*
2
);
tempSpeed
=
maxSpeed

tempRadius
;
tempAngle
=
Math
.
floor
(
Math
.
random
()
*
360
);
tempRadians
=
tempAngle
*
Math
.
PI
/
180
;
tempXunits
=
Math
.
cos
(
tempRadians
)
*
tempSpeed
;
tempYunits
=
Math
.
sin
(
tempRadians
)
*
tempSpeed
;
tempBall
=
{
x
:
tempX
,
y
:
tempY
,
radius
:
tempRadius
,
speed
:
tempSpeed
,
angle
:
tempAngle
,
xunits
:
tempXunits
,
yunits
:
tempYunits
}
balls
.
push
(
tempBall
);
}
Now we need to draw the balls onto the canvas. Inside drawScreen()
, the code to draw the balls
should look very familiar because it is essentially the same code we
used for one ball in Example 54. We just need to
loop through the balls
array to
render each ball object:
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
ball
.
x
+=
ball
.
xunits
;
ball
.
y
+=
ball
.
yunits
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
if
(
ball
.
x
>
theCanvas
.
width

ball
.
x
<
0
)
{
ball
.
angle
=
180

ball
.
angle
;
updateBall
(
ball
);
}
else
if
(
ball
.
y
>
theCanvas
.
height

ball
.
y
<
0
)
{
ball
.
angle
=
360

ball
.
angle
;
updateBall
(
ball
);
}
}
When you load Example 55 in your web
browser, you will see a bunch of balls all moving around the screen
independently, as shown in Figure 57. For the fun of
it, why not change the numBalls
variable to 500 or 1,000? What does the canvas look like then?
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX5: Multiple Ball Bounce</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
//Place balls
context
.
fillStyle
=
"#000000"
;
var
ball
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
ball
.
x
+=
ball
.
xunits
;
ball
.
y
+=
ball
.
yunits
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
if
(
ball
.
x
>
theCanvas
.
width

ball
.
x
<
0
)
{
ball
.
angle
=
180

ball
.
angle
;
updateBall
(
ball
);
}
else
if
(
ball
.
y
>
theCanvas
.
height

ball
.
y
<
0
)
{
ball
.
angle
=
360

ball
.
angle
;
updateBall
(
ball
);
}
}
}
function
updateBall
(
ball
)
{
ball
.
radians
=
ball
.
angle
*
Math
.
PI
/
180
;
ball
.
xunits
=
Math
.
cos
(
ball
.
radians
)
*
ball
.
speed
;
ball
.
yunits
=
Math
.
sin
(
ball
.
radians
)
*
ball
.
speed
;
}
var
numBalls
=
100
;
var
maxSize
=
8
;
var
minSize
=
5
;
var
maxSpeed
=
maxSize
+
5
;
var
balls
=
new
Array
();
var
tempBall
;
var
tempX
;
var
tempY
;
var
tempSpeed
;
var
tempAngle
;
var
tempRadius
;
var
tempRadians
;
var
tempXunits
;
var
tempYunits
;
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
for
(
var
i
=
0
;
i
<
numBalls
;
i
++
)
{
tempRadius
=
Math
.
floor
(
Math
.
random
()
*
maxSize
)
+
minSize
;
tempX
=
tempRadius
*
2
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
width
)

tempRadius
*
2
);
tempY
=
tempRadius
*
2
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
height
)

tempRadius
*
2
);
tempSpeed
=
maxSpeed

tempRadius
;
tempAngle
=
Math
.
floor
(
Math
.
random
()
*
360
);
tempRadians
=
tempAngle
*
Math
.
PI
/
180
;
tempXunits
=
Math
.
cos
(
tempRadians
)
*
tempSpeed
;
tempYunits
=
Math
.
sin
(
tempRadians
)
*
tempSpeed
;
tempBall
=
{
x
:
tempX
,
y
:
tempY
,
radius
:
tempRadius
,
speed
:
tempSpeed
,
angle
:
tempAngle
,
xunits
:
tempXunits
,
yunits
:
tempYunits
}
balls
.
push
(
tempBall
);
}
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
Before we move on to more complex interaction among balls, let’s try one more thing. Back in Chapter 3, we resized the canvas with some HTML5 form controls to display text in the center of the canvas. Well, let’s do the same thing now with the ball example. This will give you a better idea of how we can make objects interact with a dynamically resizing canvas.
First, in the HTML, we create two HTML5 range
controls,
one for width
and one for
height
, and set their maximum values
to 1000
. We will use these controls
to set the width and height of the canvas at runtime:
<form>
Canvas Width:<input
type=
"range"
id=
"canvasWidth"
min=
"0"
max=
"1000"
step=
"1"
value=
"500"
/>
<br>
Canvas Height:<input
type=
"range"
id=
"canvasHeight"
min=
"0"
max=
"1000"
step=
"1"
value=
"500"
/>
<br>
</form>
In canvasApp()
, we create the
event listeners for the HTML5 form controls. We listen for the change
event, which means that any time the
range
control is moved, the event
handlers will be called:
formElement
=
document
.
getElementById
(
"canvasWidth"
)
formElement
.
addEventListener
(
'change'
,
canvasWidthChanged
,
false
);
formElement
=
document
.
getElementById
(
"canvasHeight"
)
formElement
.
addEventListener
(
'change'
,
canvasHeightChanged
,
false
);
The event handler functions capture the changes to the range, set
theCanvas.width
or theCanvas.height
, and then call drawScreen()
to render the new size. Without a
call to drawScreen()
here, the canvas
will blink when the new size is applied in drawScreen()
on the next interval:
function
canvasWidthChanged
(
e
)
{
var
target
=
e
.
target
;
theCanvas
.
width
=
target
.
value
;
drawScreen
();
}
function
canvasHeightChanged
(
e
)
{
var
target
=
e
.
target
;
theCanvas
.
height
=
target
.
value
;
drawScreen
();
}
All of this is explained in gory detail in Chapter 3.
One last thing—let’s increase the number of balls set in canvasApp()
to 500
:
var
numBalls
=
500
;
Now, check out Example 56 (CH5EX6.html
from the code distribution). When
you run the code in a web browser, you should see 500 balls bounce
around the canvas, as shown in Figure 58. When you increase
the width or height using the range
controls, they continue moving until they hit the new edge of the
canvas. If you make the canvas smaller, the balls will be contained
within the smaller space. If you adjust the size too rapidly, some balls
will be lost off the canvas, but they will reappear when the canvas is
resized. Neat, huh?
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX6: Multiple Ball Bounce With Resize</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
formElement
=
document
.
getElementById
(
"canvasWidth"
)
formElement
.
addEventListener
(
'change'
,
canvasWidthChanged
,
false
);
formElement
=
document
.
getElementById
(
"canvasHeight"
)
formElement
.
addEventListener
(
'change'
,
canvasHeightChanged
,
false
);
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
//Place balls
context
.
fillStyle
=
"#000000"
;
var
ball
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
ball
.
x
+=
ball
.
xunits
;
ball
.
y
+=
ball
.
yunits
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
if
(
ball
.
x
>
theCanvas
.
width

ball
.
x
<
0
)
{
ball
.
angle
=
180

ball
.
angle
;
updateBall
(
ball
);
}
else
if
(
ball
.
y
>
theCanvas
.
height

ball
.
y
<
0
)
{
ball
.
angle
=
360

ball
.
angle
;
updateBall
(
ball
);
}
}
}
function
updateBall
(
ball
)
{
ball
.
radians
=
ball
.
angle
*
Math
.
PI
/
180
;
ball
.
xunits
=
Math
.
cos
(
ball
.
radians
)
*
ball
.
speed
;
ball
.
yunits
=
Math
.
sin
(
ball
.
radians
)
*
ball
.
speed
;
}
var
numBalls
=
500
;
var
maxSize
=
8
;
var
minSize
=
5
;
var
maxSpeed
=
maxSize
+
5
;
var
balls
=
new
Array
();
var
tempBall
;
var
tempX
;
var
tempY
;
var
tempSpeed
;
var
tempAngle
;
var
tempRadius
;
var
tempRadians
;
var
tempXunits
;
var
tempYunits
;
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
for
(
var
i
=
0
;
i
<
numBalls
;
i
++
)
{
tempRadius
=
Math
.
floor
(
Math
.
random
()
*
maxSize
)
+
minSize
;
tempX
=
tempRadius
*
2
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
width
)

tempRadius
*
2
);
tempY
=
tempRadius
*
2
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
height
)

tempRadius
*
2
);
tempSpeed
=
maxSpeed

tempRadius
;
tempAngle
=
Math
.
floor
(
Math
.
random
()
*
360
);
tempRadians
=
tempAngle
*
Math
.
PI
/
180
;
tempXunits
=
Math
.
cos
(
tempRadians
)
*
tempSpeed
;
tempYunits
=
Math
.
sin
(
tempRadians
)
*
tempSpeed
;
tempBall
=
{
x
:
tempX
,
y
:
tempY
,
radius
:
tempRadius
,
speed
:
tempSpeed
,
angle
:
tempAngle
,
xunits
:
tempXunits
,
yunits
:
tempYunits
}
balls
.
push
(
tempBall
);
}
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
function
canvasWidthChanged
(
e
)
{
var
target
=
e
.
target
;
theCanvas
.
width
=
target
.
value
;
drawScreen
();
}
function
canvasHeightChanged
(
e
)
{
var
target
=
e
.
target
;
theCanvas
.
height
=
target
.
value
;
drawScreen
();
}
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
<form>
Canvas Width:<input
type=
"range"
id=
"canvasWidth"
min=
"0"
max=
"1000"
step=
"1"
value=
"500"
/>
<br>
Canvas Height:<input
type=
"range"
id=
"canvasHeight"
min=
"0"
max=
"1000"
step=
"1"
value=
"500"
/>
<br>
</form>
</div>
</body>
</html>
Now it’s time to step it up again. Testing balls bouncing off walls is one thing, but what about balls bouncing off one another? We will need to add some pretty intricate code to handle this type of interaction.
For this example, we are going to create an elastic collision, which means that the total kinetic energy of the objects is the same before and after the collision. This is known as the law of conservation of momentum (Newton’s third law). To do this, we will take the x and y velocities of two colliding balls, and draw a “line of action” between their centers. This is illustrated in Figure 59, which has been adapted from Jobe Makar and Ben Winiarczyk’s Macromedia’s Flash MX 2004 Game Design Demystified (Macromedia Press). Then we will create new x and y velocities for each ball based on this angle and the law of conservation of momentum.
To properly calculate conservation of momentum when balls
collide on the canvas, we need to add a new property: mass
. Mass is the measurement of how much a ball (or any object)
resists any change in its velocity. Because collisions tend to change
the velocity of objects, this is an important addition to the ball
objects we will use on the canvas.
We will work from the code we created for Example 56 (CH5EX6.html
). The first big change to that
code is to make sure the balls don’t randomly start on top of one
another. If we let them start in the same location, they would be
forever intertwined and would spin off into oblivion. To be honest, it
looks pretty cool when that happens, but that’s not the result we are
looking to achieve.
In canvasApp()
, we set a
variable named tempRadius
to
5
. We will use this value as the
radius for each ball we create. Next, we create another new variable
named placeOK
and set it to
false
. When this is equal to
true
, we know we have found a place
to put a ball that is not on top of another ball.
Next, we enter a while()
loop
that will continue to iterate as long as placeOK
is false
. Then, we set all the values for our
new ball object:
tempRadius
=
5
;
var
placeOK
=
false
;
while
(
!
placeOK
)
{
tempX
=
tempRadius
*
3
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
width
)

tempRadius
*
3
);
tempY
=
tempRadius
*
3
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
height
)

tempRadius
*
3
);
tempSpeed
=
4
;
tempAngle
=
Math
.
floor
(
Math
.
random
()
*
360
);
tempRadians
=
tempAngle
*
Math
.
PI
/
180
;
tempvelocityx
=
Math
.
cos
(
tempRadians
)
*
tempSpeed
;
tempvelocityy
=
Math
.
sin
(
tempRadians
)
*
tempSpeed
;
Now, we need to make a dynamic object out of the values we just
created and place that object into the tempBall
variable. This is where we create a
mass
property for each ball. Again,
we do this so that we can calculate the effect when the balls hit one
another. For all the balls in this example, the mass
will be the same—the value of tempRadius
. We do this because, in our 2D
environment, the relative size of each ball is a very simple way to
create a value for mass
. Because
the mass
and speed
of each ball will be the same, they
will affect each other in a similar way. Later we will show you what
happens when we create ball objects with different mass
values.
Finally, we create nextX
and
nextY
properties that are equal to
x
and y
. We will use these values as “look ahead”
properties to help alleviate collisions that occur “between” our
iterations, which lead to overlapping balls and other oddities:
tempBall
=
{
x
:
tempX
,
y
:
tempY
,
nextX
:
tempX
,
nextY
:
tempY
,
radius
:
tempRadius
,
speed
:
tempSpeed
,
angle
:
tempAngle
,
velocityx
:
tempvelocityx
,
velocityy
:
tempvelocityy
,
mass
:
tempRadius
};
Now that we have our new dynamic ball object represented by the
tempBall
variable, we will test to
see whether it can be placed at the tempX
and tempY
we randomly created for it. We will do
this with a call to a new function named canStartHere()
. If canStartHere()
returns true
, we drop out of the while()
loop; if not, we start all over
again:
placeOK
=
canStartHere
(
tempBall
);
}
The canStartHere()
function
is very simple. It looks through the ball
array, testing the new tempBall
against all existing balls to see
whether they overlap. If they do, the function returns false
; if not, it returns true
. To test the overlap, we have created
another new function: hitTestCircle()
:
function
canStartHere
(
ball
)
{
var
retval
=
true
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
if
(
hitTestCircle
(
ball
,
balls
[
i
]))
{
retval
=
false
;
}
}
return
retval
;
}
The hitTestCircle()
function
performs a circle/circle collisiondetection test to see
whether the two circles (each representing a ball) passed as
parameters to the function are touching. Because we have been tracking
the balls by the center x
and
y
of their location, this is quite
easy to calculate. First, the function finds the distance of the line
that connects the center of each circle. We do this using our old
friend the Pythagorean theorem
(A^{2}+B^{2} =
C^{2}). We use the nextx
and nexty
properties of the ball because we want
to test the collision before it occurs. (Again, if we test after by
using the current x
and y
locations, there is a good chance the
balls will get stuck together and spin out of control.) We then
compare that distance
value to the
sum of the radius of each ball. If the distance is less than or equal
to the sum of the radii, we have a collision. This is a very simple
and efficient way to test collisions, and it works especially well
with collisions among balls in 2D:
function
hitTestCircle
(
ball1
,
ball2
)
{
var
retval
=
false
;
var
dx
=
ball1
.
nextx

ball2
.
nextx
;
var
dy
=
ball1
.
nexty

ball2
.
nexty
;
var
distance
=
(
dx
*
dx
+
dy
*
dy
);
if
(
distance
<=
(
ball1
.
radius
+
ball2
.
radius
)
*
(
ball1
.
radius
+
ball2
.
radius
)
)
{
retval
=
true
;
}
return
retval
;
}
Figure 510 illustrates this code.
The next thing we want to do is simplify drawScreen()
by separating the code into
controllable functions. The idea here is that to test collisions
correctly, we need to make sure some of our calculations are done in a
particular order. We like to call this an updatecolliderender
cycle.
update()
Sets the nextx
and
nexty
properties of all the
balls in the balls
array.
testWalls()
Tests to see whether the balls have hit one of the walls.
collide()
Tests collisions among balls. If the balls collide,
updates nextx
and nexty
.
render()
Makes the x
and
y
properties for each ball
equal to nextx
and nexty
, respectively, and then draws
them to the canvas.
And here is the code:
function
drawScreen
()
{
update
();
testWalls
();
collide
();
render
();
}
The update()
function loops
through all the balls in the balls
array and updates the nextx
and nexty
properties with the x
and y
velocity for each ball. We don’t directly update x
and y
here, because we want to test collisions against walls and other balls
before they occur. We will use the nextx
and nexty
properties for this purpose:
function
update
()
{
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
ball
.
nextx
=
(
ball
.
x
+=
ball
.
velocityx
);
ball
.
nexty
=
(
ball
.
y
+=
ball
.
velocityy
);
}
}
We discussed the interactions between balls and walls in the
last example, but there is still one issue. Because we move the balls
by the x
and y
location of their center, the balls would
move halfway off the canvas before a bounce occurred. To fix this, we
add or subtract the radius
of the
ball
object, depending on which
walls we are testing. For the right side and bottom of the canvas, we
add the radius of the ball when we test the walls. In this way, the
ball will appear to bounce exactly when its edge hits a wall.
Similarly, we subtract the radius when we test the left side and the
top of the canvas so that the ball does not move off the side before
we make it bounce off a wall:
function
testWalls
()
{
var
ball
;
var
testBall
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
if
(
ball
.
nextx
+
ball
.
radius
>
theCanvas
.
width
)
{
ball
.
velocityx
=
ball
.
velocityx
*
−
1
;
ball
.
nextx
=
theCanvas
.
width

ball
.
radius
;
}
else
if
(
ball
.
nextx

ball
.
radius
<
0
)
{
ball
.
velocityx
=
ball
.
velocityx
*
−
1
;
ball
.
nextx
=
ball
.
radius
;
}
else
if
(
ball
.
nexty
+
ball
.
radius
>
theCanvas
.
height
)
{
ball
.
velocityy
=
ball
.
velocityy
*
−
1
;
ball
.
nexty
=
theCanvas
.
height

ball
.
radius
;
}
else
if
(
ball
.
nexty

ball
.
radius
<
0
)
{
ball
.
velocityy
=
ball
.
velocityy
*
−
1
;
ball
.
nexty
=
ball
.
radius
;
}
}
}
The collide()
function tests
to see whether any balls have hit another. This function uses two
nested loops, both iterating through the balls
array to ensure that we test each ball
against every other ball. We take the ball from the first loop of the
balls
array and place it into the
ball
variable. Then we loop through
balls
again, placing each ball in
the testBall
variable, one at a
time. When we have both ball
and
testBall
, we make sure that they
are not equal to one another. We do this because a ball will always
have a false positive collision if we test it against itself. When we
are sure that they are not the same ball, we call hitTestCircle()
to test for a collision. If
we find one, we call collideBalls()
, and then all heck breaks
loose. (OK, not really, but the balls do collide, and some really
interesting code gets executed.) See that code here:
function
collide
()
{
var
ball
;
var
testBall
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
for
(
var
j
=
i
+
1
;
j
<
balls
.
length
;
j
++
)
{
testBall
=
balls
[
j
];
if
(
hitTestCircle
(
ball
,
testBall
))
{
collideBalls
(
ball
,
testBall
);
}
}
}
}
So now we get to the most interesting code of this example. We are
going to update the properties of each ball so that they appear to
bounce off one another. Recall that we use the nextx
and nexty
properties because we want to make
sure to test where the balls will be after they are drawn—not where
they are right now. This helps keep the balls from overlapping in a
way that will make them stick together.
Sometimes the balls will still stick together. This is a common problem when creating collisions among balls. This happens when balls overlap one another before the collision test, and the reaction bounce is not enough to split them apart completely. We have made every attempt to optimize this function for the canvas, but we are sure further optimizations are possible.
The collideBalls()
function
takes two parameters: ball1
and
ball2
. Both parameters are the
ball
objects that we want to make
collide:
function
collideBalls
(
ball1
,
ball2
)
{
First, we need to calculate the difference between the center
points of each ball. We store this as dx
and dy
(difference x and difference
y). This should look familiar because we did
something similar when we tested for a collision between the balls.
The difference is that now we know they have collided, and we want to
know how that collision occurred:
var
dx
=
ball1
.
nextx

ball2
.
nextx
;
var
dy
=
ball1
.
nexty

ball2
.
nexty
;
To do this, we need to find the angle of the collision using the
Math.atan2()
function. This
function gives us the angle in radians of the collisions between the
two balls. This is the line of action or angle of collision. We need
this value so that we can determine how the balls will react when they
collide:
var
collisionAngle
=
Math
.
atan2
(
dy
,
dx
);
Next we calculate the velocity vector for each ball given the
x
and y
velocities that existed before the
collision occurred:
var
speed1
=
Math
.
sqrt
(
ball1
.
velocityx
*
ball1
.
velocityx
+
ball1
.
velocityy
*
ball1
.
velocityy
);
var
speed2
=
Math
.
sqrt
(
ball2
.
velocityx
*
ball2
.
velocityx
+
ball2
.
velocityy
*
ball2
.
velocityy
);
Then, we calculate angles (in radians) for each ball given its current velocities:
var
direction1
=
Math
.
atan2
(
ball1
.
velocityy
,
ball1
.
velocityx
);
var
direction2
=
Math
.
atan2
(
ball2
.
velocityy
,
ball2
.
velocityx
);
Next we need to rotate the vectors counterclockwise so that we can plug those values into the equation for conservation of momentum. Basically, we are taking the angle of collision and making it flat so that we can bounce the balls, similar to how we bounced balls off the sides of the canvas:
var
velocityx_1
=
speed1
*
Math
.
cos
(
direction1

collisionAngle
);
var
velocityy_1
=
speed1
*
Math
.
sin
(
direction1

collisionAngle
);
var
velocityx_2
=
speed2
*
Math
.
cos
(
direction2

collisionAngle
);
var
velocityy_2
=
speed2
*
Math
.
sin
(
direction2

collisionAngle
);
We take the mass
values of
each ball and update their x
and
y
velocities based on the law of
conservation of momentum. To find the final velocity for both balls,
we use the following formulas:
velocity1
=
((
mass1

mass2
)
*
velocity1
+
2
*
mass2
*
velocity2
)
/
mass1
+
mass2
velocity2
=
((
mass2

mass1
)
*
velocity2
+
2
*
mass1
*
velocity1
)
/
mass1
+
mass2
Actually, only the x
velocity
needs to be updated; the y
velocity
remains constant:
var
final_velocityx_1
=
((
ball1
.
mass

ball2
.
mass
)
*
velocityx_1
+
(
ball2
.
mass
+
ball2
.
mass
)
*
velocityx_2
)
/
(
ball1
.
mass
+
ball2
.
mass
);
var
final_velocityx_2
=
((
ball1
.
mass
+
ball1
.
mass
)
*
velocityx_1
+
(
ball2
.
mass

ball1
.
mass
)
*
velocityx_2
)
/
(
ball1
.
mass
+
ball2
.
mass
);
var
final_velocityy_1
=
velocityy_1
;
var
final_velocityy_2
=
velocityy_2
After we have our final velocities, we rotate our angles back again so that the collision angle is preserved:
ball1
.
velocityx
=
Math
.
cos
(
collisionAngle
)
*
final_velocityx_1
+
Math
.
cos
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_1
;
ball1
.
velocityy
=
Math
.
sin
(
collisionAngle
)
*
final_velocityx_1
+
Math
.
sin
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_1
;
ball2
.
velocityx
=
Math
.
cos
(
collisionAngle
)
*
final_velocityx_2
+
Math
.
cos
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_2
;
ball2
.
velocityy
=
Math
.
sin
(
collisionAngle
)
*
final_velocityx_2
+
Math
.
sin
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_2
;
Now we update nextx
and
nexty
for both balls so that we can
use those values in the render()
function—or for another collision:
ball1
.
nextx
=
(
ball1
.
nextx
+=
ball1
.
velocityx
);
ball1
.
nexty
=
(
ball1
.
nexty
+=
ball1
.
velocityy
);
ball2
.
nextx
=
(
ball2
.
nextx
+=
ball2
.
velocityx
);
ball2
.
nexty
=
(
ball2
.
nexty
+=
ball2
.
velocityy
);
}
If this is confusing to you, you are not alone. It took some serious effort for us to translate this code from other sources into a working example on HTML5 Canvas. The code here is based on Flash Lite Effort: Embedded Systems and Pervasive Computing Lab by Felipe Sampaio. It is also partly based on Jobe Makar and Ben Winiarczyk’s work in Macromedia Flash MX 2004 Game Design Demystified, and Keith Peters’ books on ActionScript animation.
Here is the full code listing for Example 57.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX7: Balls With Simple Interactions</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
update
();
testWalls
();
collide
();
render
();
}
function
update
()
{
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
ball
.
nextx
=
(
ball
.
x
+=
ball
.
velocityx
);
ball
.
nexty
=
(
ball
.
y
+=
ball
.
velocityy
);
}
}
function
testWalls
()
{
var
ball
;
var
testBall
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
if
(
ball
.
nextx
+
ball
.
radius
>
theCanvas
.
width
)
{
ball
.
velocityx
=
ball
.
velocityx
*
1
;
ball
.
nextx
=
theCanvas
.
width

ball
.
radius
;
}
else
if
(
ball
.
nextx

ball
.
radius
<
0
)
{
ball
.
velocityx
=
ball
.
velocityx
*
1
;
ball
.
nextx
=
ball
.
radius
;
}
else
if
(
ball
.
nexty
+
ball
.
radius
>
theCanvas
.
height
)
{
ball
.
velocityy
=
ball
.
velocityy
*
1
;
ball
.
nexty
=
theCanvas
.
height

ball
.
radius
;
}
else
if
(
ball
.
nexty

ball
.
radius
<
0
)
{
ball
.
velocityy
=
ball
.
velocityy
*
1
;
ball
.
nexty
=
ball
.
radius
;
}
}
}
function
render
()
{
var
ball
;
context
.
fillStyle
=
"#000000"
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
ball
.
x
=
ball
.
nextx
;
ball
.
y
=
ball
.
nexty
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
}
function
collide
()
{
var
ball
;
var
testBall
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
for
(
var
j
=
i
+
1
;
j
<
balls
.
length
;
j
++
)
{
testBall
=
balls
[
j
];
if
(
hitTestCircle
(
ball
,
testBall
))
{
collideBalls
(
ball
,
testBall
);
}
}
}
}
function
hitTestCircle
(
ball1
,
ball2
)
{
var
retval
=
false
;
var
dx
=
ball1
.
nextx

ball2
.
nextx
;
var
dy
=
ball1
.
nexty

ball2
.
nexty
;
var
distance
=
(
dx
*
dx
+
dy
*
dy
);
if
(
distance
<=
(
ball1
.
radius
+
ball2
.
radius
)
*
(
ball1
.
radius
+
ball2
.
radius
)
)
{
retval
=
true
;
}
return
retval
;
}
function
collideBalls
(
ball1
,
ball2
)
{
var
dx
=
ball1
.
nextx

ball2
.
nextx
;
var
dy
=
ball1
.
nexty

ball2
.
nexty
;
var
collisionAngle
=
Math
.
atan2
(
dy
,
dx
);
var
speed1
=
Math
.
sqrt
(
ball1
.
velocityx
*
ball1
.
velocityx
+
ball1
.
velocityy
*
ball1
.
velocityy
);
var
speed2
=
Math
.
sqrt
(
ball2
.
velocityx
*
ball2
.
velocityx
+
ball2
.
velocityy
*
ball2
.
velocityy
);
var
direction1
=
Math
.
atan2
(
ball1
.
velocityy
,
ball1
.
velocityx
);
var
direction2
=
Math
.
atan2
(
ball2
.
velocityy
,
ball2
.
velocityx
);
var
velocityx_1
=
speed1
*
Math
.
cos
(
direction1

collisionAngle
);
var
velocityy_1
=
speed1
*
Math
.
sin
(
direction1

collisionAngle
);
var
velocityx_2
=
speed2
*
Math
.
cos
(
direction2

collisionAngle
);
var
velocityy_2
=
speed2
*
Math
.
sin
(
direction2

collisionAngle
);
var
final_velocityx_1
=
((
ball1
.
mass

ball2
.
mass
)
*
velocityx_1
+
(
ball2
.
mass
+
ball2
.
mass
)
*
velocityx_2
)
/
(
ball1
.
mass
+
ball2
.
mass
);
var
final_velocityx_2
=
((
ball1
.
mass
+
ball1
.
mass
)
*
velocityx_1
+
(
ball2
.
mass

ball1
.
mass
)
*
velocityx_2
)
/
(
ball1
.
mass
+
ball2
.
mass
);
var
final_velocityy_1
=
velocityy_1
;
var
final_velocityy_2
=
velocityy_2
;
ball1
.
velocityx
=
Math
.
cos
(
collisionAngle
)
*
final_velocityx_1
+
Math
.
cos
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_1
;
ball1
.
velocityy
=
Math
.
sin
(
collisionAngle
)
*
final_velocityx_1
+
Math
.
sin
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_1
;
ball2
.
velocityx
=
Math
.
cos
(
collisionAngle
)
*
final_velocityx_2
+
Math
.
cos
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_2
;
ball2
.
velocityy
=
Math
.
sin
(
collisionAngle
)
*
final_velocityx_2
+
Math
.
sin
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_2
;
ball1
.
nextx
=
(
ball1
.
nextx
+=
ball1
.
velocityx
);
ball1
.
nexty
=
(
ball1
.
nexty
+=
ball1
.
velocityy
);
ball2
.
nextx
=
(
ball2
.
nextx
+=
ball2
.
velocityx
);
ball2
.
nexty
=
(
ball2
.
nexty
+=
ball2
.
velocityy
);
}
var
numBalls
=
200
;
var
maxSize
=
15
;
var
minSize
=
5
;
var
maxSpeed
=
maxSize
+
5
;
var
balls
=
new
Array
();
var
tempBall
;
var
tempX
;
var
tempY
;
var
tempSpeed
;
var
tempAngle
;
var
tempRadius
;
var
tempRadians
;
var
tempvelocityx
;
var
tempvelocityy
;
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
for
(
var
i
=
0
;
i
<
numBalls
;
i
++
)
{
tempRadius
=
5
;
var
placeOK
=
false
;
while
(
!
placeOK
)
{
tempX
=
tempRadius
*
3
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
width
)

tempRadius
*
3
);
tempY
=
tempRadius
*
3
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
height
)

tempRadius
*
3
);
tempSpeed
=
4
;
tempAngle
=
Math
.
floor
(
Math
.
random
()
*
360
);
tempRadians
=
tempAngle
*
Math
.
PI
/
180
;
tempvelocityx
=
Math
.
cos
(
tempRadians
)
*
tempSpeed
;
tempvelocityy
=
Math
.
sin
(
tempRadians
)
*
tempSpeed
;
tempBall
=
{
x
:
tempX
,
y
:
tempY
,
nextX
:
tempX
,
nextY
:
tempY
,
radius
:
tempRadius
,
speed
:
tempSpeed
,
angle
:
tempAngle
,
velocityx
:
tempvelocityx
,
velocityy
:
tempvelocityy
,
mass
:
tempRadius
};
placeOK
=
canStartHere
(
tempBall
);
}
balls
.
push
(
tempBall
);
}
function
canStartHere
(
ball
)
{
var
retval
=
true
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
if
(
hitTestCircle
(
ball
,
balls
[
i
]))
{
retval
=
false
;
}
}
return
retval
;
}
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
><canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
Now, when you execute Example 57 (CH5EX7.html
), you will see a bunch of balls
of the same size and mass bumping off of each other and the walls of
the canvas, as shown in Figure 510. When you
look at this demo, imagine all the ways you could modify it to do
different things. You could create balls with different masses and
different speeds, or even create balls that don’t move but simply
alter the direction of other balls that hit them. In Figure 511, we will take a
slightly different look at this same code and add some new properties
to make it more interesting.
If we want the balls to slow down and eventually stop, we need to add friction to Example 57. For our purposes, simple friction is a value we use to modify the velocity of our objects every time they are drawn to the canvas.
In canvasApp()
, we now want to
create balls of various sizes. In the previous example, the balls were
all the same size. It worked, but having balls of different sizes with
different masses will create more interesting effects. To do this, we
set minSize
to 3
and maxSize
to 12
, meaning that the radii for our balls will
range from 3
to 12
pixels. We also add a new property named
friction
. This is a global property,
so it will not be applied to each individual ball. We set it to .01
, which means our balls will degrade their
x
and y
velocities by .01
pixels per frame (every time drawScreen()
is called):
var
numBalls
=
50
;
var
maxSize
=
12
;
var
minSize
=
3
;
var
maxSpeed
=
maxSize
+
5
;
var
friction
=
.
01
;
We will now allow for various ball sizes. The mass of each ball
will be different, and balls will have different effects on one another
depending on their sizes. Recall that in Example 57 we needed a mass
property so that we could calculate
conservation of momentum when the balls collided. We are doing the same
thing here, but now the masses are different depending on the
size:
for
(
var
i
=
0
;
i
<
numBalls
;
i
++
)
{
tempRadius
=
Math
.
floor
(
Math
.
random
()
*
maxSize
)
+
minSize
;
In update()
, we apply the
friction
value by calculating the
product of the current velocity multiplied by friction and then
subtracting that value from the current velocity. We do this for both
the x
and y
velocities. Why must we do this instead of
simply subtracting the friction value from the x
and y
velocities? Because the x
and
y
velocities are not always
proportional to each other. If we simply subtract the friction, we might
alter the velocity unintentionally. Instead, we need to subtract a value
for the friction that is proportional to the velocity itself, and that
value is the product of the velocity multiplied by the friction
value. This method will give you a
smooth degradation of the velocity when the friction
value is applied:
function
update
()
{
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
//Friction
ball
.
velocityx
=
ball
.
velocityx

(
ball
.
velocityx
*
friction
);
ball
.
velocityy
=
ball
.
velocityy

(
ball
.
velocityy
*
friction
);
ball
.
nextx
=
(
ball
.
x
+=
ball
.
velocityx
);
ball
.
nexty
=
(
ball
.
y
+=
ball
.
velocityy
);
}
}
You can see the full version of this code by executing CH5EX8.html
from the code distribution, or by
typing in Example 58. You should notice that
the smaller balls have less of an effect on the larger balls when they
collide, and vice versa. Also, the balls slow down as they move, due to
the applied friction.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX8: Balls With Friction</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
update
();
testWalls
();
collide
();
render
();
}
function
update
()
{
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
//Friction
ball
.
velocityx
=
ball
.
velocityx

(
ball
.
velocityx
*
friction
);
ball
.
velocityy
=
ball
.
velocityy

(
ball
.
velocityy
*
friction
);
ball
.
nextx
=
(
ball
.
x
+=
ball
.
velocityx
);
ball
.
nexty
=
(
ball
.
y
+=
ball
.
velocityy
);
}
}
function
testWalls
()
{
var
ball
;
var
testBall
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
if
(
ball
.
nextx
+
ball
.
radius
>
theCanvas
.
width
)
{
ball
.
velocityx
=
ball
.
velocityx
*
1
;
ball
.
nextx
=
theCanvas
.
width

ball
.
radius
;
}
else
if
(
ball
.
nextx

ball
.
radius
<
0
)
{
ball
.
velocityx
=
ball
.
velocityx
*
1
;
ball
.
nextx
=
ball
.
radius
;
}
else
if
(
ball
.
nexty
+
ball
.
radius
>
theCanvas
.
height
)
{
ball
.
velocityy
=
ball
.
velocityy
*
1
;
ball
.
nexty
=
theCanvas
.
height

ball
.
radius
;
}
else
if
(
ball
.
nexty

ball
.
radius
<
0
)
{
ball
.
velocityy
=
ball
.
velocityy
*
1
;
ball
.
nexty
=
ball
.
radius
;
}
}
}
function
render
()
{
var
ball
;
context
.
fillStyle
=
"#000000"
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
ball
.
x
=
ball
.
nextx
;
ball
.
y
=
ball
.
nexty
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
}
function
collide
()
{
var
ball
;
var
testBall
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
ball
=
balls
[
i
];
for
(
var
j
=
i
+
1
;
j
<
balls
.
length
;
j
++
)
{
testBall
=
balls
[
j
];
if
(
hitTestCircle
(
ball
,
testBall
))
{
collideBalls
(
ball
,
testBall
);
}
}
}
}
function
hitTestCircle
(
ball1
,
ball2
)
{
var
retval
=
false
;
var
dx
=
ball1
.
nextx

ball2
.
nextx
;
var
dy
=
ball1
.
nexty

ball2
.
nexty
;
var
distance
=
(
dx
*
dx
+
dy
*
dy
);
if
(
distance
<=
(
ball1
.
radius
+
ball2
.
radius
)
*
(
ball1
.
radius
+
ball2
.
radius
)
)
{
retval
=
true
;
}
return
retval
;
}
function
collideBalls
(
ball1
,
ball2
)
{
var
dx
=
ball1
.
nextx

ball2
.
nextx
;
var
dy
=
ball1
.
nexty

ball2
.
nexty
;
var
collisionAngle
=
Math
.
atan2
(
dy
,
dx
);
var
speed1
=
Math
.
sqrt
(
ball1
.
velocityx
*
ball1
.
velocityx
+
ball1
.
velocityy
*
ball1
.
velocityy
);
var
speed2
=
Math
.
sqrt
(
ball2
.
velocityx
*
ball2
.
velocityx
+
ball2
.
velocityy
*
ball2
.
velocityy
);
var
direction1
=
Math
.
atan2
(
ball1
.
velocityy
,
ball1
.
velocityx
);
var
direction2
=
Math
.
atan2
(
ball2
.
velocityy
,
ball2
.
velocityx
);
var
velocityx_1
=
speed1
*
Math
.
cos
(
direction1

collisionAngle
);
var
velocityy_1
=
speed1
*
Math
.
sin
(
direction1

collisionAngle
);
var
velocityx_2
=
speed2
*
Math
.
cos
(
direction2

collisionAngle
);
var
velocityy_2
=
speed2
*
Math
.
sin
(
direction2

collisionAngle
);
var
final_velocityx_1
=
((
ball1
.
mass

ball2
.
mass
)
*
velocityx_1
+
(
ball2
.
mass
+
ball2
.
mass
)
*
velocityx_2
)
/
(
ball1
.
mass
+
ball2
.
mass
);
var
final_velocityx_2
=
((
ball1
.
mass
+
ball1
.
mass
)
*
velocityx_1
+
(
ball2
.
mass

ball1
.
mass
)
*
velocityx_2
)
/
(
ball1
.
mass
+
ball2
.
mass
);
var
final_velocityy_1
=
velocityy_1
;
var
final_velocityy_2
=
velocityy_2
;
ball1
.
velocityx
=
Math
.
cos
(
collisionAngle
)
*
final_velocityx_1
+
Math
.
cos
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_1
;
ball1
.
velocityy
=
Math
.
sin
(
collisionAngle
)
*
final_velocityx_1
+
Math
.
sin
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_1
;
ball2
.
velocityx
=
Math
.
cos
(
collisionAngle
)
*
final_velocityx_2
+
Math
.
cos
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_2
;
ball2
.
velocityy
=
Math
.
sin
(
collisionAngle
)
*
final_velocityx_2
+
Math
.
sin
(
collisionAngle
+
Math
.
PI
/
2
)
*
final_velocityy_2
;
ball1
.
nextx
=
(
ball1
.
nextx
+=
ball1
.
velocityx
);
ball1
.
nexty
=
(
ball1
.
nexty
+=
ball1
.
velocityy
);
ball2
.
nextx
=
(
ball2
.
nextx
+=
ball2
.
velocityx
);
ball2
.
nexty
=
(
ball2
.
nexty
+=
ball2
.
velocityy
);
}
var
numBalls
=
50
;
var
maxSize
=
12
;
var
minSize
=
3
;
var
maxSpeed
=
maxSize
+
5
;
var
balls
=
new
Array
();
var
tempBall
;
var
tempX
;
var
tempY
;
var
tempSpeed
;
var
tempAngle
;
var
tempRadius
;
var
tempRadians
;
var
tempvelocityx
;
var
tempvelocityy
;
var
friction
=
.
01
;
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
for
(
var
i
=
0
;
i
<
numBalls
;
i
++
)
{
tempRadius
=
Math
.
floor
(
Math
.
random
()
*
maxSize
)
+
minSize
;
var
placeOK
=
false
;
while
(
!
placeOK
)
{
tempX
=
tempRadius
*
3
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
width
)

tempRadius
*
3
);
tempY
=
tempRadius
*
3
+
(
Math
.
floor
(
Math
.
random
()
*
theCanvas
.
height
)

tempRadius
*
3
);
tempSpeed
=
maxSpeed

tempRadius
;
tempAngle
=
Math
.
floor
(
Math
.
random
()
*
360
);
tempRadians
=
tempAngle
*
Math
.
PI
/
180
;
tempvelocityx
=
Math
.
cos
(
tempRadians
)
*
tempSpeed
;
tempvelocityy
=
Math
.
sin
(
tempRadians
)
*
tempSpeed
;
tempBall
=
{
x
:
tempX
,
y
:
tempY
,
radius
:
tempRadius
,
speed
:
tempSpeed
,
angle
:
tempAngle
,
velocityx
:
tempvelocityx
,
velocityy
:
tempvelocityy
,
mass
:
tempRadius
*
8
,
nextx
:
tempX
,
nexty
:
tempY
};
placeOK
=
canStartHere
(
tempBall
);
}
balls
.
push
(
tempBall
);
}
function
canStartHere
(
ball
)
{
var
retval
=
true
;
for
(
var
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
if
(
hitTestCircle
(
ball
,
balls
[
i
]))
{
retval
=
false
;
}
}
return
retval
;
}
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
Figure 512 illustrates how this code will look in the browser.
Whew! Moving and colliding balls on vectors can create some cool effects. However, moving in straight lines is not the only way you might want to move objects. In this section, we will show you some ways to animate objects by using circles, spirals, and curves.
Uniform circular motion occurs when we move an object along the distinct radius of
a defined circle. When we know the radius, we can use our old friends cosine
and
sine
to find the x
and y
locations of the moving object. The equations to find the locations of
an object moving uniformly on a defined circle are as follows:
x
=
radius
*
cosine
(
angle
)
y
=
radius
*
sine
(
angle
)
We will create an example of uniform circular movement with a
circle that has a radius of 125
, with
its center position at 250
,250
on the canvas. We will move a ball along
that circle, starting at an angle
of
0
.
In canvasApp()
, we will define
this circle path as a dynamic object stored in the circle
variable. While this object defines the properties of a circle, we will
not actually draw this circle on the canvas; rather, it defines only the
path on which we will move our ball
object:
var
circle
=
{
centerX
:
250
,
centerY
:
250
,
radius
:
125
,
angle
:
0
}
var
ball
=
{
x
:
0
,
y
:
0
,
speed
:
.
1
};
In drawScreen()
, we will
incorporate the equations for uniform circular movement. To do this, we
will set the x
and y
properties of the ball
object to the products of the equations,
added to the center location of the circle path on the canvas (circle.centerX
, circle.centerY
):
ball
.
x
=
circle
.
centerX
+
Math
.
cos
(
circle
.
angle
)
*
circle
.
radius
;
ball
.
y
=
circle
.
centerY
+
Math
.
sin
(
circle
.
angle
)
*
circle
.
radius
;
We then add the speed of the ball to the angle of the circle path.
This effectively sets the ball to move to a new location the next time
drawScreen()
is called:
circle
.
angle
+=
ball
.
speed
;
Finally, we draw the ball onto the canvas:
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
You can see what the circle path looks like in Figure 513. We have drawn the points on the canvas to illustrate the circle path.
You can easily alter the location and size of the circle path by
altering the radius
, centerX
, and centerY
properties of the circle path
object.
Example 59 shows the code for CH5EX9.html
.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX9: Moving In A Circle</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
ball
.
x
=
circle
.
centerX
+
Math
.
cos
(
circle
.
angle
)
*
circle
.
radius
;
ball
.
y
=
circle
.
centerY
+
Math
.
sin
(
circle
.
angle
)
*
circle
.
radius
;
circle
.
angle
+=
ball
.
speed
;
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
var
radius
=
100
;
var
circle
=
{
centerX
:
250
,
centerY
:
250
,
radius
:
125
,
angle
:
0
}
var
ball
=
{
x
:
0
,
y
:
0
,
speed
:
.
1
};
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
There are many complicated ways to move an object on a spiral path. One such way would be to use the Fibonacci sequence, which describes a pattern seen in nature that appears to create perfect spirals. The Fibonacci sequence starts with the number 0, and continues with each subsequent number calculated as the sum of the two previous numbers in the sequence. Each subsequent rotation of the spiral is the sum of the two previous numbers (1, 2, 3, 5, 8, 13, 21, 34, 55, 89...). However, as you might imagine, the math used to create this sequence is quite involved, and it is also difficult to translate to object movement.
For our purposes, we can create a simple spiral by increasing the
radius of the circle path on each call to drawScreen()
. If we take the code from Example 59, we would add a radiusInc
variable, which we will use as the
value to add the radius movement path of the circle. We create this new
variable in canvasApp()
:
var
radiusInc
=
2
;
Then, in drawScreen()
, we add
the following code to increase the radius of the circle every time we
move the object:
circle
.
radius
+=
radiusInc
;
In Figure 514, you can see what the resulting spiral looks like. (To illustrate the path, this example includes the points.)
If you want a tighter spiral, decrease the value of radiusInc
. Conversely, if you want a wider
spiral, increase the value of radiusInc
.
Example 510 shows the
code for CH5EX10.html
from the code
distribution.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX10: Moving In A Simple Geometric Spiral</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
var
pointImage
=
new
Image
();
pointImage
.
src
=
"point.png"
;
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
ball
.
x
=
circle
.
centerX
+
Math
.
cos
(
circle
.
angle
)
*
circle
.
radius
;
ball
.
y
=
circle
.
centerY
+
Math
.
sin
(
circle
.
angle
)
*
circle
.
radius
;
circle
.
angle
+=
ball
.
speed
;
circle
.
radius
+=
radiusInc
;
//Draw points to illustrate path
points
.
push
({
x
:
ball
.
x
,
y
:
ball
.
y
});
for
(
var
i
=
0
;
i
<
points
.
length
;
i
++
)
{
context
.
drawImage
(
pointImage
,
points
[
i
].
x
,
points
[
i
].
y
,
1
,
1
);
}
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
15
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
var
radiusInc
=
2
;
var
circle
=
{
centerX
:
250
,
centerY
:
250
,
radius
:
2
,
angle
:
0
,
radiusInc
:
2
}
var
ball
=
{
x
:
0
,
y
:
0
,
speed
:
.
1
};
var
points
=
new
Array
();
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
Cubic Bezier curves can be used to define a movement path for an object. Pierre Bezier first popularized these curves in the 1960s. They are widely used in 2D vector graphics to define smooth curves for drawing, but they can also be used in animation to define a path for motion.
A cubic Bezier curve is created using four distinct points—p0
, p1
,
p2
, and p3
:
p0
The starting point of the curve. We will refer to these
x
and y
values as x0
and y0
.
p3
The ending point of the curve. We will refer to these
x
and y
values as x3
and y3
.
p1
and p2
The control points for the curve. The curve does
not pass through these points; instead, the equation
uses these points to determine the arc of the curve. We will refer
to these x
and y
values as x0
, x1
, x2
, x3
, y0
, y1
, y2
, and y3
.
The usage of the p1
and
p2
points is the biggest stumbling
block for understanding Bezier curves. The easiest way to understand
the relationship between these points and the curve is to draw them on
a bitmapped canvas, which we will do several times in this
chapter.
After you have the four points, you need to calculate six
coefficient values that you will use to find the x
and y
locations as you move an object on the curve. These coefficients are
known as ax
, bx
, cx
,
ay
, by
, and cy
.
They are calculated as follows:
cx
=
3
*
(
x1

x0
)
bx
=
3
*
(
x2

x1
)

cx
ax
=
x3

x0

cx

bx
cy
=
3
*
(
y1

y0
)
by
=
3
*
(
y2

y1
)

cy
ay
=
y3

y0

cy

by
After you’ve calculated the six coefficients, you can find the
x
and y
locations based on the changing t
value using the following equations. The
t
value represents movement over
time:
x
(
t
)
=
axt3
+
bxt2
+
cxt
+
x0
y
(
t
)
=
ayt3
+
byt2
+
cyt
+
y0
For our purposes, the t
value
will be increased by the speed
at
which we want the object to move. However, you will notice that this
value does not easily equate to the speed
values we used elsewhere in this
chapter. The reason is that the t
value was not created with movement over time for animation in mind. The
speed we specify must be smaller than 1
so that the movement on the curve will be
incremental enough for us to see it as part of the animation. For our
example, we will increase t
by a
speed of .01
so that we will see
100
points on the movement curve
(1/100 = .01). This is advantageous because we will know our object has
finished moving when the t
value is
equal to 1
.
For Example 511 (CH5EX11.html
), we will start by creating the
four points of the Bezier curve in the canvasApp()
function:
var
p0
=
{
x
:
60
,
y
:
10
};
var
p1
=
{
x
:
70
,
y
:
200
};
var
p2
=
{
x
:
125
,
y
:
295
};
var
p3
=
{
x
:
350
,
y
:
350
};
We then create a new ball
object with a couple differing properties from those in the other
examples in this chapter. The speed
is .01
, which means that the object
will move 100 points along the curve before it is finished. We start the
t
value at 0
, which means that the ball
will begin at p0
:
var
ball
=
{
x
:
0
,
y
:
0
,
speed
:
.
01
,
t
:
0
};
Next, in the drawScreen()
function, we calculate the Bezier curve coefficient values (ax
, bx
,
cx
, ay
, by
,
cy
) based on the four points
(p0
, p1
, p2
,
p3
):
var
cx
=
3
*
(
p1
.
x

p0
.
x
);
var
bx
=
3
*
(
p2
.
x

p1
.
x
)

cx
;
var
ax
=
p3
.
x

p0
.
x

cx

bx
;
var
cy
=
3
*
(
p1
.
y

p0
.
y
);
var
by
=
3
*
(
p2
.
y

p1
.
y
)

cy
;
var
ay
=
p3
.
y

p0
.
y

cy

by
;
Then we take our t
value and
use it with the coefficients to calculate the x
and y
values for the moving object. First, we get the t
value from the ball
object, and we store it locally so that
we can use it in our calculations:
var
t
=
ball
.
t
;
Next we add the speed
to the
t
value so that we can calculate the
next point on the Bezier path:
ball
.
t
+=
ball
.
speed
;
Then we use the t
value to
calculate the x
and y
values (xt
, yt
)
using the Bezier curve equations:
var
xt
=
ax
*
(
t
*
t
*
t
)
+
bx
*
(
t
*
t
)
+
cx
*
t
+
p0
.
x
;
var
yt
=
ay
*
(
t
*
t
*
t
)
+
by
*
(
t
*
t
)
+
cy
*
t
+
p0
.
y
;
We add the speed
to the
t
value of ball
and then check to see whether t
is greater than 1
. If so, we don’t increase it any further
because we have finished moving on the curve:
ball
.
t
+=
ball
.
speed
;
if
(
ball
.
t
>
1
)
{
ball
.
t
=
1
;
}
Finally, when we draw the ball
object on the canvas, we use the xt
and yt
values:
context
.
arc
(
xt
,
yt
,
5
,
0
,
Math
.
PI
*
2
,
true
);
Figure 515 shows what
Example 511 (CH5EX11.html
) looks like when it is executed
in a web browser. In addition to drawing the points of the path using
the points
array, we also draw the
four points of the Bezier curve. These illustrate the relationship of
the points to the curve itself. Notice that the curve does not pass
through p1
or p2
.
Example 511 gives the full
code listing for CH5EX11.html
,
including the code to draw the Bezier curve points on the canvas. You
can find that code in the drawScreen()
function following the //draw the points
comment.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX11: Moving On A Cubic Bezier Curve</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
var
pointImage
=
new
Image
();
pointImage
.
src
=
"point.png"
;
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
var
t
=
ball
.
t
;
var
cx
=
3
*
(
p1
.
x

p0
.
x
)
var
bx
=
3
*
(
p2
.
x

p1
.
x
)

cx
;
var
ax
=
p3
.
x

p0
.
x

cx

bx
;
var
cy
=
3
*
(
p1
.
y

p0
.
y
);
var
by
=
3
*
(
p2
.
y

p1
.
y
)

cy
;
var
ay
=
p3
.
y

p0
.
y

cy

by
;
var
xt
=
ax
*
(
t
*
t
*
t
)
+
bx
*
(
t
*
t
)
+
cx
*
t
+
p0
.
x
;
var
yt
=
ay
*
(
t
*
t
*
t
)
+
by
*
(
t
*
t
)
+
cy
*
t
+
p0
.
y
;
ball
.
t
+=
ball
.
speed
;
if
(
ball
.
t
>
1
)
{
ball
.
t
=
1
;
}
//draw the points
context
.
font
=
"10px sans"
;
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p0
.
x
,
p0
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"0"
,
p0
.
x

2
,
p0
.
y
+
2
);
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p1
.
x
,
p1
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"1"
,
p1
.
x

2
,
p1
.
y
+
2
);
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p2
.
x
,
p2
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"2"
,
p2
.
x

2
,
p2
.
y
+
2
);
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p3
.
x
,
p3
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"3"
,
p3
.
x

2
,
p3
.
y
+
2
);
//Draw points to illustrate path
points
.
push
({
x
:
xt
,
y
:
yt
});
for
(
var
i
=
0
;
i
<
points
.
length
;
i
++
)
{
context
.
drawImage
(
pointImage
,
points
[
i
].
x
,
points
[
i
].
y
,
1
,
1
);
}
context
.
closePath
();
//Draw circle moving
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
xt
,
yt
,
5
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
var
p0
=
{
x
:
60
,
y
:
10
};
var
p1
=
{
x
:
70
,
y
:
200
};
var
p2
=
{
x
:
125
,
y
:
295
};
var
p3
=
{
x
:
350
,
y
:
350
};
var
ball
=
{
x
:
0
,
y
:
0
,
speed
:
.
01
,
t
:
0
};
var
points
=
new
Array
();
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
Moving an image on a cubic Bezier curve path is just as easy as moving a circular drawing object, as we’ll demonstrate in the next two examples. Suppose you are making a game where bull’seyes move across the canvas and the player must shoot at them. You could use cubic Bezier curve paths to create new and interesting patterns for the bull’seyes to move along.
For this example, we first create a global variable named bullseye
, which we will use to hold the
bullseye.png
image that we will
load to display on the canvas:
var
bullseye
;
function
eventWindowLoaded
()
{
bullseye
=
new
Image
();
bullseye
.
src
=
"bullseye.png"
bullseye
.
onload
=
eventAssetsLoaded
;
}
In canvasApp()
, we will create
a different path for the curve from the one in the first example by
setting new values for p0
, p1
, p2
, and
p3
. Using these values, the bullseye
will move on a parabolalike path.
(Figure 516 shows the
path of the curve.)
var
p0
=
{
x
:
60
,
y
:
10
};
var
p1
=
{
x
:
150
,
y
:
350
};
var
p2
=
{
x
:
300
,
y
:
375
};
var
p3
=
{
x
:
400
,
y
:
20
};
We also need to create a player
object that represents the bull’seye on the canvas:
var
player
=
{
x
:
0
,
y
:
0
,
speed
:
.
01
,
t
:
0
};
In drawImage()
, after we
calculate t
, xt
, and yt
,
we draw the image on the canvas:
player
.
x
=
xt

bullseye
.
width
/
2
;
player
.
y
=
yt

bullseye
.
height
/
2
;
context
.
drawImage
(
bullseye
,
player
.
x
,
player
.
y
);
The rest of Example 512 works just like Example 511.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX12: Moving An Image</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
var
bullseye
;
function
eventWindowLoaded
()
{
bullseye
=
new
Image
();
bullseye
.
src
=
"bullseye.png"
bullseye
.
onload
=
eventAssetsLoaded
;
}
function
eventAssetsLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
var
pointImage
=
new
Image
();
pointImage
.
src
=
"point.png"
;
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
var
t
=
player
.
t
;
var
cx
=
3
*
(
p1
.
x

p0
.
x
)
var
bx
=
3
*
(
p2
.
x

p1
.
x
)

cx
;
var
ax
=
p3
.
x

p0
.
x

cx

bx
;
var
cy
=
3
*
(
p1
.
y

p0
.
y
);
var
by
=
3
*
(
p2
.
y

p1
.
y
)

cy
;
var
ay
=
p3
.
y

p0
.
y

cy

by
;
var
xt
=
ax
*
(
t
*
t
*
t
)
+
bx
*
(
t
*
t
)
+
cx
*
t
+
p0
.
x
;
var
yt
=
ay
*
(
t
*
t
*
t
)
+
by
*
(
t
*
t
)
+
cy
*
t
+
p0
.
y
;
player
.
t
+=
player
.
speed
;
if
(
player
.
t
>
1
)
{
player
.
t
=
1
;
}
//draw the points
context
.
font
=
"10px sans"
;
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p0
.
x
,
p0
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"0"
,
p0
.
x

2
,
p0
.
y
+
2
);
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p1
.
x
,
p1
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"1"
,
p1
.
x

2
,
p1
.
y
+
2
);
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p2
.
x
,
p2
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"2"
,
p2
.
x

2
,
p2
.
y
+
2
);
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p3
.
x
,
p3
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"3"
,
p3
.
x

2
,
p3
.
y
+
2
);
//Draw points to illustrate path
points
.
push
({
x
:
xt
,
y
:
yt
});
for
(
var
i
=
0
;
i
<
points
.
length
;
i
++
)
{
context
.
drawImage
(
pointImage
,
points
[
i
].
x
,
points
[
i
].
y
,
1
,
1
);
}
context
.
closePath
();
player
.
x
=
xt

bullseye
.
width
/
2
;
player
.
y
=
yt

bullseye
.
height
/
2
;
context
.
drawImage
(
bullseye
,
player
.
x
,
player
.
y
);
}
var
p0
=
{
x
:
60
,
y
:
10
};
var
p1
=
{
x
:
150
,
y
:
350
};
var
p2
=
{
x
:
300
,
y
:
375
};
var
p3
=
{
x
:
400
,
y
:
20
};
var
player
=
{
x
:
0
,
y
:
0
,
speed
:
.
01
,
t
:
0
};
var
points
=
new
Array
();
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
You can create some very interesting paths using the four points
in a cubic Bezier curve. One such effect is a loop. To create a loop,
you need to make sure the points form an X, with p0
diagonal from p1
, and p2
and p3
on an opposite diagonal from
the other two points. p0
and p3
must be closer to the center of the canvas
than p1
or p2
. The points we will use to create this
effect in Example 513 are as follows:
var
p0
=
{
x
:
150
,
y
:
440
;
var
p1
=
{
x
:
450
,
y
:
10
};
var
p2
=
{
x
:
50
,
y
:
10
};
var
p3
=
{
x
:
325
,
y
:
450
};
Because it is much easier to show than tell when it comes to cubic Bezier curves, look at Figure 517. It shows what the looping curve looks like when Example 513 is executed in a web browser.
This effect can be created only with the four points of a cubic Bezier curve. There is also a threepoint Bezier curve known as a quadratic Bezier curve. You cannot create loops or S curves with quadratic Bezier curves because the three points are not as precise as the four points of a cubic Bezier curve.
Because the code for this example is essentially the same as in Example 512 (besides the four points), we have highlighted in bold the changed code in Example 513. We have done this to show you that—with relatively simple changes—you can create dramatic animation effects using cubic Bezier curves.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX13: Bezier Curve Loop</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
var
bullseye
;
function
eventWindowLoaded
()
{
bullseye
=
new
Image
();
bullseye
.
src
=
"bullseye.png"
bullseye
.
onload
=
eventAssetsLoaded
;
}
function
eventAssetsLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
var
pointImage
=
new
Image
();
pointImage
.
src
=
"point.png"
;
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
var
t
=
player
.
t
;
var
cx
=
3
*
(
p1
.
x

p0
.
x
)
var
bx
=
3
*
(
p2
.
x

p1
.
x
)

cx
;
var
ax
=
p3
.
x

p0
.
x

cx

bx
;
var
cy
=
3
*
(
p1
.
y

p0
.
y
);
var
by
=
3
*
(
p2
.
y

p1
.
y
)

cy
;
var
ay
=
p3
.
y

p0
.
y

cy

by
;
var
xt
=
ax
*
(
t
*
t
*
t
)
+
bx
*
(
t
*
t
)
+
cx
*
t
+
p0
.
x
;
var
yt
=
ay
*
(
t
*
t
*
t
)
+
by
*
(
t
*
t
)
+
cy
*
t
+
p0
.
y
;
player
.
t
+=
player
.
speed
;
if
(
player
.
t
>
1
)
{
player
.
t
=
1
;
}
//draw the points
context
.
font
=
"10px sans"
;
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p0
.
x
,
p0
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"0"
,
p0
.
x

2
,
p0
.
y
+
2
);
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p1
.
x
,
p1
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"1"
,
p1
.
x

2
,
p1
.
y
+
2
);
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p2
.
x
,
p2
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"2"
,
p2
.
x

2
,
p2
.
y
+
2
);
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
p3
.
x
,
p3
.
y
,
8
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
context
.
fillStyle
=
"#FFFFFF"
;
context
.
fillText
(
"3"
,
p3
.
x

2
,
p3
.
y
+
2
);
points
.
push
({
x
:
xt
,
y
:
yt
});
for
(
var
i
=
0
;
i
<
points
.
length
;
i
++
)
{
context
.
drawImage
(
pointImage
,
points
[
i
].
x
,
points
[
i
].
y
,
1
,
1
);
}
context
.
closePath
();
player
.
x
=
xt

bullseye
.
width
/
2
;
player
.
y
=
yt

bullseye
.
height
/
2
;
context
.
drawImage
(
bullseye
,
player
.
x
,
player
.
y
);
}
var
p0
=
{
x
:
150
,
y
:
440
};
var
p1
=
{
x
:
450
,
y
:
10
};
var
p2
=
{
x
:
50
,
y
:
10
};
var
p3
=
{
x
:
325
,
y
:
450
};
var
player
=
{
x
:
0
,
y
:
0
,
speed
:
.
01
,
t
:
0
};
var
points
=
new
Array
();
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
;
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
Adding simulated gravity, elasticity, and friction to your objects adds a sense of realism that otherwise would not exist in 2D. These properties are major forces in nature that people feel and understand at nearly every moment of their lives. This means that people who play games expect objects to act in a particular way when these properties are applied. Our job is to simulate those effects as closely as possible, while minimizing the processing power necessary to create them. While there are some very complicated physics equations we could use to create these effects, we will use simplified versions that work well with the limited resources available to HTML5 Canvas in a web browser.
A very simple yet seemingly realistic gravitational effect can be
achieved by applying a constant gravity value to the y
velocity of an object moving on a vector. To
do this, select a value for gravity, such as .1
, and then add that value to the y
velocity of your object on every call to
drawScreen()
.
For this example, let’s simulate a ball with a radius
of 15
pixels being shot from a cannon that rests
near the bottom of the canvas. The ball will move at a speed
of 4
pixels per frame, with an angle
of
305
degrees. This means it will move
up and to the right on the canvas. If we did not apply any gravity, the
ball would simply keep moving on that vector until it left the canvas.
(Actually, it would keep moving; we just would not see it any
longer.)
You have seen the code to create an effect like this already. In
the canvasApp()
function, we would
create the starting variables like this:
var
speed
=
4
;
var
angle
=
305
;
var
radians
=
angle
*
Math
.
PI
/
180
;
var
radius
=
15
;
var
vx
=
Math
.
cos
(
radians
)
*
speed
;
var
vy
=
Math
.
sin
(
radians
)
*
speed
;
Next we create the starting point for the ball as p1
, and then we create a dynamic object that
holds all the values we created for the ball
object:
var
p1
=
{
x
:
20
,
y
:
theCanvas
.
height

radius
};
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
velocityx
:
vx
,
velocityy
:
vy
,
radius
:
radius
};
If we want to add gravity to the application, we would first
create a new variable named gravity
and set it to a constant value of .1
:
var
gravity
=
.
1
;
Next, in the drawScreen()
function, we apply this gravity value to the ball
object when it is drawn to the canvas
(ball.velocityy += gravity
). We want
the ball to stop moving when it hits the “ground” (the bottom of the
canvas), so we test to see whether the y
position of the ball
plus the radius
of the ball (the outer edge) has passed
the bottom of the canvas (ball.y + ball.radius
<= theCanvas.height
). If so, we stop the ball’s
movement:
if
(
ball
.
y
+
ball
.
radius
<=
theCanvas
.
height
)
{
ball
.
velocityy
+=
gravity
;
}
else
{
ball
.
velocityx
=
0
;
ball
.
velocityy
=
0
;
ball
.
y
=
theCanvas
.
height

ball
.
radius
;
}
Next we apply the constant x
velocity and the new y
velocity to
ball
and draw it to the
canvas:
ball
.
y
+=
ball
.
velocityy
;
ball
.
x
+=
ball
.
velocityx
;
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
Figure 518 shows what the path looks like when simple gravity is applied to a ball moving on a vector. We have added the points to illustrate the path.
You can test out Example 514 with the
file CH5EX14.html
in the code
distribution, or type in the full code listing below.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX14: Simple Gravity</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
if
(
ball
.
y
+
ball
.
radius
<=
theCanvas
.
height
)
{
ball
.
velocityy
+=
gravity
;
}
else
{
ball
.
velocityx
=
0
;
ball
.
velocityy
=
0
;
ball
.
y
=
theCanvas
.
height

ball
.
radius
;
}
ball
.
y
+=
ball
.
velocityy
;
ball
.
x
+=
ball
.
velocityx
;
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
var
speed
=
4
;
var
gravity
=
.
1
;
var
angle
=
305
;
var
radians
=
angle
*
Math
.
PI
/
180
;
var
radius
=
15
;
var
vx
=
Math
.
cos
(
radians
)
*
speed
;
var
vy
=
Math
.
sin
(
radians
)
*
speed
;
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
var
p1
=
{
x
:
20
,
y
:
theCanvas
.
height

radius
};
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
velocityx
:
vx
,
velocityy
:
vy
,
radius
:
radius
};
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
The last example showed what a cannonball might look like if it were shot out, landed on a surface, and stuck there with no reaction. However, even a heavy cannonball will bounce when it hits the ground.
To create a bouncing effect, we do not have to change the code
very much at all. In drawScreen()
, we
first apply gravity
on every frame;
then, instead of stopping the ball if it hits the bottom of the canvas,
we simply need to reverse the y
velocity of ball
when it hits the
ground.
In CH5EX14.html
you would
replace this code:
if
(
ball
.
y
+
ball
.
radius
<=
theCanvas
.
height
)
{
ball
.
velocityy
+=
gravity
;
}
else
{
ball
.
velocityx
=
0
;
ball
.
velocityy
=
0
;
ball
.
y
=
theCanvas
.
height

ball
.
radius
;
}
With this:
ball
.
velocityy
+=
gravity
;
if
((
ball
.
y
+
ball
.
radius
)
>
theCanvas
.
height
)
{
ball
.
velocityy
=

(
ball
.
velocityy
)
}
This code will send the ball bouncing back “up” the canvas.
Because it is still traveling on the vector and gravity is applied every
time drawScreen()
is called, the ball
will eventually come down again as the applied gravity
overtakes the reversed y
velocity.
Figure 519 shows what the cannonball looks like when the bounce is applied.
To achieve a nicelooking bounce for this example, we also
changed the angle
of the vector in
canvasApp()
to 295
:
var
angle
=
295
;
Example 515 offers the full code.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX15: Gravity With A Bounce</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
ball
.
velocityy
+=
gravity
;
if
((
ball
.
y
+
ball
.
radius
)
>
theCanvas
.
height
)
{
ball
.
velocityy
=

(
ball
.
velocityy
)
}
ball
.
y
+=
ball
.
velocityy
;
ball
.
x
+=
ball
.
velocityx
;
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
var
speed
=
5
;
var
gravity
=
.
1
;
var
angle
=
295
;
var
radians
=
angle
*
Math
.
PI
/
180
;
var
radius
=
15
;
var
vx
=
Math
.
cos
(
radians
)
*
speed
;
var
vy
=
Math
.
sin
(
radians
)
*
speed
;
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
var
p1
=
{
x
:
20
,
y
:
theCanvas
.
height

radius
};
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
velocityx
:
vx
,
velocityy
:
vy
,
radius
:
radius
};
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
In physics, the elasticity of a bouncing ball refers to how much energy is conserved when a ball bounces off a surface. We already covered a bit about conservation of energy when we discussed balls colliding, but when we are simulating objects falling, we need to take a slightly different path with our code. In Example 515, we applied 100% elasticity and the ball bounced forever. (Actually, this was only implied because we did not consider elasticity at all.) However, in real life, balls usually lose some of their energy every time they bounce off a surface. The amount of energy conserved depends on the material the ball is made from, as well as the surface it is bouncing on. For example, a rubber Super Ball is much more elastic than a cannonball and will bounce higher on the first bounce off a surface. Both will bounce higher off a concrete surface than a surface made of thick mud. Eventually, both will come to rest on the surface as all the energy is transferred away from the ball.
We can simulate simple elasticity by applying a constant value to
the ball when it bounces off the ground. For this example, we will set
the speed
of the ball to 6
pixels per frame and the angle
to 285
. We will keep our gravity
at .1
but set a new variable named elasticity
to .5
. To make this more straightforward, we will
also assume that the surface the ball is bouncing on does not add or
subtract from the elasticity of the ball.
In canvasApp()
, we would set
the new properties like this:
var
speed
=
6
;
var
gravity
=
.
1
;
var
elasticity
=
.
5
;
var
angle
=
285
;
We then add the new elasticity
property to the ball
object because,
unlike gravity
, elasticity describes
a property of an object, not the entire world it resides within. This
means that having multiple balls with different values for elasticity
would be very easy to implement:
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
velocityx
:
vx
,
velocityy
:
vy
,
radius
:
radius
,
elasticity
:
elasticity
};
In the drawScreen()
function,
we still add the gravity
value to the
y
velocity (velocityy
). However, instead of simply
reversing the y
velocity when the
ball
hits the bottom of the canvas,
we also multiply the y
velocity by
the elasticity
value stored in the
ball.elasticity
property. This
applies the elasticity to the bounce, preserving the y
velocity by the percentage value of elasticity
for the object:
ball
.
velocityy
+=
gravity
;
if
((
ball
.
y
+
ball
.
radius
)
>
theCanvas
.
height
)
{
ball
.
velocityy
=

(
ball
.
velocityy
)
*
ball
.
elasticity
;
}
ball
.
y
+=
ball
.
velocityy
;
ball
.
x
+=
ball
.
velocityx
;
In Figure 520, you can see what this application looks like when executed in a web browser.
With gravity
applied, the
bounce is not exactly as you might expect. Gravity is always pulling
down on our object, so the effect of a loss of y
velocity due to an elastic bounce is
pronounced.
The full code is shown in Example 516.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX16: Gravity With A Vector With Bounce And Elasticity</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
ball
.
velocityy
+=
gravity
;
if
((
ball
.
y
+
ball
.
radius
)
>
theCanvas
.
height
)
{
ball
.
velocityy
=

(
ball
.
velocityy
)
*
ball
.
elasticity
;
}
ball
.
y
+=
ball
.
velocityy
;
ball
.
x
+=
ball
.
velocityx
;
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
var
speed
=
6
;
var
gravity
=
.
1
;
var
elasticity
=
.
5
;
var
angle
=
285
;
var
radians
=
angle
*
Math
.
PI
/
180
;
var
radius
=
15
;
var
vx
=
Math
.
cos
(
radians
)
*
speed
;
var
vy
=
Math
.
sin
(
radians
)
*
speed
;
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
var
p1
=
{
x
:
20
,
y
:
theCanvas
.
height

radius
};
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
velocityx
:
vx
,
velocityy
:
vy
,
radius
:
radius
,
elasticity
:
elasticity
};
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
Now that we have a ball traveling on a vector that is affected by both
gravity and elasticity, we have one more element to add to make the
animation more realistic. In the previous example, the y
velocity was affected by gravity and
elasticity, but the ball still traveled on the xaxis without any
degradation in velocity. We will fix this issue now by adding friction
into the equation.
In physics, friction is a force that resists the motion of an object. We have
already discussed friction as it applies to colliding balls, and this
implementation is similar except that it affects only the x
velocity. For our purposes, we will achieve
simple friction by degrading the x
velocity as gravity degrades the y
velocity.
Taking the code from Example 516, in canvasApp()
we create a new variable named
friction
. This is the amount of
pixels to degrade the x
velocity on
every frame:
var
friction
=
.
008
;
Notice that the amount is quite small. Friction does not have to
be a large value to look realistic—it just needs to be applied uniformly
each time drawScreen()
is called. In
drawScreen()
, we apply friction
to the x
velocity like this:
ball
.
velocityx
=
ball
.
velocityx

(
ball
.
velocityx
*
friction
);
This is the same type of proportional application of friction we
used with the colliding balls, but again, this time we applied it only
to the x
velocity.
Figure 521 shows what this final version of our application looks like when executed in a web browser.
Example 517 gives
the full code for CH5EX17.html
, the
final code of our simple gravity, simple elasticity, and simple friction
example.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX17: Gravity With A Vector With Bounce Friction</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
ball
.
velocityx
=
ball
.
velocityx

(
ball
.
velocityx
*
friction
);
ball
.
velocityy
+=
gravity
;
if
((
ball
.
y
+
ball
.
radius
)
>
theCanvas
.
height
)
{
ball
.
velocityy
=

(
ball
.
velocityy
)
*
ball
.
elasticity
;
}
ball
.
y
+=
ball
.
velocityy
;
ball
.
x
+=
ball
.
velocityx
;
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
ball
.
x
,
ball
.
y
,
ball
.
radius
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
var
speed
=
6
;
var
gravity
=
.
1
;
var
friction
=
.
008
;
var
elasticity
=
.
5
;
var
angle
=
285
;
var
radians
=
angle
*
Math
.
PI
/
180
;
var
radius
=
15
;
var
vx
=
Math
.
cos
(
radians
)
*
speed
;
var
vy
=
Math
.
sin
(
radians
)
*
speed
;
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
var
p1
=
{
x
:
20
,
y
:
theCanvas
.
height

radius
};
var
ball
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
velocityx
:
vx
,
velocityy
:
vy
,
radius
:
radius
,
elasticity
:
elasticity
};
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
Easing is a technique used in animation to make an object smoothly enter or leave a location. The idea of easing is that instead of uniformly applying movement to every frame of animation, you instead increase (easing in) or decrease (easing out) the number of pixels you move on each frame. The result is that movement appears to be more realistic and smooth. There are many different ways to create easing animations. We will concentrate on two simple examples that will help pave the way for you to further explore the subject on your own.
The process of easing out refers to easing at the end of an animation: an object moving from one point to another, starting out fast, and slowing down as it reaches the second point. To illustrate the concept, we will use the example of a spaceship landing. A spaceship starts out very fast, applies negative thrust to slow down, and by the time it reaches the ground, it’s moving slowly enough to land without incident. If you’ve ever played the video game Lunar Lander, you will understand exactly the type of movement we are trying to accomplish.
To create this easingout effect, we need to find two distinct
points and then move an object between them, slowing down the object in
linear fashion as it nears the second point. To achieve this effect, we
first calculate the distance between the points. Next, we select a percentage
value (easeValue
) that we will use to
move the object across that distance on each frame. As the distance gets
shorter, the amount we move gets shorter as well. This gives the object
the appearance of traveling slower and slower as it moves from the
starting point to the ending point, as illustrated in Figure 522. We have drawn the
points to show the easing values as the ship nears the bottom of the
screen. Notice that the points get closer and closer until there is
almost no distance between them.
Figure 522
displays the results of CH5EX18.html
. Now, let’s look at how this
example works in detail. First, we will load in the ship.png
image the same way we have loaded
images previously in this chapter:
var
shipImage
;
function
eventWindowLoaded
()
{
shipImage
=
new
Image
();
shipImage
.
src
=
"ship.png"
shipImage
.
onload
=
eventAssetsLoaded
;
}
function
eventAssetsLoaded
()
{
canvasApp
();
}
Then, in canvasApp()
, we create
a variable named easeValue
, which
represents the percentage to move the ship across the remaining distance
between the two points. In our example, it is 5% (.05
):
var
easeValue
=
.
05
;
Next we create our two points. The first point, p1
, is close to the middle of the canvas on
the xaxis, and just above the top (−20
) on the yaxis. The final point, p2
, is in the same place on the xaxis, but
near the bottom of the canvas (470
)
on the yaxis:
var
p1
=
{
x
:
240
,
y
:
20
};
var
p2
=
{
x
:
240
,
y
:
470
};
Finally, we create a dynamic object for the ship
that holds these values:
var
ship
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
endx
:
p2
.
x
,
endy
:
p2
.
y
,
velocityx
:
0
,
velocityy
:
0
};
In drawScreen()
, on every
frame, we first find out the distance between the ship and the endpoint
by subtracting the current x
and
y
values for the ship
from the endpoint x
and y
values. The distance will get shorter on each call to drawScreen()
as the ship moves farther away
from p1
and gets closer to p2
. We do this for both x
and y
,
even though, in our example, only the y
value will change as the spaceship gets
closer to p2
:
var
dx
=
ship
.
endx

ship
.
x
;
var
dy
=
ship
.
endy

ship
.
y
;
When we have the distances, we multiply those values by easeValue
to get the x
and y
velocities for the ship
on this call
to drawScreen()
:
ship
.
velocityx
=
dx
*
easeValue
;
ship
.
velocityy
=
dy
*
easeValue
;
Finally, we apply those values and draw the spaceship to the canvas:
ship
.
x
+=
ship
.
velocityx
;
ship
.
y
+=
ship
.
velocityy
;
context
.
drawImage
(
shipImage
,
ship
.
x
,
ship
.
y
);
You can test this example by executing CH5EX18.html
from the code distribution in
your web browser, or by typing in the full code listing shown in Example 518.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX18: Easing Out (Landing The Ship)</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
var
shipImage
;
function
eventWindowLoaded
()
{
shipImage
=
new
Image
();
shipImage
.
src
=
"ship.png"
shipImage
.
onload
=
eventAssetsLoaded
;
}
function
eventAssetsLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
var
pointImage
=
new
Image
();
pointImage
.
src
=
"pointwhite.png"
;
function
drawScreen
()
{
context
.
fillStyle
=
'#000000'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#ffffff'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
var
dx
=
ship
.
endx

ship
.
x
;
var
dy
=
ship
.
endy

ship
.
y
;
ship
.
velocityx
=
dx
*
easeValue
;
ship
.
velocityy
=
dy
*
easeValue
;
ship
.
x
+=
ship
.
velocityx
;
ship
.
y
+=
ship
.
velocityy
;
//Draw points to illustrate path
points
.
push
({
x
:
ship
.
x
,
y
:
ship
.
y
});
for
(
var
i
=
0
;
i
<
points
.
length
;
i
++
)
{
context
.
drawImage
(
pointImage
,
points
[
i
].
x
+
shipImage
.
width
/
2
,
points
[
i
].
y
,
1
,
1
);
}
context
.
drawImage
(
shipImage
,
ship
.
x
,
ship
.
y
);
}
var
easeValue
=
.
05
;
var
p1
=
{
x
:
240
,
y
:
20
};
var
p2
=
{
x
:
240
,
y
:
470
};
var
ship
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
endx
:
p2
.
x
,
endy
:
p2
.
y
,
velocityx
:
0
,
velocityy
:
0
};
var
points
=
new
Array
();
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
We are showing the points in this example, but because the
background is black, we load in a white bitmap point image named
pointwhite.png
instead of the
allblack image, point.png
.
Easing in is the opposite of easing out. When an animation eases in, it starts slowly and then gets faster and faster. If you have ever seen a video of a space shuttle taking off, you will understand this much better. The thrust builds up as the craft at first moves slowly and then gets faster and faster as it moves through the sky. We are going to use this “taking off” example as a way to develop code for an easingin animation on HTML5 Canvas.
In canvasApp()
, we start our
code much the same way as in the last example—by creating a variable
named easeValue
:
var
easeValue
=
.
05
;
However, for easing in, instead of this being a percentage of the remaining distance between two points, it is simply a constant value added to the velocity of the ship on each frame. Figure 523 shows what this would look like. We have added the points again to illustrate how the animation speeds up as the ship takes off.
First, we set the beginning position of the ship
(p1
)
to the bottom center of the canvas. Then we set the beginning speed
of the ship very low (.5
pixels per frame) and set the angle
to 270
(straight up the canvas). We then
calculate the x
and y
velocity values for the ship
:
var
p1
=
{
x
:
240
,
y
:
470
};
var
tempSpeed
=
.
5
;
var
tempAngle
=
270
;
var
tempRadians
=
tempAngle
*
Math
.
PI
/
180
;
var
tempvelocityx
=
Math
.
cos
(
tempRadians
)
*
tempSpeed
;
var
tempvelocityy
=
Math
.
sin
(
tempRadians
)
*
tempSpeed
;
var
ship
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
velocityx
:
tempvelocityx
,
velocityy
:
tempvelocityy
};
In drawScreen()
, instead of
finding the distance between two points, we add the easeValue
to the x
and y
velocities on each frame and then apply it to the ship x
and y
values before drawing it to the canvas. This
creates a linear increase in speed, resulting in the easingin effect we
want to see:
ship
.
velocityx
=
ship
.
velocityx
+
(
ship
.
velocityx
*
easeValue
);
ship
.
velocityy
=
ship
.
velocityy
+
(
ship
.
velocityy
*
easeValue
);
ship
.
x
+=
ship
.
velocityx
;
ship
.
y
+=
ship
.
velocityy
;
context
.
drawImage
(
shipImage
,
ship
.
x
,
ship
.
y
);
You can see this example by executing CH5EX19.html
from the code distribution, or
by typing in the code listing shown in Example 519.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX19: Taking Off (Fake Ease In)</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
var
shipImage
;
function
eventWindowLoaded
()
{
shipImage
=
new
Image
();
shipImage
.
src
=
"ship.png"
shipImage
.
onload
=
eventAssetsLoaded
;
}
function
eventAssetsLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
var
pointImage
=
new
Image
();
pointImage
.
src
=
"pointwhite.png"
;
function
drawScreen
()
{
context
.
fillStyle
=
'#000000'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#ffffff'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
ship
.
velocityx
=
ship
.
velocityx
+
(
ship
.
velocityx
*
easeValue
);
ship
.
velocityy
=
ship
.
velocityy
+
(
ship
.
velocityy
*
easeValue
);
ship
.
x
+=
ship
.
velocityx
;
ship
.
y
+=
ship
.
velocityy
;
//Draw points to illustrate path
points
.
push
({
x
:
ship
.
x
,
y
:
ship
.
y
});
for
(
var
i
=
0
;
i
<
points
.
length
;
i
++
)
{
context
.
drawImage
(
pointImage
,
points
[
i
].
x
+
shipImage
.
width
/
2
,
points
[
i
].
y
,
1
,
1
);
}
context
.
drawImage
(
shipImage
,
ship
.
x
,
ship
.
y
);
}
var
easeValue
=
.
05
;
var
p1
=
{
x
:
240
,
y
:
470
};
var
tempX
;
var
tempY
;
var
tempSpeed
=
.
5
;
var
tempAngle
=
270
;
var
tempRadians
=
tempAngle
*
Math
.
PI
/
180
;
var
tempvelocityx
=
Math
.
cos
(
tempRadians
)
*
tempSpeed
;
var
tempvelocityy
=
Math
.
sin
(
tempRadians
)
*
tempSpeed
;
var
ship
=
{
x
:
p1
.
x
,
y
:
p1
.
y
,
velocityx
:
tempvelocityx
,
velocityy
:
tempvelocityy
};
var
points
=
new
Array
();
theCanvas
=
document
.
getElementById
(
"canvasOne"
);
context
=
theCanvas
.
getContext
(
"2d"
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 50px; left: 50px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"500"
>
Your browser does not support HTML5 Canvas.</canvas>
</div>
</body>
</html>
For more information about easing, check out Robert Penner’s easing equations. These equations have been implemented in jQuery for JavaScript.
Now that we have discussed how to use math and physics to create animations on the Canvas, it’s time to take this discussion to the next level and implement a popular JavaScript library named Box2D. Box2D is a physics modeling library for 2D graphics. We will use it to replace some of the complex code we developed manually for some of the earlier examples in this chapter.
Box2D was originally developed for C++ by Erin Catto and gained popularity as a library named Box2DFlashAS3 for making Flash games in ActionScript 3. There are two Box2D implementations for JavaScript. The first is Box2Djs, and the other is Box2dWeb. Box2dWeb is the latest and greatest and had has been updated to Box2D 2.1, so that is the version we will use in our examples. Box2dWeb was ported directly from ActionScript 3 for use on the HTML5 Canvas.
To start, we need to download the latest version of Box2dWeb. At the time of this writing, the latest version was Box2dWeb2.1a.3.
Box2dWeb is a physics modeling engine. It allows a developer to create a world with physics properties and add objects to that world that react to the properties of that world and to each other. However, the world is not displayed (not formally anyway, as you’ll see in a bit). The job of the Canvas developer when using Box2dWeb is to define a physics model and then apply it to the Canvas.
Before we get started, here are some definitions in Box2dWeb related to the examples we are going to cover. (The World modeled with Box2D is more complicated than just this, but these are the things we will cover in the examples.)
A geometrical object that exists 2D space. These are circles, squares, rectangles, and so on.
A logical object that binds a shape to body and give it properties like density, friction, and restitution.
A physical manifestation of an object in the 2D world, with a fixture and a shape applied.
A 2D physics world that contains bodies that react to one another
In the examples that follow, we will show how these things relate to one another in Box2D.
For our first example, we will create a box that contains balls that fall and bounce on the screen. See Figure 524 to see an example.
The first thing we need to do is include the Box2dWeb library in JavaScript.
<
script
type
=
"text/javascript"
src
=
"Box2dWeb2.1.a.3.js"
><
/script>
Next we add the code to initialize the world. The variables created in the following code are shortcuts to the objects in Box2dWeb. They have shorter names make it much easier to define new objects in code. Not all the objects in Box2dWeb are represented here, so if you need a different one (such as a joint with b2JointDef), you will need to define a new shortcut (or just use the full object path name).
var
b2Vec2
=
Box2D
.
Common
.
Math
.
b2Vec2
,
b2BodyDef
=
Box2D
.
Dynamics
.
b2BodyDef
,
b2Body
=
Box2D
.
Dynamics
.
b2Body
,
b2FixtureDef
=
Box2D
.
Dynamics
.
b2FixtureDef
,
b2World
=
Box2D
.
Dynamics
.
b2World
,
b2PolygonShape
=
Box2D
.
Collision
.
Shapes
.
b2PolygonShape
,
b2CircleShape
=
Box2D
.
Collision
.
Shapes
.
b2CircleShape
,
b2DebugDraw
=
Box2D
.
Dynamics
.
b2DebugDraw
;
Now you create your world object. This will define your physics
world. b2Vec()
is a Box2D object that
accepts two parameters: xaxis gravity and yaxis gravity. The second
parameter is doSleep
, which improves
performance by not simulating inactive bodies if set to true.
var
world
=
new
b2World
(
new
b2Vec2
(
0
,
10
),
true
);
The first Box2D objects we will create are the four walls that define the area that holds our balls (a 500×400 Canvas). These walls are similar to the boundaries we defined in code for the earlier bouncing balls demos. We will define the walls as dynamic objects in an array, each with four properties:
{
x
:
,
x
position
,
y
:
y
position
,
w
:
width
,
h
:
height
}
var
wallDefs
=
new
Array
(
{
x
:
8.3
,
y
:
.
03
,
w
:
8.3
,
h
:
.
03
},
//top wall
{
x
:
8.3
,
y
:
13.33
,
w
:
8.3
,
h
:
.
03
},
//bottom wall
{
x
:
0
,
y
:
6.67
,
w
:
.
03
,
h
:
6.67
},
//left wall
{
x
:
16.7
,
y
:
6.67
,
w
:
.
03
,
h
:
6.67
}
);
//right wall
Notice that the values defined above do not look like anything we have used before. The values are in MTS units. As we stated earlier, MTS units refer to “meterstonnesecond” and are used represent large objects and spaces. However, because we draw everything in pixels on the Canvas, we need a way to think of MTS units in terms of the Canvas and vice versa.
To get these values, we need to choose a “scale” value to scale
the Canvas pixels to MTS units. For this example (and all the examples
in the rest of this chapter), we have chosen the value 30
as the scale value to translate pixels to
MTS units.
At the same time, objects displayed in Box2D have their origin at the center (not the upperleft corner), so we need to compensate for that as well. Figure 525 shows how the units translate and the relative origins of the walls.
To get the sizes of the walls (width and height) we need to divide all the relative Canvas pixel values by 30. To get the location, we need to do the same but also make sure that we provide origins in the center, not the upperleft side of the object.
To top this off, the width and height passed to the Box2D setAsBox()
method (width and height) are given
in ½ width and ½ height. (We will discuss setAsBox()
a little later in this
chapter.)
So, for the top wall that we want to extend all the way across the Canvas with a height of 1 pixel, the code would be as follows:
{
x
:
250
/
30
,
y
:
1
/
30
,
w
:
500
/
2
/
30
,
h
:
1
/
30
}
Or, alternatively:
{
x
:
8.3
,
y
:
.
03
,
w
:
8.33
,
h
:
.
03
},
//top wall
The rest of the wall values use the same type of calculations to convert pixel values to MTS units for Box2D. Now let’s use those units to make something.
What is interesting about this scale value is that when you use it uniformly (to convert to MTS units from pixels and from pixels to MTS units), the value can be anything. It just needs to be consistent. However, the value does have an effect on your physics world. The size of the value models the world in a different way. The smaller the scale value, the bigger the objects are in your world and vice versa. You will need to manipulate this scale value to tweak the physics for the world you are simulating.
We now have our walls defined with locations and sizes based on
MTS units. We can start defining the walls based on Box2D objects. We
start by creating an array to hold our walls, and then we loop through
the wallDefs
array to create the four
walls that bound the screen.
var
walls
=
new
Array
();
for
(
var
i
=
0
;
i
<
wallDefs
.
length
;
i
++
)
{
Next we start to define a wall by creating an instance b2BodyDef
. A b2BodyDef
object will hold the definitions we
need to define a rigid body. Recall that a rigid body will have a
shape and fixture applied to
create a Box2D object that we can manipulate. We set the type to be
b2_staticBody
, which means that this
object will never move and has infinite mass: perfect for the walls that
bound our Canvas. We then set the position of wallDef
to the position for this wall as set
in the array of dynamic objects. Finally, we call world.createBody(wallDef)
to create a rigid
body in Box2D, based on the definitions set in the b2BodyDef
object:
var
wallDef
=
new
b2BodyDef
;
wallDef
.
type
=
b2Body
.
b2_staticBody
;
wallDef
.
position
.
Set
(
wallDefs
[
i
].
x
,
wallDefs
[
i
].
y
);
var
newWall
=
world
.
CreateBody
(
wallDef
)
Now it is time to create the fixture definition for the wall.
Recall that a fixture binds a shape to the rigid body, and it contains
properties for our Box2D object. First, we create a new b2FixtureDef
, and then we set the density
(mass per unit volume), friction
(resistance, usually between 0 and
1), and the restitution
(elasticity,
usually between 0 and 1):
var
wallFixture
=
new
b2FixtureDef
;
wallFixture
.
density
=
10.0
;
wallFixture
.
friction
=
0.5
;
wallFixture
.
restitution
=
1
;
Now that we have the fixture, we need to create the shape. We set
the shape
property of the fixture as
b2PolygonShape
, and then we call the
shape’s setAsBox()
method (pass the
width
and height
of the wall) to finish defining the
shape. Next we close the loop by calling the createFixture()
method of our rigid body,
passing wallFixture
as the parameter.
This sets the fixture and shape to the rigid body. Finally, we add the
rigid body to our walls array.
wallFixture
.
shape
=
new
b2PolygonShape
;
wallFixture
.
shape
.
SetAsBox
(
wallDefs
[
i
].
w
,
wallDefs
[
i
].
h
);
newWall
.
CreateFixture
(
wallFixture
);
walls
.
push
(
newWall
);
}
Creating the balls that will fall and bounce around in our demo is very
similar to creating walls in Box2D. For this exercise, we will create 50
balls with random locations and sizes. The locations and sizes are
(again) in MTS units. The only real difference from creating the walls
is that we set the fixture shape property to b2CircleShape(size)
, where size is a random
size in MTS units.
var
numBalls
=
50
;
var
balls
=
new
Array
();
for
(
var
i
=
0
;
i
<
numBalls
;
i
++
)
{
var
ballDef
=
new
b2BodyDef
;
ballDef
.
type
=
b2Body
.
b2_dynamicBody
;
var
ypos
=
(
Math
.
random
()
*
8
)
+
1
;
var
xpos
=
(
Math
.
random
()
*
14
)
+
1
;
var
size
=
(
Math
.
random
()
*
.
4
)
+
0.2
;
ballDef
.
position
.
Set
(
xpos
,
ypos
);
var
ballFixture
=
new
b2FixtureDef
;
ballFixture
.
density
=
10.0
;
ballFixture
.
friction
=
0.5
;
ballFixture
.
restitution
=
1
;
ballFixture
.
shape
=
new
b2CircleShape
(
size
);
var
newBall
=
world
.
CreateBody
(
ballDef
)
newBall
.
CreateFixture
(
ballFixture
);
balls
.
push
(
newBall
);
}
The b2debugDraw
functionality
of Box2D is just a way to test your physics world and see it
working in a web browser. It is not an implementation of the physics for
your application. We will use debugDraw
to illustrate the objects we have
created so that we can see them in action. In example CH5EX22.html
, we will show you how to apply
Box2D to the Canvas. For now, we just want to see the physics model we
have created.
The following code is fairly boilerplate for getting b2debugDraw
to work. The SetSprite()
method takes the Canvas context as a parameter. This means that
the Canvas will be completely overwritten by the output from b2debugDraw
. This is why it’s just for
testing. SetScaleFactor()
takes the value we used to
convert from pixels to MTS units (30). SetFillAlpha()
sets the transparency of the lines of the objects when
displayed, and likewise, SetLineThickness()
sets the thickness of those
lines. The SetFlags()
method
accepts a single bitwise parameter of options for
display: e_shapeBit
draws the shapes, and e_jointBit
draws joints (but because we have not created any, you will not
see them):
var
debugDraw
=
new
b2DebugDraw
();
debugDraw
.
SetSprite
(
context2
);
debugDraw
.
SetDrawScale
(
30
);
debugDraw
.
SetFillAlpha
(
0.3
);
debugDraw
.
SetLineThickness
(
1.0
);
debugDraw
.
SetFlags
(
b2DebugDraw
.
e_shapeBit

b2DebugDraw
.
e_jointBit
);
world
.
SetDebugDraw
(
debugDraw
);
Finally, we are ready to draw our Box2D physics models to the
HTML5 Canvas. To make that work, we replace all of
the code we have previously created in our drawScreen()
function with following three
lines:
world
.
Step
(
1
/
60
,
10
,
10
);
world
.
DrawDebugData
();
world
.
ClearForces
();
world.Step()
takes three parameters: time step, velocity iterations, and
position iterations. The time step is how much time in the simulation to
simulate on every call. For our application, we are simulating 1/60 of a
second. The other two parameters set the precision of the iterations and
should be left alone for now. world.DrawDebugData()
draws the b2DebugDraw
to the Canvas. world.clearForces()
is called after every step
to reset the physics model for the next step.
Example 520 provides the full code
listing for CH1EX20.html
. When you
test this example, you will see 50 randomly created balls fall down and
bounce off the bottom of the canvas. Some will bounce around for a long
time, others will stop bouncing much sooner. Notice that balls appear to
act the way you would expect balls to act. However, if you look back at
the code we wrote, we did not create any functions for hit detection,
angle of reflection, or linear movement. All of that was handled by
Box2D.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX20: Box2dWeb Balls Demo</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
src
=
"Box2dWeb2.1.a.3.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
world
.
Step
(
1
/
60
,
10
,
10
);
world
.
DrawDebugData
();
world
.
ClearForces
();
}
theCanvas
=
document
.
getElementById
(
'canvasOne'
);
context
=
theCanvas
.
getContext
(
'2d'
);
var
b2Vec2
=
Box2D
.
Common
.
Math
.
b2Vec2
,
b2BodyDef
=
Box2D
.
Dynamics
.
b2BodyDef
,
b2Body
=
Box2D
.
Dynamics
.
b2Body
,
b2FixtureDef
=
Box2D
.
Dynamics
.
b2FixtureDef
,
b2World
=
Box2D
.
Dynamics
.
b2World
,
b2PolygonShape
=
Box2D
.
Collision
.
Shapes
.
b2PolygonShape
,
b2CircleShape
=
Box2D
.
Collision
.
Shapes
.
b2CircleShape
,
b2DebugDraw
=
Box2D
.
Dynamics
.
b2DebugDraw
;
var
world
=
new
b2World
(
new
b2Vec2
(
0
,
10
),
true
);
var
numBalls
=
50
;
var
balls
=
new
Array
();
for
(
var
i
=
0
;
i
<
numBalls
;
i
++
)
{
var
ballDef
=
new
b2BodyDef
;
ballDef
.
type
=
b2Body
.
b2_dynamicBody
;
var
ypos
=
(
Math
.
random
()
*
8
)
+
1
;
var
xpos
=
(
Math
.
random
()
*
14
)
+
1
;
var
size
=
(
Math
.
random
()
*
.
4
)
+
.
2
;
ballDef
.
position
.
Set
(
xpos
,
ypos
);
var
ballFixture
=
new
b2FixtureDef
;
ballFixture
.
density
=
10.0
;
ballFixture
.
friction
=
0.5
;
ballFixture
.
restitution
=
1
;
ballFixture
.
shape
=
new
b2CircleShape
(
size
);
var
newBall
=
world
.
CreateBody
(
ballDef
)
newBall
.
CreateFixture
(
ballFixture
);
balls
.
push
(
newBall
);
}
var
wallDefs
=
new
Array
({
x
:
8.3
,
y
:
.
03
,
w
:
8.3
,
h
:
.
03
},
//top
{
x
:
8.3
,
y
:
13.33
,
w
:
8.3
,
h
:
.
03
},
//bottom
{
x
:
0
,
y
:
6.67
,
w
:
.
03
,
h
:
6.67
},
//left
{
x
:
16.7
,
y
:
6.67
,
w
:
.
03
,
h
:
6.67
}
);
//right
var
walls
=
new
Array
();
for
(
var
i
=
0
;
i
<
wallDefs
.
length
;
i
++
)
{
var
wallDef
=
new
b2BodyDef
;
wallDef
.
type
=
b2Body
.
b2_staticBody
;
wallDef
.
position
.
Set
(
wallDefs
[
i
].
x
,
wallDefs
[
i
].
y
);
var
newWall
=
world
.
CreateBody
(
wallDef
)
var
wallFixture
=
new
b2FixtureDef
;
wallFixture
.
density
=
10.0
;
wallFixture
.
friction
=
0.5
;
wallFixture
.
restitution
=
1
;
wallFixture
.
shape
=
new
b2PolygonShape
;
wallFixture
.
shape
.
SetAsBox
(
wallDefs
[
i
].
w
,
wallDefs
[
i
].
h
);
newWall
.
CreateFixture
(
wallFixture
);
walls
.
push
(
newWall
);
}
var
debugDraw
=
new
b2DebugDraw
();
debugDraw
.
SetSprite
(
context
);
debugDraw
.
SetDrawScale
(
30
);
//define scale
debugDraw
.
SetFillAlpha
(
0.3
);
//define transparency
debugDraw
.
SetLineThickness
(
1.0
);
debugDraw
.
SetFlags
(
b2DebugDraw
.
e_shapeBit

b2DebugDraw
.
e_jointBit
);
world
.
SetDebugDraw
(
debugDraw
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<div
style=
"position: absolute; top: 0px; left: 0px;"
>
<canvas
id=
"canvasOne"
width=
"500"
height=
"400"
>
Your browser does not support the HTML 5 Canvas.</canvas>
</div>
</body>
</html>
Do you recall from CH5EX7.html
how we created bouncing balls that bounce off one another? We are going
to iterate the last example with just a few simple changes to create a
world that acts a lot like that example.
First, we will change the gravity in the world definition so that there is no yaxis gravity at all. Instead of looking at balls from the side, we are looking at balls from the top (like it’s a flat 2D world and we are looking down). We will also set our demo to have 50 balls instead of 30.
var
world
=
new
b2World
(
new
b2Vec2
(
0
,
0
),
true
);
var
numBalls
=
50
;
Because there is no gravity in this world, we need some way for
the balls to move when the demo starts. To do this, we will add a random
x and y velocity to each ball
and then pass those as parameters of a new b2Vec2()
object with a call to the ball’s SetLinearVelocity()
method.
var
xVelocity
=
Math
.
floor
(
Math
.
random
()
*
10
)
−
5
;
var
yVelocity
=
Math
.
floor
(
Math
.
random
()
*
10
)
−
5
;
newBall
.
SetLinearVelocity
(
new
b2Vec2
(
xVelocity
,
yVelocity
))
That’s all we need to do to change our falling balls demo into something similar to the bouncing balls demo from earlier in the chapter.
You can test this example by opening CH5EX21.html
in the code distribution. The
results will look something like what you see in Figure 526.
As you can see, getting up to speed with Box2D might take a bit of time, but when you get the hang of it, you can make some dramatic changes with very little code.
So far, we have looked only at Box2D output with b2DebigDraw
. While this looks neat, it’s not
really useful for Canvas applications. As we stated earlier, Box2D is
just a “physics model.” It is your job to apply that model to the HTML5
Canvas. In this next example, we will take the bouncing balls from the
last example and show them running sidebywide with the Canvas. (See
Figure 527.)
The first thing we need to do is to define a second Canvas in our
HTML to show the b2DebugDraw
output:
<canvas
id=
"canvasOne"
width=
"450"
height=
"350"
>
Your browser does not support the HTML 5 Canvas.</canvas>
<canvas
id=
"canvasTwo"
width=
"450"
height=
"350"
>
Your browser does not support the HTML 5 Canvas.</canvas>
Notice that the Canvas is now smaller (450×350). We changed the size so that we could easily fit two Canvases side by side in the web browser. We also need to make sure to get a reference to both in our JavaScript code:
var
theCanvas
=
document
.
getElementById
(
'canvasOne'
);
var
context
=
theCanvas
.
getContext
(
'2d'
);
var
theCanvasTwo
=
document
.
getElementById
(
'canvasTwo'
);
var
context2
=
theCanvasTwo
.
getContext
(
'2d'
);
Now that we have that out of the way, let’s start by making our
lives a lot easier. Instead of using a literal for our scale factor, we
will create a variable named scaleFactor
that we can use for scale
conversions:
var
scaleFactor
=
30
;
We can use scaleFactor
when
finding positions and sizes for our objects in Box2D. First, in the loop
where we created the balls to display, we now use the entire canvas
width and height, modified by the scaleFactor
to randomly position the
balls:
var
ypos
=
(
Math
.
random
()
*
theCanvas
.
height
)
/
scaleFactor
;
var
xpos
=
(
Math
.
random
()
*
theCanvas
.
width
/
scaleFactor
);
var
size
=
((
Math
.
random
()
*
20
)
+
5
)
/
scaleFactor
;
We also now define the wallDefs
using the width and height of the Canvas instead of the MTS values we
used in the previous examples. This is essentially the same, but it
makes it much easier to adjust the size of the Canvas now that we are
not using literals. Because our Canvas is now smaller, it helps that we
don’t have to recalculate all the values:
var
wallDefs
=
new
Array
(
{
x
:
theCanvas
.
width
,
y
:
0
,
w
:
theCanvas
.
width
,
h
:
1
},
//top
{
x
:
theCanvas
.
width
,
y
:
theCanvas
.
height
,
w
:
theCanvas
.
width
,
h
:
1
},
//bottom
{
x
:
0
,
y
:
theCanvas
.
height
,
w
:
1
,
h
:
theCanvas
.
height
},
//left
{
x
:
theCanvas
.
width
,
y
:
theCanvas
.
height
,
w
:
1
,
h
:
theCanvas
.
height
}
);
//right
In the loop where we define the walls, we now use scaleFactor
to convert the pixels values in
our array to MTS values for positions and sizes of the walls:
wallDef
.
position
.
Set
(
wallDefs
[
i
].
x
/
scaleFactor
,
wallDefs
[
i
].
y
/
scaleFactor
);
...
wallFixture
.
shape
.
SetAsBox
(
wallDefs
[
i
].
w
/
scaleFactor
,
wallDefs
[
i
].
h
/
scaleFactor
);
When we define our instance of b2DebugDraw
, we pass a reference to the second
Canvas (context2
) so that the debug
output will display there, and we can draw on the first Canvas with the
Canvas drawing API:
debugDraw
.
SetSprite
(
context2
);
debugDraw
.
SetDrawScale
(
scaleFactor
);
//define scale
The big changes for this example arrive in drawScreen()
. The calls to the methods of the
world object are the same as in the last example (even though world.DrawDebugData()
is now using context2
):
function
drawScreen
()
{
world
.
Step
(
1
/
60
,
10
,
10
);
world
.
DrawDebugData
();
world
.
ClearForces
();
Now we are going to translate the Box2D data in our model to the
Canvas. Most of this drawScreen()
function should look familiar, because it is not much different than
some of the earlier bouncing ball demos. First we clear the Canvas, and
then we look through the balls
array
to display each ball:
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
strokeStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
for
(
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
The first big difference is how we get the position and size of
the ball we are going to draw. The items we have in our balls
array are instances of the Box2D
b2Body
object. We use that object’s GetPosition()
method to find the current
x
and y
coordinates of the ball. We then call the GetFixtureList()
method of b2Body
. This will give us the b2Fixture
we created for the ball. Recall that
for each fixture we set a shape
property. We need to get that shape
property by calling fixtureList.GetShape()
:
var
position
=
balls
[
i
].
GetPosition
();
var
fixtureList
=
balls
[
i
].
GetFixtureList
();
var
shape
=
fixtureList
.
GetShape
();
With all the information we need, we can now draw the ball on the
Canvas. Using the context.arc()
method, we set the x
and y
position multiplied by the scaleFactor
(because we are now converting
from MTS units to pixels). We call shape.GetRadius()
for the size of the circle
and use the same conversion with scaleFactor
:
context
.
fillStyle
=
"#000000"
;
context
.
beginPath
();
context
.
arc
(
position
.
x
*
scaleFactor
,
position
.
y
*
scaleFactor
,
shape
.
GetRadius
()
*
scaleFactor
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
When you run the example (CH5EX22.html
), you will see something similar
to what is in Figure 527 The Canvas on the
left displays balls using the Canvas drawing objects and looks very
similar to the demo we created in CH5EX8.html
. The Canvas on the right shows
the b2DebugDraw
output from Box2D. We
have now successfully applied a Box2D physics model to the HTML5
Canvas.
Again, notice that, besides getting familiar with the object model of Box2D, this example is very simple. We did not have to build any movement or collisions detection routines ourselves. Box2D takes care of all that stuff on its own. We just need to define a Box2D world and then put objects inside of it.
So far, we have simply placed objects in a defined Box2D world and let them move on their own. While that type of application is great for teaching or instruction, creating an interactive world with Box2D is a path to creating exciting games and simulations on the Canvas.
For this example, we will create a game where you shoot balls at a pile of boxes and knock them over. However, before we get to that example, we will start with a simpler iteration where we display some boxes and model them falling and rotating. (See Figure 528.)
The world we are going to simulate is essentially the same as in the last
example, with a positive yaxis gravitation force of 10 and a scaleFactor
of 30.
var
world
=
new
b2World
(
new
b2Vec2
(
0
,
10
),
true
);
var
scaleFactor
=
30
;
In this world, we will create a set of eight 25×25 pixel boxes that are piled on top of each other, 100 pixels from the right side of the Canvas, and starting 100 pixels from the bottom so that they will fall to the ground when the demo starts:
var
numBoxes
=
8
;
var
boxes
=
new
Array
();
var
boxHeight
=
25
;
var
boxWidth
=
25
;
var
startX
=
(
theCanvas
.
width

100
);
var
startY
=
(
theCanvas
.
height

boxHeight
)

100
When we loop through, creating the boxes, we perform essentially
the same operations as when we create walls. We use the scaleFactor
to convert to MTS units, but
instead of defining a b2_staticBody
,
we define the box as a b2_dynamicBody
, which means it can move and be
affected by the physicsdriven models in Box2D.
One other thing we do here is offset each box to the right by
(i*2)
pixels. We do this so that each
successive box will fall a tiny bit to the right of the box under it.
This will make the stack unstable so that the boxes fall over when they
hit the ground. We want to do this so that we can illustrate rotating
the boxes on the Canvas:
for
(
var
i
=
0
;
i
<
numBoxes
;
i
++
)
{
var
boxDef
=
new
b2BodyDef
;
boxDef
.
type
=
b2Body
.
b2_dynamicBody
;
var
ypos
=
(
startY

(
i
*
boxHeight
))
/
scaleFactor
;
var
xpos
=
(
startX
+
(
i
*
2
))
/
scaleFactor
;
boxDef
.
position
.
Set
(
xpos
,
ypos
);
var
newBox
=
world
.
CreateBody
(
boxDef
)
var
boxFixture
=
new
b2FixtureDef
;
boxFixture
.
density
=
10.0
;
boxFixture
.
friction
=
0.5
;
boxFixture
.
restitution
=
1
;
boxFixture
.
shape
=
new
b2PolygonShape
;
boxFixture
.
shape
.
SetAsBox
((
boxWidth
/
scaleFactor
)
/
2
,
(
boxHeight
/
scaleFactor
)
/
2
);
newBox
.
CreateFixture
(
boxFixture
);
The one addition we will add to the boxes that we did not have for
the walls is using the SetUserData()
method of the b2Body
object. This allows us to create a
dynamic object that holds any type of data we might need and attach it
to the body. For our purposes, we will attach an object that holds the
width and height of the boxes (in MTS units) so that we can easily model
them on the Canvas in our drawScreen()
function.
newBox
.
SetUserData
({
width
:
boxWidth
/
scaleFactor
,
height
:
boxHeight
/
scaleFactor
})
boxes
.
push
(
newBox
);
}
To render the boxes, we loop through the boxes array in drawScreen()
in a similar manner to how we
looped through the balls in CH5EX22.html
. We retrieve the position and
shape using GetPosition()
and
GetShape()
, preparing to render the boxes:
for
(
i
=
0
;
i
<
boxes
.
length
;
i
++
)
{
var
position
=
boxes
[
i
].
GetPosition
();
var
fixtureList
=
boxes
[
i
].
GetFixtureList
();
var
shape
=
fixtureList
.
GetShape
();
However, because these are boxes that fall and rotate, we need to
perform some significantly different operations to render the boxes
correctly. First we retrieve the object we saved in the user data
attached to the body by calling GetUserData()
:
var
userData
=
boxes
[
i
].
GetUserData
();
Next we save the Canvas context, reset the transformation matrix,
and translate to the center of the box. Because the origin of the
objects in Box2D is set to the center, we don’t have to offset to the
center for our transformation. Next we rotate the Canvas to the angle of
the box. We find the angle with a call to GetAngle()
:
context
.
save
();
context
.
setTransform
(
1
,
0
,
0
,
1
,
0
,
0
);
context
.
translate
(
position
.
x
*
scaleFactor
,
position
.
y
*
scaleFactor
);
context
.
rotate
(
boxes
[
i
].
GetAngle
());
To draw the box, we need to offset the x
and y
from the position back to ½ width and height because we draw on the
Canvas with an origin at the upperleft corner. Then we restore the
context, and we are done with the box:
context
.
fillRect
(
0

(
userData
.
width
*
scaleFactor
/
2
),
0

(
userData
.
height
*
scaleFactor
/
2
),
userData
.
width
*
scaleFactor
,
userData
.
height
*
scaleFactor
);
context
.
restore
();
}
We have now modeled boxes falling and reacting to gravity and
collisions with other boxes in Box2D. You can view this example by
looking at CH5EX23.html
in the code
distribution.
The final example in our foray into Box2D will add interactivity to the Canvas so that we can shoot balls that knock over the pile of boxes we created in the last example. Figure 529 shows what the example looks like when executed in a web browser.
There are a few changes we need to make for this example from CH5EX23.html
when we create the boxes. In the
loop, we set the x position for the boxes to be right on top of each
other, instead of offset to the right. We no longer need the boxes to
fall over on their own; the balls we shoot at the boxes will do that for
us!
We also set the density of the boxes to 20 so that they are harder to knock over, and we reduce the restitution to .1 so that they don’t bounce around as much as they did in the last example:
var
xpos
=
(
startX
)
/
scaleFactor
;
boxFixture
.
density
=
20.0
;
...
boxFixture
.
restitution
=
.
1
;
Also, when we create the walls, we set the wall restitution to .5, which means objects will be less apt to bounce around on top of them:
wallFixture
.
restitution
=
.
5
;
For this example, we want to listen for a mouse click on the Canvas. When
the user clicks, we want to create a ball that flies across the Canvas
towards the piles of boxes. The first thing we need to do is create an
event listener for the mouseup
event:
theCanvas
.
addEventListener
(
"mouseup"
,
createBall
,
false
);
Next we need to create the createBall()
function. First, we get the
x and y position of the mouse
from the event object passed to createBall()
. Then we use some crossbrowser
code to figure out the mouse position relative to the Canvas. The
following boilerplate code captured (at the time of this writing) the
proper x
and y
mouse position on the Canvas:
function
createBall
(
event
)
{
var
x
;
var
y
;
if
(
event
.
pageX

event
.
pageY
)
{
x
=
event
.
pageX
;
y
=
event
.
pageY
;
}
else
{
x
=
e
.
clientX
+
document
.
body
.
scrollLeft
+
document
.
documentElement
.
scrollLeft
;
y
=
e
.
clientY
+
document
.
body
.
scrollTop
+
document
.
documentElement
.
scrollTop
;
}
x
=
theCanvas
.
offsetLeft
;
y
=
theCanvas
.
offsetTop
;
mouseX
=
x
;
mouseY
=
y
;
Because we are capturing the mouse x
and y
position relative to the Canvas, you need to make sure that the <canvas>
element in the HTML page is
styled with top and left values so that the offsetLeft
and offsetTop
values are correct. For example, if
you position the Canvas inside a <div>
at 50,50
and leave the left
and top
style values at 0
, the mouse clicks will not be captured in
the correct locations:
<canvas
id=
"canvasOne"
width=
"450"
height=
"350"
style=
"position: absolute; top: 0px; left: 0px;"
>
Your browser does not support the HTML5 Canvas.</canvas>
<canvas
id=
"canvasTwo"
width=
"450"
height=
"350"
style=
"position: absolute; top: 0px; left: 451px;"
>
Your browser does not support the HTML5 Canvas.</canvas>
Next, we create a rigid body definition object, setting the
x and y position in the
b2BodyDef
to the location of the
mouse click, and we set the size of the ball radius to 7 pixels, which
we will use a few lines down:
var
ballDef
=
new
b2BodyDef
;
ballDef
.
type
=
b2Body
.
b2_dynamicBody
;
var
ypos
=
mouseY
/
scaleFactor
;
var
xpos
=
mouseX
/
scaleFactor
;
var
size
=
7
/
scaleFactor
;
ballDef
.
position
.
Set
(
xpos
,
ypos
);
Next we create the fixture, setting the density of the ball to 30 (harder than the boxes), friction to .6 (only because it makes the simulation feel better when running), and set the restitution to .2, which is more than both the boxes and the walls because we want to balls to bounce a bit higher than the boxes when they hit another object:
var
ballFixture
=
new
b2FixtureDef
;
ballFixture
.
density
=
30.0
;
ballFixture
.
friction
=
0.6
;
ballFixture
.
restitution
=
.
2
;
ballFixture
.
shape
=
new
b2CircleShape
(
size
);
var
newBall
=
world
.
CreateBody
(
ballDef
)
newBall
.
CreateFixture
(
ballFixture
);
Then, we set the linear velocity of the balls to positive 15 on the x axis, which will have them start moving across the Canvas to the right when they are created:
var
xVelocity
=
15
;
var
yVelocity
=
0
;
newBall
.
SetLinearVelocity
(
new
b2Vec2
(
xVelocity
,
yVelocity
))
balls
.
push
(
newBall
);
}
Finally, we need to render the balls in drawScreen()
. Except for the fact that we
color the balls red, the rendering we do here is essentially the same as
we did in the bouncing balls demo in CH5EX22
:
for
(
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
var
position
=
balls
[
i
].
GetPosition
();
var
fixtureList
=
balls
[
i
].
GetFixtureList
();
var
shape
=
fixtureList
.
GetShape
();
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
position
.
x
*
scaleFactor
,
position
.
y
*
scaleFactor
,
shape
.
GetRadius
()
*
scaleFactor
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
Those are all the changes we need to make to convert the stacked
boxes example (CH5EX23.html
) into
an interactive experience when the user shoots balls to knock down
boxes. Load CH5EX24.html
from the
code distribution into your web browser, and check it out! The full code
listing is provided in Example 521.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF8"
>
<title>
CH5EX24: Box2dWeb Box Battle</title>
<
script
src
=
"modernizr.js"
><
/script>
<
script
type
=
"text/javascript"
src
=
"Box2dWeb2.1.a.3.js"
><
/script>
<
script
type
=
"text/javascript"
>
window
.
addEventListener
(
'load'
,
eventWindowLoaded
,
false
);
function
eventWindowLoaded
()
{
canvasApp
();
}
function
canvasSupport
()
{
return
Modernizr
.
canvas
;
}
function
canvasApp
()
{
if
(
!
canvasSupport
())
{
return
;
}
function
drawScreen
()
{
world
.
Step
(
1
/
60
,
10
,
10
);
world
.
DrawDebugData
();
world
.
ClearForces
();
context
.
strokeStyle
=
'#000000'
;
context
.
fillStyle
=
'#EEEEEE'
;
context
.
fillRect
(
0
,
0
,
theCanvas
.
width
,
theCanvas
.
height
);
//Box
context
.
fillStyle
=
'#000000'
;
context
.
strokeRect
(
1
,
1
,
theCanvas
.
width

2
,
theCanvas
.
height

2
);
for
(
i
=
0
;
i
<
boxes
.
length
;
i
++
)
{
var
position
=
boxes
[
i
].
GetPosition
();
var
fixtureList
=
boxes
[
i
].
GetFixtureList
();
var
shape
=
fixtureList
.
GetShape
();
var
userData
=
boxes
[
i
].
GetUserData
();
context
.
save
();
context
.
setTransform
(
1
,
0
,
0
,
1
,
0
,
0
);
context
.
translate
(
position
.
x
*
scaleFactor
,
position
.
y
*
scaleFactor
);
context
.
rotate
(
boxes
[
i
].
GetAngle
());
context
.
fillRect
(
0

(
userData
.
width
*
scaleFactor
/
2
)
,
0

(
userData
.
height
*
scaleFactor
/
2
),
userData
.
width
*
scaleFactor
,
userData
.
height
*
scaleFactor
);
context
.
restore
();
}
for
(
i
=
0
;
i
<
balls
.
length
;
i
++
)
{
var
position
=
balls
[
i
].
GetPosition
();
var
fixtureList
=
balls
[
i
].
GetFixtureList
();
var
shape
=
fixtureList
.
GetShape
();
context
.
fillStyle
=
"#FF0000"
;
context
.
beginPath
();
context
.
arc
(
position
.
x
*
scaleFactor
,
position
.
y
*
scaleFactor
,
shape
.
GetRadius
()
*
scaleFactor
,
0
,
Math
.
PI
*
2
,
true
);
context
.
closePath
();
context
.
fill
();
}
}
function
createBall
(
event
)
{
var
x
;
var
y
;
if
(
event
.
pageX

event
.
pageY
)
{
x
=
event
.
pageX
;
y
=
event
.
pageY
;
}
else
{
x
=
e
.
clientX
+
document
.
body
.
scrollLeft
+
document
.
documentElement
.
scrollLeft
;
y
=
e
.
clientY
+
document
.
body
.
scrollTop
+
document
.
documentElement
.
scrollTop
;
}
x
=
theCanvas
.
offsetLeft
;
y
=
theCanvas
.
offsetTop
;
mouseX
=
x
;
mouseY
=
y
;
var
ballDef
=
new
b2BodyDef
;
ballDef
.
type
=
b2Body
.
b2_dynamicBody
;
var
ypos
=
mouseY
/
scaleFactor
;
var
xpos
=
mouseX
/
scaleFactor
;
var
size
=
7
/
scaleFactor
;
ballDef
.
position
.
Set
(
xpos
,
ypos
);
var
ballFixture
=
new
b2FixtureDef
;
ballFixture
.
density
=
30.0
;
ballFixture
.
friction
=
0.6
;
ballFixture
.
restitution
=
.
2
;
ballFixture
.
shape
=
new
b2CircleShape
(
size
);
var
newBall
=
world
.
CreateBody
(
ballDef
)
newBall
.
CreateFixture
(
ballFixture
);
var
xVelocity
=
15
;
var
yVelocity
=
0
;
newBall
.
SetLinearVelocity
(
new
b2Vec2
(
xVelocity
,
yVelocity
))
balls
.
push
(
newBall
);
}
var
theCanvas
=
document
.
getElementById
(
'canvasOne'
);
var
context
=
theCanvas
.
getContext
(
'2d'
);
var
theCanvasTwo
=
document
.
getElementById
(
'canvasTwo'
);
var
context2
=
theCanvasTwo
.
getContext
(
'2d'
);
theCanvas
.
addEventListener
(
"mouseup"
,
createBall
,
false
);
var
scaleFactor
=
30
;
var
b2Vec2
=
Box2D
.
Common
.
Math
.
b2Vec2
,
b2BodyDef
=
Box2D
.
Dynamics
.
b2BodyDef
,
b2Body
=
Box2D
.
Dynamics
.
b2Body
,
b2FixtureDef
=
Box2D
.
Dynamics
.
b2FixtureDef
,
b2World
=
Box2D
.
Dynamics
.
b2World
,
b2PolygonShape
=
Box2D
.
Collision
.
Shapes
.
b2PolygonShape
,
b2CircleShape
=
Box2D
.
Collision
.
Shapes
.
b2CircleShape
,
b2DebugDraw
=
Box2D
.
Dynamics
.
b2DebugDraw
;
var
world
=
new
b2World
(
new
b2Vec2
(
0
,
20
),
true
);
var
numBoxes
=
8
;
var
boxes
=
new
Array
();
var
balls
=
new
Array
();
var
boxHeight
=
25
;
var
boxWidth
=
25
;
var
startX
=
(
theCanvas
.
width

100
);
var
startY
=
(
theCanvas
.
height

boxHeight
)

100
for
(
var
i
=
0
;
i
<
numBoxes
;
i
++
)
{
var
boxDef
=
new
b2BodyDef
;
boxDef
.
type
=
b2Body
.
b2_dynamicBody
;
var
ypos
=
(
startY

(
i
*
boxHeight
))
/
scaleFactor
;
var
xpos
=
(
startX
)
/
scaleFactor
;
boxDef
.
position
.
Set
(
xpos
,
ypos
);
var
newBox
=
world
.
CreateBody
(
boxDef
)
var
boxFixture
=
new
b2FixtureDef
;
boxFixture
.
density
=
20.0
;
boxFixture
.
friction
=
.
5
;
boxFixture
.
restitution
=
.
1
;
boxFixture
.
shape
=
new
b2PolygonShape
;
boxFixture
.
shape
.
SetAsBox
((
boxWidth
/
scaleFactor
)
/
2
,
(
boxHeight
/
scaleFactor
)
/
2
);
newBox
.
CreateFixture
(
boxFixture
);
newBox
.
SetUserData
({
width
:
boxWidth
/
scaleFactor
,
height
:
boxHeight
/
scaleFactor
})
boxes
.
push
(
newBox
);
}
var
wallDefs
=
new
Array
(
{
x
:
theCanvas
.
width
,
y
:
0
,
w
:
theCanvas
.
width
,
h
:
1
},
//top
{
x
:
theCanvas
.
width
,
y
:
theCanvas
.
height
,
w
:
theCanvas
.
width
,
h
:
1
},
//bottom
{
x
:
0
,
y
:
theCanvas
.
height
,
w
:
1
,
h
:
theCanvas
.
height
},
//left
{
x
:
theCanvas
.
width
,
y
:
theCanvas
.
height
,
w
:
1
,
h
:
theCanvas
.
height
}
);
//right
var
walls
=
new
Array
();
for
(
var
i
=
0
;
i
<
wallDefs
.
length
;
i
++
)
{
var
wallDef
=
new
b2BodyDef
;
wallDef
.
type
=
b2Body
.
b2_staticBody
;
wallDef
.
position
.
Set
(
wallDefs
[
i
].
x
/
scaleFactor
,
wallDefs
[
i
].
y
/
scaleFactor
);
var
newWall
=
world
.
CreateBody
(
wallDef
)
var
wallFixture
=
new
b2FixtureDef
;
wallFixture
.
density
=
10.0
;
wallFixture
.
friction
=
0.5
;
wallFixture
.
restitution
=
.
5
;
wallFixture
.
shape
=
new
b2PolygonShape
;
wallFixture
.
shape
.
SetAsBox
(
wallDefs
[
i
].
w
/
scaleFactor
,
wallDefs
[
i
].
h
/
scaleFactor
);
newWall
.
CreateFixture
(
wallFixture
);
walls
.
push
(
newWall
);
}
var
debugDraw
=
new
b2DebugDraw
();
debugDraw
.
SetSprite
(
context2
);
debugDraw
.
SetDrawScale
(
scaleFactor
);
//define scale
debugDraw
.
SetFillAlpha
(
0.3
);
//define transparency
debugDraw
.
SetLineThickness
(
1.0
);
debugDraw
.
SetFlags
(
b2DebugDraw
.
e_shapeBit

b2DebugDraw
.
e_jointBit
);
world
.
SetDebugDraw
(
debugDraw
);
function
gameLoop
()
{
window
.
setTimeout
(
gameLoop
,
20
);
drawScreen
()
}
gameLoop
();
}
<
/script>
</head>
<body>
<canvas
id=
"canvasOne"
width=
"450"
height=
"350"
style=
"position: absolute;
top: 0px; left: 0px;"
>
Your browser does not support the HTML 5 Canvas.</canvas>
<canvas
id=
"canvasTwo"
width=
"450"
height=
"350"
style=
"position: absolute;
top: 0px; left: 451px;"
>
Your browser does not support the HTML 5 Canvas.</canvas>
</body>
</html>
As you can see, Box2D has a steep learning curve, but after you get the hang of it, relatively minor changes to your code can create some very cool applications. We have covered only a very small percentage of what can be accomplished with Box2D. Here are some resources to continue your Box2D journey:
We have shown you a plethora of examples for how you can use HTML5 Canvas to animate objects using some basic principles of math, physics, and Box2D. However, we have really only begun to scratch the surface of the multitude of ways that you can use math and physics in your applications. In the next couple chapters, we will switch gears, discussing audio and video, before we start applying many of the concepts we have learned in this book to a couple of indepth game projects.