├── .gitignore ├── img ├── help.png ├── pitch3.png ├── splash.png ├── awayPlayer.png ├── background.png └── homePlayer.png ├── js ├── awayPlayer.png ├── homePlayer.png ├── football.js └── SAT.js ├── README.md ├── LICENSE ├── index.html └── css └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /img/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pea1bee/football-game/HEAD/img/help.png -------------------------------------------------------------------------------- /img/pitch3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pea1bee/football-game/HEAD/img/pitch3.png -------------------------------------------------------------------------------- /img/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pea1bee/football-game/HEAD/img/splash.png -------------------------------------------------------------------------------- /img/awayPlayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pea1bee/football-game/HEAD/img/awayPlayer.png -------------------------------------------------------------------------------- /img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pea1bee/football-game/HEAD/img/background.png -------------------------------------------------------------------------------- /img/homePlayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pea1bee/football-game/HEAD/img/homePlayer.png -------------------------------------------------------------------------------- /js/awayPlayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pea1bee/football-game/HEAD/js/awayPlayer.png -------------------------------------------------------------------------------- /js/homePlayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pea1bee/football-game/HEAD/js/homePlayer.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # football-game 2 | A foosball game written in vanilla javascript using HTML5 canvas. 3 | 4 | You can play the game: http://prashantbaid.github.io/football-game 5 | 6 | # License 7 | MIT License 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Prashant Baid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Foosball Game 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 |
14 |
FoosBall
15 | 16 |


17 | 18 |
19 | (Use left and right arrow keys to play) 20 | 21 |
22 | 23 |
24 |
FoosBall
25 | 26 |


27 |
28 | 29 | 30 | 31 |
32 |
33 | Time Left 34 |
02:00
35 |
36 |

Juventus

37 |
0
38 |

Liverpool

