├── .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 |
PLAY
16 |
17 |
18 |
19 |
(Use left and right arrow keys to play)
20 |
21 |
22 |
23 |
24 |
FoosBall
25 |
PLAY AGAIN
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Time Left
34 |
02:00
35 |
36 |
Juventus
37 |
0
38 |
Liverpool
39 |
0
40 |
41 |
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 |
--------------------------------------------------------------------------------