In the previous post I've talked about some introductory techniques and good habits. Let's expand and add to that. In this tutorial I will focus on making an animation on canvas that is a little bit more complex than just spawning random circles here and there. This kind of tasks may seem silly and useless but they are a good piece of learning curve. They help you get more comfortable with the language, get used to syntax, and certain techniques. You can think of a different exercise and do that rather than doing exactly this one - this is just what I came up with. Besides, these type of exercises are fun to do and you get a nice visual output :) So don't shy away from doing this type of stuff!
Task 1
I want to make a square board which I will fill with black squares going in a spiral. I want the leading square to be red. I want the animation to stop as soon as the whole board is filled. I want it to work regardless of the board size and the unit square size. If my board has an even number of squares I want the central one to be twice as big. If it's an odd number I want it to be normal size. (To imagine that better you probably should copy the code in order and run it and see for yourself.) Let's get started!
In general, it's good to keep all your variables and data in the
var model={}; object. (If you are new to JavaScript, you should probably read up on that first. A couple simple things though - there are no classes but there are objects; arrays are a sub case of objects; functions are values.) There might be a couple top level variables like
c and
ctx and maybe a couple others for your own convenience. For instance, maybe I want to make a square board and manually set the size of the board and the unit square at the very beginning. Like this:
<html>
<head>
<title>Spiraling Square</title>
<script type="text/javascript">
var c, ctx;
var unitSquare = 20;
var boardSize = 15;
In my code things will depend on those variables but I will never alter them - they are kind of static and I arbitrarily decide how big or small I want the board and the unit square to be. So I set them at the very beginning and if I want to change it my calculations in the code will not suffer from it. You can set it to any natural number.
Now let's make our model object.
var model = {
count : 0 ,
x : -1 * unitSquare ,
y : 0,
size : unitSquare,
N : boardSize * unitSquare
};
model.count variable is for me to know which loop of the spiral I'm on.
model.x and
model.y are the coordinates for the squares to be drawn at.
model.size by default is the size of the
unitSquare (if
boardSize is odd i will double the
model.size later).
model.N is the actual size of the board (in pixels) based on what you set your first 2 vars to.
Now notice that
model.x is set to -
20 in this instance. This is because I call
update() first and then draw things out. I will come back to this later.
Now let's make our
window.onload function.
window.onload = function() {
c = document.getElementById("myCanvas");
c.width = model.N;
c.height = model.N;
ctx = c.getContext("2d");
setTimeout(draw, 20);
};
As you can see I'm actually setting the canvas
width and
height here rather than later. This way your canvas will be as big as your board.
Now the
draw() function:
var draw = function(){
update();
ctx.fillStyle = "#000000";
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "#ff0000";
ctx.rect(model.x , model.y , model.size , model.size);
ctx.fill();
setTimeout(draw, 20);
};
Call
update() at the beginning, set fill color to black and use it. This will fill all your previous shapes with black. So everything that's drawn on the board prior to whatever you're drawing right now will be of the same color. Then I set color to red and draw the leading square - at
model.x , model.y coordinates and with the
model.size of
unitSquare (at the moment..)
ctx.beginPath(); is important there - it separates your leading square from everything else that was drawn before.
And the
update() function:
var update = function(){
if ((model.x==model.y)&&(boardSize % 2 == 0)&&(model.x==model.N/2-unitSquare))
model.size=2*unitSquare;
else if((model.x <= model.N-2*unitSquare-model.count)&&(model.y==model.count))
model.x+=unitSquare;
else if ((model.x == model.N-unitSquare-model.count)&&(model.y<=model.N-2*unitSquare-model.count))
model.y+=unitSquare;
else if((model.y == model.N-unitSquare-model.count)&&(model.x>=model.count+unitSquare))
model.x-=unitSquare;
else if((model.x==model.count)&&(model.y>=unitSquare+model.count))
{
model.y-=unitSquare;
if((model.y==model.count+unitSquare)&&(model.count<model.N/2-unitSquare))
model.count+=unitSquare;
}
};
That might looks confusing at first. But notice that there are no loops anywhere in the code and
draw() only draws things and
update() only alters the values.
First
if is checking if our
model.x and
model.y are the same and if they are equal to the central coordinates of the board and if our board has an even number of squares. If that condition is met we double the
model.size of our leading square. So this happens only when we reach the center and the board is even.
The 4 "
else if"s look at where we are on the board, what cycle of the spiral we are on, and what direction we're going in.
going Right:
model.x+=unitSquare, leave
model.y alone. We go
right until
model.x gets to the maximum it can be. As soon as
model.x is at max we change the direction to
down.
going Down:
model.y+=unitSquare, leave
model.x alone. We go
down until
model.y gets to the maximum it can be. As soon as
model.y is at max we change the direction to
left.
going Left:
model.x-=unitSquare, leave
model.y alone. We go
left until
model.x gets to the minimum it can be. As soon as
model.x is at min we change the direction to
up.
going Up:
model.y-=unitSquare, leave
model.x alone. We go
up until
model.y gets to the minimum it can be. As soon as
model.y is at min we change the direction to
right thus completing the current cycle of the spiral or current square. The
if condition nested here is for taking care of
model.count As soon as we complete the current spiral cycle we increment the
model.count by
unitSquare.
Now remember how initially I set
model.y to
0 and
model.x to -
1*unitSquare? That is because the very first thing that happens is we get into
go right conditional in
update() which will increase
model.x by a
unitSquare, thus, bringing it to
0. And after that
draw() will draw this out - literally it will draw the first square at
0,
0 then go into
update() again, draw the next square at
0,
20 and so on. (
20 being our
unitSquare).
Finally, last bit of code:
</script>
</head>
<body style="padding: 0 ; margin: 0">
<canvas id="myCanvas" style="border: 5px solid red" tabindex="1">
</canvas>
</body>
</html>
The math behind it:
It is important to keep in mind that the coordinates you're dealing with are for the top left of your
unitSquare. So, for example, during going
right:
0 <=
model.x <=
300-
20-
0 for the first line,
20 <=
model.x <=
300-
20-
20 for the second line
...
model.count <=
model.x <=
model.N -
unitSquare -
model.count
so for going right my condition is
model.x <=
model.N -
2*unitSquare -
model.count
and
model.y ==
model.count
If these conditions are met
model.x +=
unitSquare.
so for CONDITION
model.x <=
model.N -
2*unitSquare -
model.count OR you can write it as
model.x <
model.N -
unitSquare -
model.count because if that's met
model.x +=
unitSquare thus bringing it to
model.N -
unitSquare -
model.count on the other hand i don't need to worry about where
model.x starts because
model.y==
model.count means we just changed the direction from
up to
right and
model.x min was set in the previous run of
update().
Task 2
So let's complicate things a little bit. Now let's say I want the spiral to leave empty corners on each cycle - basically I want the 2 diagonals to be seen and for them to meet in the center with a red square.
For that, leave everything as is except the
update() function which should look like this:
var update = function(){
if((model.x <= model.N-2*unitSquare-model.count)&&(model.y==model.count))
model.x+=unitSquare;
if ((model.x == model.N-unitSquare-model.count)&&(model.y<=model.N-2*unitSquare-model.count))
model.y+=unitSquare;
if((model.y == model.N-unitSquare-model.count)&&(model.x>=model.count+unitSquare))
model.x-=unitSquare;
if((model.x==model.count)&&(model.y>=unitSquare+model.count))
{
model.y-=unitSquare;
if((model.y==model.count+unitSquare)&&(model.count<model.N/2-unitSquare))
model.count+=unitSquare;
}
if(model.x==model.y)
if(boardSize % 2 == 0)
if(model.x==model.N/2-unitSquare)
model.size=2*unitSquare;
else
model.x+=unitSquare;
else if(model.x<=model.N/2-unitSquare)
model.x+=unitSquare;
};
So, pretty similar. Just no "
else"s and slightly different rules for what happens when
model.x==model.y. In there we look if
boardSize is even or odd. If it is even and if
model.x is in the center we double the size of the leading square otherwise we increase
model.x by
unitSquare. And if the
boardSize is odd then we just increase
model.x by
unitSquare. Without this bit
model.x==model.y half of diagonal would be filled with black squares. So this way we tell it to skip over. The lack of "
else"s takes care of the other 3 parts of the diagonals.
The end.
In part 3 of these tutorial series I will show you how to make the snake game. That will be a lot more work but a lot more rewarding, too.