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 math-based 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 math-based animations.
For the simplest kinds of animations—moving objects in a straight
line up and down the canvas—this 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) 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 33
milliseconds. This is the loop we need to update our objects and move them
around the canvas:
setInterval(drawScreen, 33);
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 value of x
and y
. Since 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 5-1 shows the complete code needed to create basic movement in a straight line.
Example 5-1. Moving in a straight line
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CH5EX1: Moving In A Straight Line</title> <script src="modernizr-1.6.min.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; setInterval(drawScreen, 33); } </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 of 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 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 line 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 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 Equation 5-1. 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 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 re-create the distance equation in Equation 5-1. 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
, 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
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:
setInterval(drawScreen, 33);
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 5-2. 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 would 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, that 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 immediate-mode 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 5-1,
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 5-2 is the full code listing for CH5EX2.html.
Example 5-2. Moving on a simple line
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CH5EX2: Moving On A Simple Line</title> <script src="modernizr-1.6.min.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"); setInterval(drawScreen, 33); } </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
of the moving object, and
the direction will be an angle
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
(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
(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. So, we
will work with angles, but we still need to convert our 45-degree 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.
You can see how these values relate to a 45-degree angle in Figure 5-2.
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
. 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:
ball.x += xunits; ball.y += yunits;
Figure 5-3 shows what Example 5-3 looks like when it is executed in a web browser. Recall that the points are drawn for illustration only.
Example 5-3 gives the full code listing.
Example 5-3. Moving on a vector
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CH5EX3: Moving On A Vector</title> <script src="modernizr-1.6.min.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"); setInterval(drawScreen, 33); } </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>