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!
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:
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.
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.
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:
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:
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:
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().
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:
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.
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.
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;
<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
};
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);
};
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);
};
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;
}
};
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>
</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;
};
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.
No comments:
Post a Comment