39 |
0
40 |
41 |
Created By
@prashantbaid
42 |
43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | #container { 2 | width: 720px; 3 | height: 720px; 4 | border-style: solid; 5 | border-width: 10px; 6 | margin: 0 auto; 7 | position: relative; 8 | } 9 | canvas { 10 | background: url("../img/pitch3.png") center; 11 | float: left; 12 | z-index: 1; 13 | } 14 | #startScreen { 15 | z-index: 2; 16 | width: 720px; 17 | height: 720px; 18 | background-image: url('../img/splash.png'); 19 | position: absolute; 20 | font-family: verdana; 21 | color: white; 22 | } 23 | #gameOverScreen { 24 | z-index: -1; 25 | width: 480px; 26 | height: 720px; 27 | background-image: url('../img/splash.png'); 28 | position: absolute; 29 | font-family: verdana; 30 | color: white; 31 | } 32 | #score { 33 | background-image: url('../img/background.png'); 34 | float: right; 35 | width: 240px; 36 | height: 690px; 37 | font-family: verdana; 38 | text-align: center; 39 | padding-top: 30px; 40 | z-index: 1; 41 | } 42 | #blue { 43 | color: #919191; 44 | } 45 | #red { 46 | color: #E03333; 47 | } 48 | #home, 49 | #away { 50 | font-size: 48px; 51 | font-weight: bold; 52 | } 53 | #timer { 54 | padding-bottom: 100px; 55 | font-size: 28px; 56 | font-weight: bold; 57 | } 58 | #credits { 59 | padding-top: 100px; 60 | font-size: 12px; 61 | font-weight: bold; 62 | position: fixed; 63 | height: 50px; 64 | bottom: 0px; 65 | left: 0px; 66 | right: 0px; 67 | margin-bottom: 50px; 68 | margin-left: 475px; 69 | } 70 | a { 71 | text-decoration: none; 72 | color: #01BBE4; 73 | } 74 | #title { 75 | font-size: 50px; 76 | font-weight: bold; 77 | font-family: verdana; 78 | color: white; 79 | text-align: center; 80 | padding-top: 100px; 81 | } 82 | #play { 83 | margin-left: 300px; 84 | margin-top: 100px; 85 | width: auto; 86 | margin-bottom: 100px; 87 | padding: 20px 35px; 88 | background: white; 89 | border: 0; 90 | font-size: 20px; 91 | color: #424242; 92 | font-weight: bold; 93 | font-family: verdana; 94 | border-radius: 4px; 95 | margin-bottom: 10px; 96 | } 97 | #playagain { 98 | margin-left: 140px; 99 | margin-top: 100px; 100 | width: auto; 101 | margin-bottom: 100px; 102 | padding: 20px 35px; 103 | background: white; 104 | border: 0; 105 | font-size: 20px; 106 | color: #424242; 107 | font-weight: bold; 108 | font-family: verdana; 109 | border-radius: 4px; 110 | margin-bottom: 10px; 111 | } 112 | #help { 113 | padding-left: 285px; 114 | opacity: 0.7; 115 | } 116 | #instructions { 117 | font-size: 12px; 118 | padding-left: 250px; 119 | text-align: center; 120 | opacity: 0.7; 121 | } 122 | #right { 123 | padding-left: 70px; 124 | font-size: 12px; 125 | text-align: center; 126 | opacity: 0.7; 127 | } 128 | #foot { 129 | margin-top: 160px; 130 | text-align: center; 131 | font-size: 10px; 132 | opacity: 0.5; 133 | } 134 | #status { 135 | margin-top: 100px; 136 | font-weight: bold; 137 | font-size: 20px; 138 | } -------------------------------------------------------------------------------- /js/football.js: -------------------------------------------------------------------------------- 1 | //build canvas 2 | var canvas = document.getElementById("myCanvas"); 3 | var ctx = canvas.getContext("2d"); 4 | 5 | //set initial ball location 6 | var x = canvas.width/2; 7 | var y = canvas.height/2; 8 | 9 | //set ball radius 10 | var ballRadius = 6; 11 | 12 | //set ball speed 13 | var dx = 3; 14 | var dy = -3; 15 | 16 | //initialize ball speed 17 | var m = 0; 18 | var j = 0; 19 | 20 | var aiSpeed = 1.25; 21 | 22 | //set paddle dimensions 23 | var paddleHeight = 10; 24 | var paddleWidth = 30; 25 | 26 | var paddleX = (canvas.width-paddleWidth); 27 | 28 | //initialize keypress status 29 | var rightPressed = false; 30 | var leftPressed = false; 31 | 32 | //set goalpost dimensions 33 | var goalpostWidth = 150; 34 | var goalpostHeight = 10; 35 | 36 | //initialize scorecard 37 | var homeScore = 0; 38 | var awayScore = 0; 39 | 40 | //set player dimensions 41 | var playerHeight = 50; 42 | var playerWidth = 30; 43 | 44 | 45 | //set flags 46 | var initFlag = true; 47 | var gameOver = false; 48 | var flag1 = 1; 49 | var flag2 = 1; 50 | var drawFlag = true; 51 | 52 | //register for keypress events 53 | document.addEventListener("keydown", keyDownHandler, false); 54 | document.addEventListener("keyup", keyUpHandler, false); 55 | 56 | 57 | //initialize SAT.js variables 58 | var V = SAT.Vector; 59 | var C = SAT.Circle; 60 | var B = SAT.Box; 61 | 62 | var circle; 63 | var box; 64 | 65 | //initialize images 66 | var homePlayer = new Image(); 67 | var awayPlayer = new Image(); 68 | 69 | 70 | //it all starts here 71 | function init() { 72 | removeStatus(); 73 | homePlayer.src = 'img/homePlayer.png'; 74 | awayPlayer.src = 'img/awayPlayer.png'; 75 | document.getElementById('startScreen').style['z-index'] = '-1'; 76 | document.getElementById('gameOverScreen').style['z-index'] = '-1'; 77 | document.getElementById('home').innerHTML = '0'; 78 | document.getElementById('away').innerHTML = '0'; 79 | awayScore = 0; 80 | homeScore = 0; 81 | gameOver = 0; 82 | setInitialDelay(); 83 | } 84 | 85 | function setInitialDelay() { 86 | setTimeout(function() { 87 | startTimer(60 * 2); 88 | drawFlag = true; 89 | window.requestAnimationFrame(draw); 90 | updateStatus('You are team
in RED'); 91 | }, 1500); 92 | } 93 | 94 | function setDelay() { 95 | setTimeout(function() { 96 | drawFlag = true; 97 | window.requestAnimationFrame(draw); 98 | }, 1500); 99 | } 100 | 101 | function startTimer(duration) { 102 | var timer = duration, 103 | minutes, seconds; 104 | countdown = setInterval(function() { 105 | minutes = parseInt(timer / 60, 10) 106 | seconds = parseInt(timer % 60, 10); 107 | 108 | minutes = minutes < 10 ? "0" + minutes : minutes; 109 | seconds = seconds < 10 ? "0" + seconds : seconds; 110 | 111 | document.getElementById('countdown').innerHTML = minutes + ":" + seconds; 112 | 113 | if (--timer < 0) { 114 | document.getElementById('gameOverScreen').style['z-index'] = 3; 115 | gameOver = true; 116 | clearInterval(countdown); 117 | if (homeScore > awayScore) 118 | updateStatus('GAME OVER!
Liverpool Won!'); 119 | else if (awayScore > homeScore) 120 | updateStatus('GAME OVER!
Juventus Won!'); 121 | else 122 | updateStatus('GAME OVER!
Draw!') 123 | } 124 | }, 1000); 125 | } 126 | 127 | //it all happens here 128 | function draw() { 129 | ctx.clearRect(0, 0, canvas.width, canvas.height); 130 | drawBall(); 131 | drawPlayers(); 132 | drawGoalPost(); 133 | x += dx; 134 | y += dy; 135 | if (rightPressed && paddleX * 3 / 4 + m < canvas.width - paddleWidth) { 136 | m += 2; 137 | } else if (leftPressed && paddleX / 4 + m > 0) { 138 | m -= 2; 139 | } 140 | if (drawFlag && !gameOver) 141 | window.requestAnimationFrame(draw); 142 | } 143 | 144 | 145 | function drawBall() { 146 | ctx.beginPath(); 147 | ctx.arc(x, y, ballRadius, 0, Math.PI * 2); 148 | ctx.fillStyle = "white"; 149 | ctx.fill(); 150 | ctx.closePath(); 151 | circle = new C(new V(x, y), 6); 152 | if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) { 153 | dx = -dx; 154 | if(x<0) 155 | x=0; 156 | if(x>canvas.width) 157 | x = canvas.width; 158 | } 159 | if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) { 160 | dy = -dy; 161 | } 162 | 163 | } 164 | 165 | function drawPlayers() { 166 | drawHomeTeam(); 167 | drawAwayTeam(); 168 | 169 | } 170 | 171 | function drawHomeTeam() { 172 | //home 173 | drawGoalkeeper(); 174 | drawDefenders(); 175 | drawMidfielders(); 176 | drawStrikers(); 177 | } 178 | 179 | function drawAwayTeam() { 180 | //away 181 | drawAwayGoalkeeper(); 182 | drawAwayDefenders(); 183 | drawAwayMidfielders(); 184 | drawAwayStrikers(); 185 | } 186 | 187 | function drawGoalPost() { 188 | 189 | //home 190 | ctx.beginPath(); 191 | var gphX = (canvas.width - goalpostWidth) / 2; 192 | var gphY = canvas.height - goalpostHeight; 193 | ctx.rect(gphX, gphY, goalpostWidth, goalpostHeight); 194 | ctx.fillStyle = "#9C9C9C"; 195 | ctx.fill(); 196 | ctx.closePath(); 197 | box = new B(new V(gphX, gphY), goalpostWidth, goalpostHeight).toPolygon(); 198 | if (goalDetection(box)) { 199 | updateScore('home'); 200 | updateStatus('GOAL!
Juventus Score!'); 201 | removeStatus(); 202 | resetBall(); 203 | setDelay(); 204 | } 205 | //away 206 | ctx.beginPath(); 207 | var gpaX = (canvas.width - goalpostWidth) / 2; 208 | var gpaY = paddleHeight - goalpostHeight; 209 | ctx.rect(gpaX, gpaY, goalpostWidth, goalpostHeight); 210 | ctx.fillStyle = "#9C9C9C"; 211 | ctx.fill(); 212 | ctx.closePath(); 213 | 214 | box = new B(new V(gpaX, gpaY), goalpostWidth, goalpostHeight).toPolygon(); 215 | if (goalDetection(box)) { 216 | updateScore('away'); 217 | updateStatus('GOAL!
Liverpool Score!'); 218 | removeStatus(); 219 | resetBall(); 220 | setDelay(); 221 | } 222 | } 223 | 224 | 225 | function updateScore(goal) { 226 | 227 | if (goal === 'home') { 228 | awayScore += 1; 229 | document.getElementById('away').innerHTML = awayScore; 230 | } else { 231 | homeScore += 1; 232 | document.getElementById('home').innerHTML = homeScore; 233 | } 234 | } 235 | 236 | function resetBall() { 237 | x = canvas.width / 2; 238 | y = canvas.height / 2; 239 | drawBall(); 240 | drawFlag = false; 241 | window.requestAnimationFrame(draw); 242 | 243 | } 244 | 245 | function updateStatus(message) { 246 | document.getElementById('status').innerHTML = message; 247 | 248 | } 249 | 250 | function removeStatus() { 251 | setTimeout(function() { 252 | document.getElementById('status').innerHTML = ''; 253 | }, 1500); 254 | } 255 | 256 | 257 | 258 | function drawGoalkeeper() { 259 | 260 | var gkX = paddleX / 2 + m; 261 | var gkY = canvas.height * 7 / 8 - paddleHeight; 262 | ctx.drawImage(homePlayer, gkX, gkY - 15, playerWidth, playerHeight); 263 | drawRods(gkY); 264 | box = new B(new V(gkX, gkY), playerWidth, paddleHeight).toPolygon(); 265 | collisionDetection(box, gkX); 266 | 267 | } 268 | 269 | 270 | function drawDefenders() { 271 | 272 | var lcbX = paddleX / 4 + m; 273 | var lcbY = canvas.height * 13 / 16 - paddleHeight; 274 | drawRods(lcbY); 275 | ctx.drawImage(homePlayer, lcbX, lcbY - 15, playerWidth, playerHeight); 276 | box = new B(new V(lcbX, lcbY), playerWidth, paddleHeight).toPolygon(); 277 | collisionDetection(box, lcbX); 278 | 279 | var rcbX = paddleX * 3 / 4 + m; 280 | var rcbY = canvas.height * 13 / 16 - paddleHeight; 281 | ctx.drawImage(homePlayer, rcbX, rcbY - 15, playerWidth, playerHeight); 282 | box = new B(new V(rcbX, rcbY), playerWidth, paddleHeight).toPolygon(); 283 | collisionDetection(box, rcbX); 284 | } 285 | 286 | function drawMidfielders() { 287 | 288 | //midfielders 289 | var lwbX = paddleX * 1 / 8 + m; 290 | var lwbY = canvas.height * 5 / 8 - paddleHeight; 291 | drawRods(lwbY); 292 | ctx.drawImage(homePlayer, lwbX, lwbY - 15, playerWidth, playerHeight); 293 | box = new B(new V(lwbX, lwbY), playerWidth, paddleHeight).toPolygon(); 294 | collisionDetection(box, lwbX); 295 | 296 | var lcmX = paddleX * 3 / 8 + m; 297 | var lcmY = canvas.height * 5 / 8 - paddleHeight; 298 | ctx.drawImage(homePlayer, lcmX, lcmY - 15, playerWidth, playerHeight); 299 | box = new B(new V(lcmX, lcmY), playerWidth, paddleHeight).toPolygon(); 300 | collisionDetection(box, lcmX); 301 | 302 | var rcmX = paddleX * 5 / 8 + m; 303 | var rcmY = canvas.height * 5 / 8 - paddleHeight; 304 | ctx.drawImage(homePlayer, rcmX, rcmY - 15, playerWidth, playerHeight); 305 | box = new B(new V(rcmX, rcmY), playerWidth, paddleHeight).toPolygon(); 306 | collisionDetection(box, rcmX); 307 | 308 | var rwbX = paddleX * 7 / 8 + m; 309 | var rwbY = canvas.height * 5 / 8 - paddleHeight; 310 | ctx.drawImage(homePlayer, rwbX, rwbY - 15, playerWidth, playerHeight); 311 | box = new B(new V(rwbX, rwbY), playerWidth, paddleHeight).toPolygon(); 312 | collisionDetection(box, rwbX); 313 | 314 | } 315 | 316 | function drawStrikers() { 317 | //attackers 318 | var lwX = paddleX / 4 + m; 319 | var lwY = canvas.height * 9 / 32 - paddleHeight; 320 | drawRods(lwY); 321 | ctx.drawImage(homePlayer, lwX, lwY - 15, playerWidth, playerHeight); 322 | box = new B(new V(lwX, lwY), playerWidth, paddleHeight).toPolygon(); 323 | collisionDetection(box, lwX); 324 | 325 | var cfX = paddleX / 2 + m; 326 | var cfY = canvas.height * 9 / 32 - paddleHeight; 327 | ctx.drawImage(homePlayer, cfX, cfY - 15, playerWidth, playerHeight); 328 | box = new B(new V(cfX, cfY), playerWidth, paddleHeight).toPolygon(); 329 | collisionDetection(box, cfX); 330 | 331 | var rwX = paddleX * 3 / 4 + m; 332 | var rwY = canvas.height * 9 / 32 - paddleHeight; 333 | ctx.drawImage(homePlayer, rwX, rwY - 15, playerWidth, playerHeight); 334 | box = new B(new V(rwX, rwY), playerWidth, paddleHeight).toPolygon(); 335 | collisionDetection(box, rwX); 336 | 337 | } 338 | 339 | 340 | 341 | function drawAwayGoalkeeper() { 342 | 343 | var gkX = paddleX / 2 + j; 344 | var gkY = canvas.height * 1 / 8 - paddleHeight; 345 | drawRods(gkY); 346 | ctx.drawImage(awayPlayer, gkX, gkY - 15, playerWidth, playerHeight); 347 | box = new B(new V(gkX, gkY), playerWidth, paddleHeight).toPolygon(); 348 | collisionDetectionAway(box, gkX); 349 | 350 | if (x > gkX && gkX < paddleX * 3 / 4) 351 | j += aiSpeed; 352 | else if (gkX > paddleX * 1 / 4) 353 | j -= aiSpeed; 354 | 355 | } 356 | 357 | function drawAwayDefenders() { 358 | 359 | var lcbX = paddleX / 4 + j; 360 | var lcbY = canvas.height * 3 / 16 - paddleHeight; 361 | drawRods(lcbY); 362 | ctx.drawImage(awayPlayer, lcbX, lcbY - 15, playerWidth, playerHeight); 363 | box = new B(new V(lcbX, lcbY), playerWidth, paddleHeight).toPolygon(); 364 | collisionDetectionAway(box, lcbX); 365 | 366 | var rcbX = paddleX * 3 / 4 + j; 367 | var rcbY = canvas.height * 3 / 16 - paddleHeight; 368 | ctx.drawImage(awayPlayer, rcbX, rcbY - 15, playerWidth, playerHeight); 369 | box = new B(new V(rcbX, rcbY), playerWidth, paddleHeight).toPolygon(); 370 | collisionDetectionAway(box, rcbX); 371 | 372 | if (x > lcbX && lcbX < paddleX * 3 / 4) 373 | j += aiSpeed; 374 | else if (lcbX > paddleX * 1 / 4) 375 | j -= aiSpeed; 376 | if (x > rcbX && rcbX < paddleX * 3 / 4) 377 | j += aiSpeed; 378 | else if (rcbX > paddleX * 1 / 4) 379 | j -= aiSpeed; 380 | } 381 | 382 | function drawAwayMidfielders() { 383 | 384 | //midfielders 385 | var lwbX = paddleX * 1 / 8 + j; 386 | var lwbY = canvas.height * 3 / 8 - paddleHeight; 387 | drawRods(lwbY) 388 | ctx.drawImage(awayPlayer, lwbX, lwbY - 15, playerWidth, playerHeight); 389 | box = new B(new V(lwbX, lwbY), playerWidth, paddleHeight).toPolygon(); 390 | collisionDetectionAway(box, lwbX); 391 | 392 | var lcmX = paddleX * 3 / 8 + j; 393 | var lcmY = canvas.height * 3 / 8 - paddleHeight; 394 | ctx.drawImage(awayPlayer, lcmX, lcmY - 15, playerWidth, playerHeight); 395 | box = new B(new V(lcmX, lcmY), playerWidth, paddleHeight).toPolygon(); 396 | collisionDetectionAway(box, lcmX); 397 | 398 | var rcmX = paddleX * 5 / 8 + j; 399 | var rcmY = canvas.height * 3 / 8 - paddleHeight; 400 | ctx.drawImage(awayPlayer, rcmX, rcmY - 15, playerWidth, playerHeight); 401 | box = new B(new V(rcmX, rcmY), playerWidth, paddleHeight).toPolygon(); 402 | collisionDetectionAway(box, rcmX); 403 | 404 | var rwbX = paddleX * 7 / 8 + j; 405 | var rwbY = canvas.height * 3 / 8 - paddleHeight; 406 | ctx.drawImage(awayPlayer, rwbX, rwbY - 15, playerWidth, playerHeight); 407 | box = new B(new V(rwbX, rwbY), playerWidth, paddleHeight).toPolygon(); 408 | collisionDetectionAway(box, rwbX); 409 | 410 | if (x > lwbX && lwbX < paddleX * 3 / 4) 411 | j += aiSpeed; 412 | else if (lwbX > paddleX * 1 / 4) 413 | j -= aiSpeed; 414 | if (x > rwbX && rwbX < paddleX * 3 / 4) 415 | j += aiSpeed; 416 | else if (rwbX > paddleX * 1 / 4) 417 | j -= aiSpeed; 418 | if (x > rcmX && rcmX < paddleX * 3 / 4) 419 | j += aiSpeed; 420 | else if (rcmX > paddleX * 1 / 4) 421 | j -= aiSpeed; 422 | if (x > lcmX && lcmX < paddleX * 3 / 4) 423 | j += aiSpeed; 424 | else if (lcmX > paddleX * 1 / 4) 425 | j -= aiSpeed; 426 | } 427 | 428 | 429 | function drawAwayStrikers() { 430 | //attackers 431 | ctx.beginPath(); 432 | var lwX = paddleX / 4 + j; 433 | var lwY = canvas.height * 23 / 32 - paddleHeight; 434 | drawRods(lwY); 435 | ctx.drawImage(awayPlayer, lwX, lwY - 15, playerWidth, playerHeight); 436 | box = new B(new V(lwX, lwY), playerWidth, paddleHeight).toPolygon(); 437 | collisionDetectionAway(box, lwX); 438 | 439 | ctx.beginPath(); 440 | var cfX = paddleX / 2 + j; 441 | var cfY = canvas.height * 23 / 32 - paddleHeight; 442 | ctx.drawImage(awayPlayer, cfX, cfY - 15, playerWidth, playerHeight); 443 | box = new B(new V(cfX, cfY), playerWidth, paddleHeight).toPolygon(); 444 | collisionDetectionAway(box, cfX); 445 | 446 | ctx.beginPath(); 447 | var rwX = paddleX * 3 / 4 + j; 448 | var rwY = canvas.height * 23 / 32 - paddleHeight; 449 | ctx.drawImage(awayPlayer, rwX, rwY - 15, playerWidth, playerHeight); 450 | box = new B(new V(rwX, rwY), playerWidth, paddleHeight).toPolygon(); 451 | collisionDetectionAway(box, rwX); 452 | 453 | 454 | // if(y + 10 == rwY || y - 10 == rwY) { 455 | if (x > lwX && lwX < paddleX * 3 / 4) 456 | j += aiSpeed; 457 | else if (lwX > paddleX * 1 / 4) 458 | j -= aiSpeed; 459 | if (x > rwX && rwX < paddleX * 3 / 4) 460 | j += aiSpeed; 461 | else if (rwX > paddleX * 1 / 4) 462 | j -= aiSpeed; 463 | if (x > cfX && cfX < paddleX * 3 / 4) 464 | j += aiSpeed; 465 | else if (cfX > paddleX * 1 / 4) 466 | j -= aiSpeed; 467 | //} 468 | 469 | 470 | } 471 | 472 | 473 | function collisionDetection(box, pX) { 474 | var response = new SAT.Response(); 475 | if (SAT.testPolygonCircle(box, circle, response)) { 476 | var speed = (x + (12 / 2) - pX + (20 / 2)) / (20 / 2) * 5; 477 | if (flag1 == 1) { 478 | if (dy > 0) { 479 | dy = -dy; 480 | y = y - speed; 481 | if (dx > 0) 482 | x = x + speed; 483 | else 484 | x = x - speed; 485 | } else { 486 | y = y - speed; 487 | if (dx > 0) 488 | x = x - speed; 489 | else 490 | x = x + speed; 491 | } 492 | flag1 = 0; 493 | } 494 | } else 495 | flag1 = 1; 496 | } 497 | 498 | function collisionDetectionAway(box, pX) { 499 | var response = new SAT.Response(); 500 | if (SAT.testPolygonCircle(box, circle, response)) { 501 | var speed = (x + (12 / 2) - pX + (20 / 2)) / (20 / 2) * 5; 502 | if (flag2 == 1) { 503 | if (dy < 0) { 504 | dy = -dy; 505 | y = y + speed; 506 | if (dx > 0) 507 | x = x + speed; 508 | else 509 | x = x - speed; 510 | } else { 511 | y = y + speed; 512 | if (dx > 0) 513 | x = x + speed; 514 | else 515 | x = x - speed; 516 | } 517 | } 518 | } else 519 | flag2 = 1; 520 | } 521 | 522 | 523 | function goalDetection(box) { 524 | var response = new SAT.Response(); 525 | return SAT.testPolygonCircle(box, circle, response); 526 | } 527 | 528 | function drawRods(yAxis) { 529 | ctx.beginPath(); 530 | ctx.rect(0, yAxis + 2, canvas.width, paddleHeight - 5); 531 | ctx.fillStyle = "#BDBDBD"; 532 | ctx.fill(); 533 | ctx.strokeStyle = 'black'; 534 | ctx.stroke(); 535 | ctx.closePath(); 536 | } 537 | 538 | function keyDownHandler(e) { 539 | if (e.keyCode == 39) { 540 | rightPressed = true; 541 | } else if (e.keyCode == 37) { 542 | leftPressed = true; 543 | } 544 | } 545 | 546 | function keyUpHandler(e) { 547 | if (e.keyCode == 39) { 548 | rightPressed = false; 549 | } else if (e.keyCode == 37) { 550 | leftPressed = false; 551 | } 552 | } -------------------------------------------------------------------------------- /js/SAT.js: -------------------------------------------------------------------------------- 1 | // Version 0.5.0 - Copyright 2012 - 2015 - Jim Riecken 2 | // 3 | // Released under the MIT License - https://github.com/jriecken/sat-js 4 | // 5 | // A simple library for determining intersections of circles and 6 | // polygons using the Separating Axis Theorem. 7 | /** @preserve SAT.js - Version 0.5.0 - Copyright 2012 - 2015 - Jim Riecken - released under the MIT License. https://github.com/jriecken/sat-js */ 8 | 9 | /*global define: false, module: false*/ 10 | /*jshint shadow:true, sub:true, forin:true, noarg:true, noempty:true, 11 | eqeqeq:true, bitwise:true, strict:true, undef:true, 12 | curly:true, browser:true */ 13 | 14 | // Create a UMD wrapper for SAT. Works in: 15 | // 16 | // - Plain browser via global SAT variable 17 | // - AMD loader (like require.js) 18 | // - Node.js 19 | // 20 | // The quoted properties all over the place are used so that the Closure Compiler 21 | // does not mangle the exposed API in advanced mode. 22 | /** 23 | * @param {*} root - The global scope 24 | * @param {Function} factory - Factory that creates SAT module 25 | */ 26 | (function (root, factory) { 27 | "use strict"; 28 | if (typeof define === 'function' && define['amd']) { 29 | define(factory); 30 | } else if (typeof exports === 'object') { 31 | module['exports'] = factory(); 32 | } else { 33 | root['SAT'] = factory(); 34 | } 35 | }(this, function () { 36 | "use strict"; 37 | 38 | var SAT = {}; 39 | 40 | // 41 | // ## Vector 42 | // 43 | // Represents a vector in two dimensions with `x` and `y` properties. 44 | 45 | 46 | // Create a new Vector, optionally passing in the `x` and `y` coordinates. If 47 | // a coordinate is not specified, it will be set to `0` 48 | /** 49 | * @param {?number=} x The x position. 50 | * @param {?number=} y The y position. 51 | * @constructor 52 | */ 53 | function Vector(x, y) { 54 | this['x'] = x || 0; 55 | this['y'] = y || 0; 56 | } 57 | SAT['Vector'] = Vector; 58 | // Alias `Vector` as `V` 59 | SAT['V'] = Vector; 60 | 61 | 62 | // Copy the values of another Vector into this one. 63 | /** 64 | * @param {Vector} other The other Vector. 65 | * @return {Vector} This for chaining. 66 | */ 67 | Vector.prototype['copy'] = Vector.prototype.copy = function(other) { 68 | this['x'] = other['x']; 69 | this['y'] = other['y']; 70 | return this; 71 | }; 72 | 73 | // Create a new vector with the same coordinates as this on. 74 | /** 75 | * @return {Vector} The new cloned vector 76 | */ 77 | Vector.prototype['clone'] = Vector.prototype.clone = function() { 78 | return new Vector(this['x'], this['y']); 79 | }; 80 | 81 | // Change this vector to be perpendicular to what it was before. (Effectively 82 | // roatates it 90 degrees in a clockwise direction) 83 | /** 84 | * @return {Vector} This for chaining. 85 | */ 86 | Vector.prototype['perp'] = Vector.prototype.perp = function() { 87 | var x = this['x']; 88 | this['x'] = this['y']; 89 | this['y'] = -x; 90 | return this; 91 | }; 92 | 93 | // Rotate this vector (counter-clockwise) by the specified angle (in radians). 94 | /** 95 | * @param {number} angle The angle to rotate (in radians) 96 | * @return {Vector} This for chaining. 97 | */ 98 | Vector.prototype['rotate'] = Vector.prototype.rotate = function (angle) { 99 | var x = this['x']; 100 | var y = this['y']; 101 | this['x'] = x * Math.cos(angle) - y * Math.sin(angle); 102 | this['y'] = x * Math.sin(angle) + y * Math.cos(angle); 103 | return this; 104 | }; 105 | 106 | // Reverse this vector. 107 | /** 108 | * @return {Vector} This for chaining. 109 | */ 110 | Vector.prototype['reverse'] = Vector.prototype.reverse = function() { 111 | this['x'] = -this['x']; 112 | this['y'] = -this['y']; 113 | return this; 114 | }; 115 | 116 | 117 | // Normalize this vector. (make it have length of `1`) 118 | /** 119 | * @return {Vector} This for chaining. 120 | */ 121 | Vector.prototype['normalize'] = Vector.prototype.normalize = function() { 122 | var d = this.len(); 123 | if(d > 0) { 124 | this['x'] = this['x'] / d; 125 | this['y'] = this['y'] / d; 126 | } 127 | return this; 128 | }; 129 | 130 | // Add another vector to this one. 131 | /** 132 | * @param {Vector} other The other Vector. 133 | * @return {Vector} This for chaining. 134 | */ 135 | Vector.prototype['add'] = Vector.prototype.add = function(other) { 136 | this['x'] += other['x']; 137 | this['y'] += other['y']; 138 | return this; 139 | }; 140 | 141 | // Subtract another vector from this one. 142 | /** 143 | * @param {Vector} other The other Vector. 144 | * @return {Vector} This for chaiing. 145 | */ 146 | Vector.prototype['sub'] = Vector.prototype.sub = function(other) { 147 | this['x'] -= other['x']; 148 | this['y'] -= other['y']; 149 | return this; 150 | }; 151 | 152 | // Scale this vector. An independant scaling factor can be provided 153 | // for each axis, or a single scaling factor that will scale both `x` and `y`. 154 | /** 155 | * @param {number} x The scaling factor in the x direction. 156 | * @param {?number=} y The scaling factor in the y direction. If this 157 | * is not specified, the x scaling factor will be used. 158 | * @return {Vector} This for chaining. 159 | */ 160 | Vector.prototype['scale'] = Vector.prototype.scale = function(x,y) { 161 | this['x'] *= x; 162 | this['y'] *= y || x; 163 | return this; 164 | }; 165 | 166 | // Project this vector on to another vector. 167 | /** 168 | * @param {Vector} other The vector to project onto. 169 | * @return {Vector} This for chaining. 170 | */ 171 | Vector.prototype['project'] = Vector.prototype.project = function(other) { 172 | var amt = this.dot(other) / other.len2(); 173 | this['x'] = amt * other['x']; 174 | this['y'] = amt * other['y']; 175 | return this; 176 | }; 177 | 178 | // Project this vector onto a vector of unit length. This is slightly more efficient 179 | // than `project` when dealing with unit vectors. 180 | /** 181 | * @param {Vector} other The unit vector to project onto. 182 | * @return {Vector} This for chaining. 183 | */ 184 | Vector.prototype['projectN'] = Vector.prototype.projectN = function(other) { 185 | var amt = this.dot(other); 186 | this['x'] = amt * other['x']; 187 | this['y'] = amt * other['y']; 188 | return this; 189 | }; 190 | 191 | // Reflect this vector on an arbitrary axis. 192 | /** 193 | * @param {Vector} axis The vector representing the axis. 194 | * @return {Vector} This for chaining. 195 | */ 196 | Vector.prototype['reflect'] = Vector.prototype.reflect = function(axis) { 197 | var x = this['x']; 198 | var y = this['y']; 199 | this.project(axis).scale(2); 200 | this['x'] -= x; 201 | this['y'] -= y; 202 | return this; 203 | }; 204 | 205 | // Reflect this vector on an arbitrary axis (represented by a unit vector). This is 206 | // slightly more efficient than `reflect` when dealing with an axis that is a unit vector. 207 | /** 208 | * @param {Vector} axis The unit vector representing the axis. 209 | * @return {Vector} This for chaining. 210 | */ 211 | Vector.prototype['reflectN'] = Vector.prototype.reflectN = function(axis) { 212 | var x = this['x']; 213 | var y = this['y']; 214 | this.projectN(axis).scale(2); 215 | this['x'] -= x; 216 | this['y'] -= y; 217 | return this; 218 | }; 219 | 220 | // Get the dot product of this vector and another. 221 | /** 222 | * @param {Vector} other The vector to dot this one against. 223 | * @return {number} The dot product. 224 | */ 225 | Vector.prototype['dot'] = Vector.prototype.dot = function(other) { 226 | return this['x'] * other['x'] + this['y'] * other['y']; 227 | }; 228 | 229 | // Get the squared length of this vector. 230 | /** 231 | * @return {number} The length^2 of this vector. 232 | */ 233 | Vector.prototype['len2'] = Vector.prototype.len2 = function() { 234 | return this.dot(this); 235 | }; 236 | 237 | // Get the length of this vector. 238 | /** 239 | * @return {number} The length of this vector. 240 | */ 241 | Vector.prototype['len'] = Vector.prototype.len = function() { 242 | return Math.sqrt(this.len2()); 243 | }; 244 | 245 | // ## Circle 246 | // 247 | // Represents a circle with a position and a radius. 248 | 249 | // Create a new circle, optionally passing in a position and/or radius. If no position 250 | // is given, the circle will be at `(0,0)`. If no radius is provided, the circle will 251 | // have a radius of `0`. 252 | /** 253 | * @param {Vector=} pos A vector representing the position of the center of the circle 254 | * @param {?number=} r The radius of the circle 255 | * @constructor 256 | */ 257 | function Circle(pos, r) { 258 | this['pos'] = pos || new Vector(); 259 | this['r'] = r || 0; 260 | } 261 | SAT['Circle'] = Circle; 262 | 263 | // Compute the axis-aligned bounding box (AABB) of this Circle. 264 | // 265 | // Note: Returns a _new_ `Polygon` each time you call this. 266 | /** 267 | * @return {Polygon} The AABB 268 | */ 269 | Circle.prototype['getAABB'] = Circle.prototype.getAABB = function() { 270 | var r = this['r']; 271 | var corner = this["pos"].clone().sub(new Vector(r, r)); 272 | return new Box(corner, r*2, r*2).toPolygon(); 273 | }; 274 | 275 | // ## Polygon 276 | // 277 | // Represents a *convex* polygon with any number of points (specified in counter-clockwise order) 278 | // 279 | // Note: Do _not_ manually change the `points`, `angle`, or `offset` properties. Use the 280 | // provided setters. Otherwise the calculated properties will not be updated correctly. 281 | // 282 | // `pos` can be changed directly. 283 | 284 | // Create a new polygon, passing in a position vector, and an array of points (represented 285 | // by vectors relative to the position vector). If no position is passed in, the position 286 | // of the polygon will be `(0,0)`. 287 | /** 288 | * @param {Vector=} pos A vector representing the origin of the polygon. (all other 289 | * points are relative to this one) 290 | * @param {Array.=} points An array of vectors representing the points in the polygon, 291 | * in counter-clockwise order. 292 | * @constructor 293 | */ 294 | function Polygon(pos, points) { 295 | this['pos'] = pos || new Vector(); 296 | this['angle'] = 0; 297 | this['offset'] = new Vector(); 298 | this.setPoints(points || []); 299 | } 300 | SAT['Polygon'] = Polygon; 301 | 302 | // Set the points of the polygon. 303 | // 304 | // Note: The points are counter-clockwise *with respect to the coordinate system*. 305 | // If you directly draw the points on a screen that has the origin at the top-left corner 306 | // it will _appear_ visually that the points are being specified clockwise. This is just 307 | // because of the inversion of the Y-axis when being displayed. 308 | /** 309 | * @param {Array.=} points An array of vectors representing the points in the polygon, 310 | * in counter-clockwise order. 311 | * @return {Polygon} This for chaining. 312 | */ 313 | Polygon.prototype['setPoints'] = Polygon.prototype.setPoints = function(points) { 314 | // Only re-allocate if this is a new polygon or the number of points has changed. 315 | var lengthChanged = !this['points'] || this['points'].length !== points.length; 316 | if (lengthChanged) { 317 | var i; 318 | var calcPoints = this['calcPoints'] = []; 319 | var edges = this['edges'] = []; 320 | var normals = this['normals'] = []; 321 | // Allocate the vector arrays for the calculated properties 322 | for (i = 0; i < points.length; i++) { 323 | calcPoints.push(new Vector()); 324 | edges.push(new Vector()); 325 | normals.push(new Vector()); 326 | } 327 | } 328 | this['points'] = points; 329 | this._recalc(); 330 | return this; 331 | }; 332 | 333 | // Set the current rotation angle of the polygon. 334 | /** 335 | * @param {number} angle The current rotation angle (in radians). 336 | * @return {Polygon} This for chaining. 337 | */ 338 | Polygon.prototype['setAngle'] = Polygon.prototype.setAngle = function(angle) { 339 | this['angle'] = angle; 340 | this._recalc(); 341 | return this; 342 | }; 343 | 344 | // Set the current offset to apply to the `points` before applying the `angle` rotation. 345 | /** 346 | * @param {Vector} offset The new offset vector. 347 | * @return {Polygon} This for chaining. 348 | */ 349 | Polygon.prototype['setOffset'] = Polygon.prototype.setOffset = function(offset) { 350 | this['offset'] = offset; 351 | this._recalc(); 352 | return this; 353 | }; 354 | 355 | // Rotates this polygon counter-clockwise around the origin of *its local coordinate system* (i.e. `pos`). 356 | // 357 | // Note: This changes the **original** points (so any `angle` will be applied on top of this rotation). 358 | /** 359 | * @param {number} angle The angle to rotate (in radians) 360 | * @return {Polygon} This for chaining. 361 | */ 362 | Polygon.prototype['rotate'] = Polygon.prototype.rotate = function(angle) { 363 | var points = this['points']; 364 | var len = points.length; 365 | for (var i = 0; i < len; i++) { 366 | points[i].rotate(angle); 367 | } 368 | this._recalc(); 369 | return this; 370 | }; 371 | 372 | // Translates the points of this polygon by a specified amount relative to the origin of *its own coordinate 373 | // system* (i.e. `pos`). 374 | // 375 | // This is most useful to change the "center point" of a polygon. If you just want to move the whole polygon, change 376 | // the coordinates of `pos`. 377 | // 378 | // Note: This changes the **original** points (so any `offset` will be applied on top of this translation) 379 | /** 380 | * @param {number} x The horizontal amount to translate. 381 | * @param {number} y The vertical amount to translate. 382 | * @return {Polygon} This for chaining. 383 | */ 384 | Polygon.prototype['translate'] = Polygon.prototype.translate = function (x, y) { 385 | var points = this['points']; 386 | var len = points.length; 387 | for (var i = 0; i < len; i++) { 388 | points[i].x += x; 389 | points[i].y += y; 390 | } 391 | this._recalc(); 392 | return this; 393 | }; 394 | 395 | 396 | // Computes the calculated collision polygon. Applies the `angle` and `offset` to the original points then recalculates the 397 | // edges and normals of the collision polygon. 398 | /** 399 | * @return {Polygon} This for chaining. 400 | */ 401 | Polygon.prototype._recalc = function() { 402 | // Calculated points - this is what is used for underlying collisions and takes into account 403 | // the angle/offset set on the polygon. 404 | var calcPoints = this['calcPoints']; 405 | // The edges here are the direction of the `n`th edge of the polygon, relative to 406 | // the `n`th point. If you want to draw a given edge from the edge value, you must 407 | // first translate to the position of the starting point. 408 | var edges = this['edges']; 409 | // The normals here are the direction of the normal for the `n`th edge of the polygon, relative 410 | // to the position of the `n`th point. If you want to draw an edge normal, you must first 411 | // translate to the position of the starting point. 412 | var normals = this['normals']; 413 | // Copy the original points array and apply the offset/angle 414 | var points = this['points']; 415 | var offset = this['offset']; 416 | var angle = this['angle']; 417 | var len = points.length; 418 | var i; 419 | for (i = 0; i < len; i++) { 420 | var calcPoint = calcPoints[i].copy(points[i]); 421 | calcPoint.x += offset.x; 422 | calcPoint.y += offset.y; 423 | if (angle !== 0) { 424 | calcPoint.rotate(angle); 425 | } 426 | } 427 | // Calculate the edges/normals 428 | for (i = 0; i < len; i++) { 429 | var p1 = calcPoints[i]; 430 | var p2 = i < len - 1 ? calcPoints[i + 1] : calcPoints[0]; 431 | var e = edges[i].copy(p2).sub(p1); 432 | normals[i].copy(e).perp().normalize(); 433 | } 434 | return this; 435 | }; 436 | 437 | 438 | // Compute the axis-aligned bounding box. Any current state 439 | // (translations/rotations) will be applied before constructing the AABB. 440 | // 441 | // Note: Returns a _new_ `Polygon` each time you call this. 442 | /** 443 | * @return {Polygon} The AABB 444 | */ 445 | Polygon.prototype["getAABB"] = Polygon.prototype.getAABB = function() { 446 | var points = this["calcPoints"]; 447 | var len = points.length; 448 | var xMin = points[0]["x"]; 449 | var yMin = points[0]["y"]; 450 | var xMax = points[0]["x"]; 451 | var yMax = points[0]["y"]; 452 | for (var i = 1; i < len; i++) { 453 | var point = points[i]; 454 | if (point["x"] < xMin) { 455 | xMin = point["x"]; 456 | } 457 | else if (point["x"] > xMax) { 458 | xMax = point["x"]; 459 | } 460 | if (point["y"] < yMin) { 461 | yMin = point["y"]; 462 | } 463 | else if (point["y"] > yMax) { 464 | yMax = point["y"]; 465 | } 466 | } 467 | return new Box(this["pos"].clone().add(new Vector(xMin, yMin)), xMax - xMin, yMax - yMin).toPolygon(); 468 | }; 469 | 470 | 471 | // ## Box 472 | // 473 | // Represents an axis-aligned box, with a width and height. 474 | 475 | 476 | // Create a new box, with the specified position, width, and height. If no position 477 | // is given, the position will be `(0,0)`. If no width or height are given, they will 478 | // be set to `0`. 479 | /** 480 | * @param {Vector=} pos A vector representing the bottom-left of the box (i.e. the smallest x and smallest y value). 481 | * @param {?number=} w The width of the box. 482 | * @param {?number=} h The height of the box. 483 | * @constructor 484 | */ 485 | function Box(pos, w, h) { 486 | this['pos'] = pos || new Vector(); 487 | this['w'] = w || 0; 488 | this['h'] = h || 0; 489 | } 490 | SAT['Box'] = Box; 491 | 492 | // Returns a polygon whose edges are the same as this box. 493 | /** 494 | * @return {Polygon} A new Polygon that represents this box. 495 | */ 496 | Box.prototype['toPolygon'] = Box.prototype.toPolygon = function() { 497 | var pos = this['pos']; 498 | var w = this['w']; 499 | var h = this['h']; 500 | return new Polygon(new Vector(pos['x'], pos['y']), [ 501 | new Vector(), new Vector(w, 0), 502 | new Vector(w,h), new Vector(0,h) 503 | ]); 504 | }; 505 | 506 | // ## Response 507 | // 508 | // An object representing the result of an intersection. Contains: 509 | // - The two objects participating in the intersection 510 | // - The vector representing the minimum change necessary to extract the first object 511 | // from the second one (as well as a unit vector in that direction and the magnitude 512 | // of the overlap) 513 | // - Whether the first object is entirely inside the second, and vice versa. 514 | /** 515 | * @constructor 516 | */ 517 | function Response() { 518 | this['a'] = null; 519 | this['b'] = null; 520 | this['overlapN'] = new Vector(); 521 | this['overlapV'] = new Vector(); 522 | this.clear(); 523 | } 524 | SAT['Response'] = Response; 525 | 526 | // Set some values of the response back to their defaults. Call this between tests if 527 | // you are going to reuse a single Response object for multiple intersection tests (recommented 528 | // as it will avoid allcating extra memory) 529 | /** 530 | * @return {Response} This for chaining 531 | */ 532 | Response.prototype['clear'] = Response.prototype.clear = function() { 533 | this['aInB'] = true; 534 | this['bInA'] = true; 535 | this['overlap'] = Number.MAX_VALUE; 536 | return this; 537 | }; 538 | 539 | // ## Object Pools 540 | 541 | // A pool of `Vector` objects that are used in calculations to avoid 542 | // allocating memory. 543 | /** 544 | * @type {Array.} 545 | */ 546 | var T_VECTORS = []; 547 | for (var i = 0; i < 10; i++) { T_VECTORS.push(new Vector()); } 548 | 549 | // A pool of arrays of numbers used in calculations to avoid allocating 550 | // memory. 551 | /** 552 | * @type {Array.>} 553 | */ 554 | var T_ARRAYS = []; 555 | for (var i = 0; i < 5; i++) { T_ARRAYS.push([]); } 556 | 557 | // Temporary response used for polygon hit detection. 558 | /** 559 | * @type {Response} 560 | */ 561 | var T_RESPONSE = new Response(); 562 | 563 | // Unit square polygon used for polygon hit detection. 564 | /** 565 | * @type {Polygon} 566 | */ 567 | var UNIT_SQUARE = new Box(new Vector(), 1, 1).toPolygon(); 568 | 569 | // ## Helper Functions 570 | 571 | // Flattens the specified array of points onto a unit vector axis, 572 | // resulting in a one dimensional range of the minimum and 573 | // maximum value on that axis. 574 | /** 575 | * @param {Array.} points The points to flatten. 576 | * @param {Vector} normal The unit vector axis to flatten on. 577 | * @param {Array.} result An array. After calling this function, 578 | * result[0] will be the minimum value, 579 | * result[1] will be the maximum value. 580 | */ 581 | function flattenPointsOn(points, normal, result) { 582 | var min = Number.MAX_VALUE; 583 | var max = -Number.MAX_VALUE; 584 | var len = points.length; 585 | for (var i = 0; i < len; i++ ) { 586 | // The magnitude of the projection of the point onto the normal 587 | var dot = points[i].dot(normal); 588 | if (dot < min) { min = dot; } 589 | if (dot > max) { max = dot; } 590 | } 591 | result[0] = min; result[1] = max; 592 | } 593 | 594 | // Check whether two convex polygons are separated by the specified 595 | // axis (must be a unit vector). 596 | /** 597 | * @param {Vector} aPos The position of the first polygon. 598 | * @param {Vector} bPos The position of the second polygon. 599 | * @param {Array.} aPoints The points in the first polygon. 600 | * @param {Array.} bPoints The points in the second polygon. 601 | * @param {Vector} axis The axis (unit sized) to test against. The points of both polygons 602 | * will be projected onto this axis. 603 | * @param {Response=} response A Response object (optional) which will be populated 604 | * if the axis is not a separating axis. 605 | * @return {boolean} true if it is a separating axis, false otherwise. If false, 606 | * and a response is passed in, information about how much overlap and 607 | * the direction of the overlap will be populated. 608 | */ 609 | function isSeparatingAxis(aPos, bPos, aPoints, bPoints, axis, response) { 610 | var rangeA = T_ARRAYS.pop(); 611 | var rangeB = T_ARRAYS.pop(); 612 | // The magnitude of the offset between the two polygons 613 | var offsetV = T_VECTORS.pop().copy(bPos).sub(aPos); 614 | var projectedOffset = offsetV.dot(axis); 615 | // Project the polygons onto the axis. 616 | flattenPointsOn(aPoints, axis, rangeA); 617 | flattenPointsOn(bPoints, axis, rangeB); 618 | // Move B's range to its position relative to A. 619 | rangeB[0] += projectedOffset; 620 | rangeB[1] += projectedOffset; 621 | // Check if there is a gap. If there is, this is a separating axis and we can stop 622 | if (rangeA[0] > rangeB[1] || rangeB[0] > rangeA[1]) { 623 | T_VECTORS.push(offsetV); 624 | T_ARRAYS.push(rangeA); 625 | T_ARRAYS.push(rangeB); 626 | return true; 627 | } 628 | // This is not a separating axis. If we're calculating a response, calculate the overlap. 629 | if (response) { 630 | var overlap = 0; 631 | // A starts further left than B 632 | if (rangeA[0] < rangeB[0]) { 633 | response['aInB'] = false; 634 | // A ends before B does. We have to pull A out of B 635 | if (rangeA[1] < rangeB[1]) { 636 | overlap = rangeA[1] - rangeB[0]; 637 | response['bInA'] = false; 638 | // B is fully inside A. Pick the shortest way out. 639 | } else { 640 | var option1 = rangeA[1] - rangeB[0]; 641 | var option2 = rangeB[1] - rangeA[0]; 642 | overlap = option1 < option2 ? option1 : -option2; 643 | } 644 | // B starts further left than A 645 | } else { 646 | response['bInA'] = false; 647 | // B ends before A ends. We have to push A out of B 648 | if (rangeA[1] > rangeB[1]) { 649 | overlap = rangeA[0] - rangeB[1]; 650 | response['aInB'] = false; 651 | // A is fully inside B. Pick the shortest way out. 652 | } else { 653 | var option1 = rangeA[1] - rangeB[0]; 654 | var option2 = rangeB[1] - rangeA[0]; 655 | overlap = option1 < option2 ? option1 : -option2; 656 | } 657 | } 658 | // If this is the smallest amount of overlap we've seen so far, set it as the minimum overlap. 659 | var absOverlap = Math.abs(overlap); 660 | if (absOverlap < response['overlap']) { 661 | response['overlap'] = absOverlap; 662 | response['overlapN'].copy(axis); 663 | if (overlap < 0) { 664 | response['overlapN'].reverse(); 665 | } 666 | } 667 | } 668 | T_VECTORS.push(offsetV); 669 | T_ARRAYS.push(rangeA); 670 | T_ARRAYS.push(rangeB); 671 | return false; 672 | } 673 | 674 | // Calculates which Voronoi region a point is on a line segment. 675 | // It is assumed that both the line and the point are relative to `(0,0)` 676 | // 677 | // | (0) | 678 | // (-1) [S]--------------[E] (1) 679 | // | (0) | 680 | /** 681 | * @param {Vector} line The line segment. 682 | * @param {Vector} point The point. 683 | * @return {number} LEFT_VORONOI_REGION (-1) if it is the left region, 684 | * MIDDLE_VORONOI_REGION (0) if it is the middle region, 685 | * RIGHT_VORONOI_REGION (1) if it is the right region. 686 | */ 687 | function voronoiRegion(line, point) { 688 | var len2 = line.len2(); 689 | var dp = point.dot(line); 690 | // If the point is beyond the start of the line, it is in the 691 | // left voronoi region. 692 | if (dp < 0) { return LEFT_VORONOI_REGION; } 693 | // If the point is beyond the end of the line, it is in the 694 | // right voronoi region. 695 | else if (dp > len2) { return RIGHT_VORONOI_REGION; } 696 | // Otherwise, it's in the middle one. 697 | else { return MIDDLE_VORONOI_REGION; } 698 | } 699 | // Constants for Voronoi regions 700 | /** 701 | * @const 702 | */ 703 | var LEFT_VORONOI_REGION = -1; 704 | /** 705 | * @const 706 | */ 707 | var MIDDLE_VORONOI_REGION = 0; 708 | /** 709 | * @const 710 | */ 711 | var RIGHT_VORONOI_REGION = 1; 712 | 713 | // ## Collision Tests 714 | 715 | // Check if a point is inside a circle. 716 | /** 717 | * @param {Vector} p The point to test. 718 | * @param {Circle} c The circle to test. 719 | * @return {boolean} true if the point is inside the circle, false if it is not. 720 | */ 721 | function pointInCircle(p, c) { 722 | var differenceV = T_VECTORS.pop().copy(p).sub(c['pos']); 723 | var radiusSq = c['r'] * c['r']; 724 | var distanceSq = differenceV.len2(); 725 | T_VECTORS.push(differenceV); 726 | // If the distance between is smaller than the radius then the point is inside the circle. 727 | return distanceSq <= radiusSq; 728 | } 729 | SAT['pointInCircle'] = pointInCircle; 730 | 731 | // Check if a point is inside a convex polygon. 732 | /** 733 | * @param {Vector} p The point to test. 734 | * @param {Polygon} poly The polygon to test. 735 | * @return {boolean} true if the point is inside the polygon, false if it is not. 736 | */ 737 | function pointInPolygon(p, poly) { 738 | UNIT_SQUARE['pos'].copy(p); 739 | T_RESPONSE.clear(); 740 | var result = testPolygonPolygon(UNIT_SQUARE, poly, T_RESPONSE); 741 | if (result) { 742 | result = T_RESPONSE['aInB']; 743 | } 744 | return result; 745 | } 746 | SAT['pointInPolygon'] = pointInPolygon; 747 | 748 | // Check if two circles collide. 749 | /** 750 | * @param {Circle} a The first circle. 751 | * @param {Circle} b The second circle. 752 | * @param {Response=} response Response object (optional) that will be populated if 753 | * the circles intersect. 754 | * @return {boolean} true if the circles intersect, false if they don't. 755 | */ 756 | function testCircleCircle(a, b, response) { 757 | // Check if the distance between the centers of the two 758 | // circles is greater than their combined radius. 759 | var differenceV = T_VECTORS.pop().copy(b['pos']).sub(a['pos']); 760 | var totalRadius = a['r'] + b['r']; 761 | var totalRadiusSq = totalRadius * totalRadius; 762 | var distanceSq = differenceV.len2(); 763 | // If the distance is bigger than the combined radius, they don't intersect. 764 | if (distanceSq > totalRadiusSq) { 765 | T_VECTORS.push(differenceV); 766 | return false; 767 | } 768 | // They intersect. If we're calculating a response, calculate the overlap. 769 | if (response) { 770 | var dist = Math.sqrt(distanceSq); 771 | response['a'] = a; 772 | response['b'] = b; 773 | response['overlap'] = totalRadius - dist; 774 | response['overlapN'].copy(differenceV.normalize()); 775 | response['overlapV'].copy(differenceV).scale(response['overlap']); 776 | response['aInB']= a['r'] <= b['r'] && dist <= b['r'] - a['r']; 777 | response['bInA'] = b['r'] <= a['r'] && dist <= a['r'] - b['r']; 778 | } 779 | T_VECTORS.push(differenceV); 780 | return true; 781 | } 782 | SAT['testCircleCircle'] = testCircleCircle; 783 | 784 | // Check if a polygon and a circle collide. 785 | /** 786 | * @param {Polygon} polygon The polygon. 787 | * @param {Circle} circle The circle. 788 | * @param {Response=} response Response object (optional) that will be populated if 789 | * they interset. 790 | * @return {boolean} true if they intersect, false if they don't. 791 | */ 792 | function testPolygonCircle(polygon, circle, response) { 793 | // Get the position of the circle relative to the polygon. 794 | var circlePos = T_VECTORS.pop().copy(circle['pos']).sub(polygon['pos']); 795 | var radius = circle['r']; 796 | var radius2 = radius * radius; 797 | var points = polygon['calcPoints']; 798 | var len = points.length; 799 | var edge = T_VECTORS.pop(); 800 | var point = T_VECTORS.pop(); 801 | 802 | // For each edge in the polygon: 803 | for (var i = 0; i < len; i++) { 804 | var next = i === len - 1 ? 0 : i + 1; 805 | var prev = i === 0 ? len - 1 : i - 1; 806 | var overlap = 0; 807 | var overlapN = null; 808 | 809 | // Get the edge. 810 | edge.copy(polygon['edges'][i]); 811 | // Calculate the center of the circle relative to the starting point of the edge. 812 | point.copy(circlePos).sub(points[i]); 813 | 814 | // If the distance between the center of the circle and the point 815 | // is bigger than the radius, the polygon is definitely not fully in 816 | // the circle. 817 | if (response && point.len2() > radius2) { 818 | response['aInB'] = false; 819 | } 820 | 821 | // Calculate which Voronoi region the center of the circle is in. 822 | var region = voronoiRegion(edge, point); 823 | // If it's the left region: 824 | if (region === LEFT_VORONOI_REGION) { 825 | // We need to make sure we're in the RIGHT_VORONOI_REGION of the previous edge. 826 | edge.copy(polygon['edges'][prev]); 827 | // Calculate the center of the circle relative the starting point of the previous edge 828 | var point2 = T_VECTORS.pop().copy(circlePos).sub(points[prev]); 829 | region = voronoiRegion(edge, point2); 830 | if (region === RIGHT_VORONOI_REGION) { 831 | // It's in the region we want. Check if the circle intersects the point. 832 | var dist = point.len(); 833 | if (dist > radius) { 834 | // No intersection 835 | T_VECTORS.push(circlePos); 836 | T_VECTORS.push(edge); 837 | T_VECTORS.push(point); 838 | T_VECTORS.push(point2); 839 | return false; 840 | } else if (response) { 841 | // It intersects, calculate the overlap. 842 | response['bInA'] = false; 843 | overlapN = point.normalize(); 844 | overlap = radius - dist; 845 | } 846 | } 847 | T_VECTORS.push(point2); 848 | // If it's the right region: 849 | } else if (region === RIGHT_VORONOI_REGION) { 850 | // We need to make sure we're in the left region on the next edge 851 | edge.copy(polygon['edges'][next]); 852 | // Calculate the center of the circle relative to the starting point of the next edge. 853 | point.copy(circlePos).sub(points[next]); 854 | region = voronoiRegion(edge, point); 855 | if (region === LEFT_VORONOI_REGION) { 856 | // It's in the region we want. Check if the circle intersects the point. 857 | var dist = point.len(); 858 | if (dist > radius) { 859 | // No intersection 860 | T_VECTORS.push(circlePos); 861 | T_VECTORS.push(edge); 862 | T_VECTORS.push(point); 863 | return false; 864 | } else if (response) { 865 | // It intersects, calculate the overlap. 866 | response['bInA'] = false; 867 | overlapN = point.normalize(); 868 | overlap = radius - dist; 869 | } 870 | } 871 | // Otherwise, it's the middle region: 872 | } else { 873 | // Need to check if the circle is intersecting the edge, 874 | // Change the edge into its "edge normal". 875 | var normal = edge.perp().normalize(); 876 | // Find the perpendicular distance between the center of the 877 | // circle and the edge. 878 | var dist = point.dot(normal); 879 | var distAbs = Math.abs(dist); 880 | // If the circle is on the outside of the edge, there is no intersection. 881 | if (dist > 0 && distAbs > radius) { 882 | // No intersection 883 | T_VECTORS.push(circlePos); 884 | T_VECTORS.push(normal); 885 | T_VECTORS.push(point); 886 | return false; 887 | } else if (response) { 888 | // It intersects, calculate the overlap. 889 | overlapN = normal; 890 | overlap = radius - dist; 891 | // If the center of the circle is on the outside of the edge, or part of the 892 | // circle is on the outside, the circle is not fully inside the polygon. 893 | if (dist >= 0 || overlap < 2 * radius) { 894 | response['bInA'] = false; 895 | } 896 | } 897 | } 898 | 899 | // If this is the smallest overlap we've seen, keep it. 900 | // (overlapN may be null if the circle was in the wrong Voronoi region). 901 | if (overlapN && response && Math.abs(overlap) < Math.abs(response['overlap'])) { 902 | response['overlap'] = overlap; 903 | response['overlapN'].copy(overlapN); 904 | } 905 | } 906 | 907 | // Calculate the final overlap vector - based on the smallest overlap. 908 | if (response) { 909 | response['a'] = polygon; 910 | response['b'] = circle; 911 | response['overlapV'].copy(response['overlapN']).scale(response['overlap']); 912 | } 913 | T_VECTORS.push(circlePos); 914 | T_VECTORS.push(edge); 915 | T_VECTORS.push(point); 916 | return true; 917 | } 918 | SAT['testPolygonCircle'] = testPolygonCircle; 919 | 920 | // Check if a circle and a polygon collide. 921 | // 922 | // **NOTE:** This is slightly less efficient than polygonCircle as it just 923 | // runs polygonCircle and reverses everything at the end. 924 | /** 925 | * @param {Circle} circle The circle. 926 | * @param {Polygon} polygon The polygon. 927 | * @param {Response=} response Response object (optional) that will be populated if 928 | * they interset. 929 | * @return {boolean} true if they intersect, false if they don't. 930 | */ 931 | function testCirclePolygon(circle, polygon, response) { 932 | // Test the polygon against the circle. 933 | var result = testPolygonCircle(polygon, circle, response); 934 | if (result && response) { 935 | // Swap A and B in the response. 936 | var a = response['a']; 937 | var aInB = response['aInB']; 938 | response['overlapN'].reverse(); 939 | response['overlapV'].reverse(); 940 | response['a'] = response['b']; 941 | response['b'] = a; 942 | response['aInB'] = response['bInA']; 943 | response['bInA'] = aInB; 944 | } 945 | return result; 946 | } 947 | SAT['testCirclePolygon'] = testCirclePolygon; 948 | 949 | // Checks whether polygons collide. 950 | /** 951 | * @param {Polygon} a The first polygon. 952 | * @param {Polygon} b The second polygon. 953 | * @param {Response=} response Response object (optional) that will be populated if 954 | * they interset. 955 | * @return {boolean} true if they intersect, false if they don't. 956 | */ 957 | function testPolygonPolygon(a, b, response) { 958 | var aPoints = a['calcPoints']; 959 | var aLen = aPoints.length; 960 | var bPoints = b['calcPoints']; 961 | var bLen = bPoints.length; 962 | // If any of the edge normals of A is a separating axis, no intersection. 963 | for (var i = 0; i < aLen; i++) { 964 | if (isSeparatingAxis(a['pos'], b['pos'], aPoints, bPoints, a['normals'][i], response)) { 965 | return false; 966 | } 967 | } 968 | // If any of the edge normals of B is a separating axis, no intersection. 969 | for (var i = 0;i < bLen; i++) { 970 | if (isSeparatingAxis(a['pos'], b['pos'], aPoints, bPoints, b['normals'][i], response)) { 971 | return false; 972 | } 973 | } 974 | // Since none of the edge normals of A or B are a separating axis, there is an intersection 975 | // and we've already calculated the smallest overlap (in isSeparatingAxis). Calculate the 976 | // final overlap vector. 977 | if (response) { 978 | response['a'] = a; 979 | response['b'] = b; 980 | response['overlapV'].copy(response['overlapN']).scale(response['overlap']); 981 | } 982 | return true; 983 | } 984 | SAT['testPolygonPolygon'] = testPolygonPolygon; 985 | 986 | return SAT; 987 | })); 988 | --------------------------------------------------------------------------------