The drawBlock function we wrote last time lets us draw a block at any point on the screen, but it looks pretty flat. If you look at most Tetris games, the blocks have a beveled edge to make them appear three dimensional. We can add this purely stylistic effect to our game to make it look nicer, and it will introduce a few more drawing concepts as well. A beveled edge will have the same base color as the rest of the block, but will appear lighter or darker to simulate different lighting angles. Unfortunately, the RGB colorspace is not well-suited to adjusting the lightness of specific hues. For this, it is easier to use the HSL colorspace. This defines a hue as an angle on a color wheel in the range [0,360], a saturation value between 0 and 1, and a lightness value between 0 and 1. Luckily, we can easily define colors in the HSL colorspace. We’ll modify the drawBlock function to accept a third color hue parameter and use this as the basis for our drawing commands. The updated code looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
<!DOCTYPE html> <html> <head> <title>Tetris</title> <script> var ctx; //Canvas object /************************************************ Initialize the drawing canvas ************************************************/ function initialize() { //Get the canvas context object from the body c = document.getElementById("myCanvas"); ctx = c.getContext("2d"); //Draw blocks in the four corners of the canvas drawBlock(0,0,0); drawBlock(9,0,0); drawBlock(0,19,0); drawBlock(9,19,0); } /************************************************ Draws a block at the specified game coordinate x = [0,9] x-coordinate y = [0,19] y-coordinate c = [0,360] color hue ************************************************/ function drawBlock(x, y, c) { //Convert game coordinaes to pixel coordinates pixelX = x*20; pixelY = (19-y)*20; /**** Draw the center part of the block ****/ //Set the fill color using the supplied color ctx.fillStyle = "hsl(" + c + ",100%,50%)"; //Create a filled rectangle ctx.fillRect(pixelX+2,pixelY+2,16,16); /**** Draw the top part of the block ****/ //Set the fill color slightly lighter ctx.fillStyle = "hsl(" + c + ",100%,70%)"; //Create the top polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX,pixelY); ctx.lineTo(pixelX+20,pixelY); ctx.lineTo(pixelX+18,pixelY+2); ctx.lineTo(pixelX+2,pixelY+2); ctx.fill(); /**** Draw the sides of the block ****/ //Set the fill color slightly darker ctx.fillStyle = "hsl(" + c + ",100%,40%)"; //Create the left polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX,pixelY); ctx.lineTo(pixelX,pixelY+20); ctx.lineTo(pixelX+2,pixelY+18); ctx.lineTo(pixelX+2,pixelY+2); ctx.fill(); //Create the right polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX+20,pixelY); ctx.lineTo(pixelX+20,pixelY+20); ctx.lineTo(pixelX+18,pixelY+18); ctx.lineTo(pixelX+18,pixelY+2); ctx.fill(); /**** Draw the bottom part of the block ****/ //Set the fill color much darker ctx.fillStyle = "hsl(" + c + ",100%,30%)"; //Create the bottom polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX,pixelY+20); ctx.lineTo(pixelX+20,pixelY+20); ctx.lineTo(pixelX+18,pixelY+18); ctx.lineTo(pixelX+2,pixelY+18); ctx.fill(); } </script> <body onload="initialize();" style="background-color:#EEEEEE"> <canvas id="myCanvas" height="400px" width="200px" style="background-color:#444444"></canvas> <div style="width:200px;background-color:#CCCCCC"> Score: 0 </div> </body> </html> |
The color of the center part of the block is set with the command
1 |
ctx.fillStyle = "hsl(" + c + ",100%,50%)"; |
This uses the HSL colorspace, using the variable c for the hue, and 100% saturation and 50% lightness. Technically, we are constructing a string by using the + operator before and after c. If c = 0, this expression is equivalent to
1 |
ctx.fillStyle = "hsl(0,100%,50%)"; |
After drawing the center square with the appropriate offsets, we draw each of the beveled edges with a different value for the lightness parameter. Each side is a trapezoid, which we define as a path of line segments. Each trapezoid starts with the command
1 |
ctx.beginPath(); |
followed by
1 |
ctx.moveTo(x,y); |
to indicate the start point. Lines are added my moving the drawing cursor to new points with the command
1 |
ctx.lineTo(x,y); |
finally, the polygon is closed and filled with the current color with the command
1 |
ctx.fill(); |
To draw individual tetriminos, we will create another function that takes a game coordinate, a tetrimino type, and an orientation, and draws it on the screen. Tetris has seven different tetriminos, each consisting of a different arrangement of four blocks.
In the game, tetriminos appear with various orientations, so our function needs to take that into account. Although there is probably an elegant mathematical way to define the tetriminos with their various orientations, I’m going to approach this problem in the simple and explicit way. Our function will use a series of conditional statements to determine which tetrimino to draw and in what orientation. The block locations and colors are explicitly defined relative to the supplied game coordinate. The full code listing is given below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 |
<!DOCTYPE html> <html> <head> <title>Tetris</title> <script> var ctx; //Canvas object /************************************************ Initialize the drawing canvas ************************************************/ function initialize() { //Get the canvas context object from the body c = document.getElementById("myCanvas"); ctx = c.getContext("2d"); //Draw test tetriminos drawTetrimino(1,2,0,0); drawTetrimino(3,2,0,1); drawTetrimino(6,2,0,2); drawTetrimino(9,2,0,3); drawTetrimino(1,5,1,0); drawTetrimino(3,5,1,1); drawTetrimino(6,5,1,2); drawTetrimino(9,5,1,3); drawTetrimino(1,8,2,0); drawTetrimino(3,8,2,1); drawTetrimino(6,8,2,2); drawTetrimino(9,8,2,3); drawTetrimino(0,11,3,0); drawTetrimino(2,11,3,1); drawTetrimino(4,11,3,2); drawTetrimino(6,11,3,3); drawTetrimino(1,13,4,0); drawTetrimino(3,13,4,1); drawTetrimino(6,13,4,2); drawTetrimino(9,13,4,3); drawTetrimino(1,16,5,0); drawTetrimino(3,16,5,1); drawTetrimino(6,16,5,2); drawTetrimino(9,16,5,3); drawTetrimino(1,18,6,0); drawTetrimino(4,19,6,2); drawTetrimino(6,18,6,1); drawTetrimino(9,18,6,3); } /************************************************ Draws a block at the specified game coordinate x = [0,9] x-coordinate y = [0,19] y-coordinate c = [0,360] color hue ************************************************/ function drawBlock(x, y, c) { //Convert game coordinaes to pixel coordinates pixelX = x*20; pixelY = (19-y)*20; /**** Draw the center part of the block ****/ //Set the fill color using the supplied color ctx.fillStyle = "hsl(" + c + ",100%,50%)"; //Create a filled rectangle ctx.fillRect(pixelX+2,pixelY+2,16,16); /**** Draw the top part of the block ****/ //Set the fill color slightly lighter ctx.fillStyle = "hsl(" + c + ",100%,70%)"; //Create the top polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX,pixelY); ctx.lineTo(pixelX+20,pixelY); ctx.lineTo(pixelX+18,pixelY+2); ctx.lineTo(pixelX+2,pixelY+2); ctx.fill(); /**** Draw the sides of the block ****/ //Set the fill color slightly darker ctx.fillStyle = "hsl(" + c + ",100%,40%)"; //Create the left polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX,pixelY); ctx.lineTo(pixelX,pixelY+20); ctx.lineTo(pixelX+2,pixelY+18); ctx.lineTo(pixelX+2,pixelY+2); ctx.fill(); //Create the right polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX+20,pixelY); ctx.lineTo(pixelX+20,pixelY+20); ctx.lineTo(pixelX+18,pixelY+18); ctx.lineTo(pixelX+18,pixelY+2); ctx.fill(); /**** Draw the bottom part of the block ****/ //Set the fill color much darker ctx.fillStyle = "hsl(" + c + ",100%,30%)"; //Create the bottom polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX,pixelY+20); ctx.lineTo(pixelX+20,pixelY+20); ctx.lineTo(pixelX+18,pixelY+18); ctx.lineTo(pixelX+2,pixelY+18); ctx.fill(); } /************************************************* Draws a tetrimino at the specified game coordinate with the specified orientation x = [0,9] x-coordinate y = [0,19] y-coordinate t = [0,6] tetrimino type o = [0,3] orientation *************************************************/ function drawTetrimino(x,y,t,o) { /**** Pick the appropriate tetrimino type ****/ if(t == 0) { //I Type c = 180; //Cyan //Get orientation if(o == 0) { drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x+1,y,c); drawBlock(x+2,y,c); } else if(o == 1) { drawBlock(x+1,y+1,c); drawBlock(x+1,y,c); drawBlock(x+1,y-1,c); drawBlock(x+1,y-2,c); } else if(o == 2) { drawBlock(x-1,y-1,c); drawBlock(x,y-1,c); drawBlock(x+1,y-1,c); drawBlock(x+2,y-1,c); } else if(o == 3) { drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x,y-1,c); drawBlock(x,y-2,c); } } if(t == 1) { //J Type c = 240; //Blue //Get orientation if(o == 0) { drawBlock(x-1,y+1,c); drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x+1,y,c); } else if(o == 1) { drawBlock(x+1,y+1,c); drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x,y-1,c); } else if(o == 2) { drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x+1,y,c); drawBlock(x+1,y-1,c); } else if(o == 3) { drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x,y-1,c); drawBlock(x-1,y-1,c); } } if(t == 2) { //L Type c = 40; //Orange //Get orientation if(o == 0) { drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x+1,y,c); drawBlock(x+1,y+1,c); } else if(o == 1) { drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x,y-1,c); drawBlock(x+1,y-1,c); } else if(o == 2) { drawBlock(x-1,y-1,c); drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x+1,y,c); } else if(o == 3) { drawBlock(x-1,y+1,c); drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x,y-1,c); } } if(t == 3) { //O Type c = 60; //Yellow //Orientation doesn't matter drawBlock(x,y,c); drawBlock(x+1,y,c); drawBlock(x,y-1,c); drawBlock(x+1,y-1,c); } if(t == 4) { //S Type c = 120; //Green //Get orientation if(o == 0) { drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x,y+1,c); drawBlock(x+1,y+1,c); } else if(o == 1) { drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x+1,y,c); drawBlock(x+1,y-1,c); } else if(o == 2) { drawBlock(x-1,y-1,c); drawBlock(x,y-1,c); drawBlock(x,y,c); drawBlock(x+1,y,c); } else if(o == 3) { drawBlock(x-1,y+1,c); drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x,y-1,c); } } if(t == 5) { //T Type c = 280; //Purple //Get orientation if(o == 0) { drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x+1,y,c); drawBlock(x,y+1,c); } else if(o == 1) { drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x,y-1,c); drawBlock(x+1,y,c); } else if(o == 2) { drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x+1,y,c); drawBlock(x,y-1,c); } else if(o == 3) { drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x,y-1,c); drawBlock(x-1,y,c); } } if(t == 6) { //Z Type c = 0; //Red //Get orientation if(o == 0) { drawBlock(x-1,y+1,c); drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x+1,y,c); } else if(o == 1) { drawBlock(x+1,y+1,c); drawBlock(x+1,y,c); drawBlock(x,y,c); drawBlock(x,y-1,c); } else if(o == 2) { drawBlock(x-1,y,c); drawBlock(x,y,c); drawBlock(x,y-1,c); drawBlock(x+1,y-1,c); } else if(o == 3) { drawBlock(x,y+1,c); drawBlock(x,y,c); drawBlock(x-1,y,c); drawBlock(x-1,y-1,c); } } } </script> <body onload="initialize();" style="background-color:#EEEEEE"> <canvas id="myCanvas" height="400px" width="200px" style="background-color:#444444"></canvas> <div style="width:200px;background-color:#CCCCCC"> Score: 0 </div> </body> </html> |
If you’ve never seen it before, this code introduces the if-else conditional statement. It’s central to all programming languages and allows the program to execute different blocks of code depending on certain conditions. Here, we have an outer layer of if-else statements that checks which tetrimino is being drawn. Note that we represent the tetrimino as a number between 0 and 6 and use the double equal ( ==) to check for equality with the input parameter. One of the blocks will be executed, where in most cases we repeat the process, checking the orientation. Finally, one set of four drawBlock statements will be executed, drawing the appropriate tetrimino. The initialize function draws several test tetriminos, that we can use to validate that our code is correct. The output should look something like this.
The code is getting pretty long now, and we haven’t even added any interactivity yet. You can probably start to see how complicated a complete game can be. I’ll try to keep things as simple as possible while still covering all of the necessary parts. Next time we’ll start working on adding controls to move the tetriminos around.