├── images
├── red.png
├── blue.png
├── green.png
├── orange.png
└── yellow.png
├── readme.md
├── index.html
├── bookmarklet
└── index.html
└── browser-breakout.js
/images/red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/browser-breakout/master/images/red.png
--------------------------------------------------------------------------------
/images/blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/browser-breakout/master/images/blue.png
--------------------------------------------------------------------------------
/images/green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/browser-breakout/master/images/green.png
--------------------------------------------------------------------------------
/images/orange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/browser-breakout/master/images/orange.png
--------------------------------------------------------------------------------
/images/yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/browser-breakout/master/images/yellow.png
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Browser Breakout
2 |
3 | ## Post
4 |
5 | - [https://f90.co.uk/labs/browser-breakout/](https://f90.co.uk/labs/browser-breakout/)
6 |
7 | ## Example
8 |
9 | - [https://orangespaceman.github.io/browser-breakout](https://orangespaceman.github.io/browser-breakout)
10 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Browser Breakout
6 |
42 |
43 |
44 |
45 |
Browser Breakout!
46 |
Play Browser Breakout!
47 |
48 |
49 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/bookmarklet/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Bookmarklet
5 |
29 |
30 |
31 | Browser Breakout
32 |
33 |
34 |
--------------------------------------------------------------------------------
/browser-breakout.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Browser Breakout!
3 | *
4 | * Thanks to http://billmill.org/static/canvastutorial/
5 | *
6 | */
7 | var browserBreakout = function() {
8 |
9 | /*
10 | * The HTML body element
11 | */
12 | var body = null,
13 |
14 | /*
15 | * The canvas HTMl element
16 | */
17 | canvas = null,
18 |
19 | /*
20 | * The canvas draw context
21 | */
22 | drawContext = null,
23 |
24 | /*
25 | * The draw interval
26 | */
27 | drawInterval = null,
28 |
29 | /*
30 | * Reseting on browser resize
31 | */
32 | reseting = false,
33 |
34 | /*
35 | * Current Game State. Can be:
36 | * 'title' => Game initialised for the first time
37 | * 'running' => Game is running!
38 | * 'gameEnded' => Game has ended unsuccessfully
39 | * 'victory' => Game has ended successfully
40 | * 'noImages' => No appropriate images were found
41 | */
42 | gameState = 'title',
43 |
44 | /*
45 | * text blocks
46 | */
47 | textBlocks = {},
48 | textAnimationTypes = ['default', 'vertical', 'horizontal', 'random', 'reverse'],
49 |
50 | /*
51 | * Acceptable images to play with
52 | */
53 | images = [],
54 | imagesAcceptable = 0,
55 | imagesRemaining = 0,
56 |
57 | /*
58 | * Visible position (within page)
59 | */
60 | visibleXStart = 0,
61 | visibleXEnd = 0,
62 | visibleYStart = 0,
63 | visibleYEnd = 0,
64 | visibleWidth = 0,
65 | visibleHeight = 0,
66 |
67 | /*
68 | * Ball positions
69 | */
70 | ballRadius = 7,
71 | ballColour = '#FFFFFF',
72 | ballXStart = ballX = 35,
73 | ballYStart = ballY = 35,
74 | ballDxStart = ballDx = 2,
75 | ballDyStart = ballDy = 2,
76 | ballVelocity = 3,
77 |
78 | /*
79 | * Paddle positions
80 | */
81 | paddleColour = '#FFFFFF',
82 | paddleX = 0,
83 | paddleY = 0,
84 | paddleHeight = 10,
85 | paddleWidth = 100,
86 |
87 | /*
88 | * scores
89 | */
90 | lastScore = 0,
91 | topScore = 0,
92 |
93 | /*
94 | * Current canvas transparency
95 | */
96 | canvasTransparency = 0,
97 | targetTransparency = 0.7,
98 |
99 | /*
100 | * keys
101 | */
102 | downDown = false,
103 | upDown = false,
104 | leftDown = false,
105 | rightDown = false,
106 |
107 | /*
108 | * Display debug messages?
109 | */
110 | debugMode = true,
111 |
112 | /*
113 | * Debug timeout
114 | */
115 | debugTimeout = null,
116 |
117 |
118 | /*
119 | * Fix browser CSS to disable scrolling
120 | */
121 | hideOverflow = function() {
122 |
123 | debug('hideOverflow()');
124 |
125 | body = document.getElementsByTagName('body')[0];
126 | // body.style.height = '100%';
127 | // body.style.width = '100%';
128 | body.style.overflow = 'hidden';
129 | },
130 |
131 |
132 | /*
133 | * Create canvas element
134 | */
135 | createCanvas = function() {
136 |
137 | debug('createCanvas()');
138 |
139 | // create canvas
140 | canvas = document.createElement('canvas');
141 | canvas.id = 'canvas';
142 | canvas.style.position = 'absolute';
143 | canvas.style.zIndex = 10000000; // hopefully that's high enough...
144 |
145 | setCanvasPosition();
146 |
147 | // add the canvas into the page
148 | body.appendChild(canvas);
149 |
150 | // get the draw context
151 | drawContext = canvas.getContext('2d');
152 | },
153 |
154 |
155 | /*
156 | * position canvas within visible portion of the screen
157 | */
158 | setCanvasPosition = function() {
159 | canvas.width = visibleWidth;
160 | canvas.height = visibleHeight;
161 | canvas.style.left = visibleXStart + 'px';
162 | canvas.style.top = visibleYStart + 'px';
163 | },
164 |
165 |
166 | /*
167 | * Calculate visible area of page
168 | */
169 | calculatePositions = function() {
170 |
171 | visibleXStart = window.pageXOffset;
172 | visibleYStart = window.pageYOffset;
173 |
174 | visibleWidth = window.innerWidth;
175 | visibleHeight = window.innerHeight;
176 |
177 | visibleXEnd = visibleWidth + visibleXStart;
178 | visibleYEnd = visibleHeight + visibleYStart;
179 |
180 | // set the ball to start within the visible frame
181 | ballX += visibleXStart;
182 | ballY += visibleYStart;
183 | },
184 |
185 |
186 |
187 | /*
188 | * Retrieve all images of a reasonable size
189 | */
190 | collectImages = function() {
191 |
192 | debug('collectImages()');
193 |
194 | // find all images on the page
195 | var allImages = document.getElementsByTagName('img');
196 |
197 | debug('collectImages: visible frame - X:' + visibleXStart + '-' + visibleXEnd + '; Y: ' + visibleYStart + '-' + visibleYEnd + ';');
198 |
199 | // loop through all images
200 | for (var counter = allImages.length - 1; counter >= 0; counter--){
201 |
202 | // get image details
203 | var imagePosition = findPos(allImages[counter]);
204 | var imageXStart = imagePosition[0] - visibleXStart;
205 | var imageYStart = imagePosition[1] - visibleYStart;
206 | var imageWidth = allImages[counter].width || allImages[counter].style.width;
207 | var imageHeight = allImages[counter].height || allImages[counter].style.height;
208 |
209 | // condition : test all images are large enough to be worth using
210 | if (imageWidth > 20 && imageHeight > 20) {
211 |
212 |
213 | // condition : if image is within visible (& safe) viewable area, use it
214 | if (
215 | imageXStart > (paddleHeight*2) &&
216 | imageYStart > (paddleHeight*2) &&
217 | (visibleXStart + imageXStart + imageWidth) < visibleXEnd - (paddleHeight*2) &&
218 | (visibleYStart + imageYStart + imageHeight) < visibleYEnd - (paddleHeight*2)
219 | ) {
220 |
221 | images.push({
222 | 'imageXStart' : imageXStart,
223 | 'imageYStart' : imageYStart,
224 | 'imageWidth' : imageWidth,
225 | 'imageHeight' : imageHeight,
226 | 'image' : allImages[counter],
227 | 'state' : 1,
228 | 'visible' : 0
229 | });
230 | }
231 | }
232 | }
233 |
234 | // store values for later use
235 | imagesAcceptable = imagesRemaining = images.length;
236 |
237 | debug('collectImages: - found ' + imagesAcceptable + ' out of ' + allImages.length);
238 | },
239 |
240 |
241 | /*
242 | * findpos - find an element's exact co-ordinates on a page
243 | * from : http://www.quirksmode.org/js/findpos.html
244 | */
245 | findPos = function(obj) {
246 | var curleft = curtop = 0;
247 | if (obj.offsetParent) {
248 | while (obj.offsetParent) {
249 | curleft += obj.offsetLeft;
250 | curtop += obj.offsetTop;
251 | obj = obj.offsetParent;
252 | }
253 | }
254 | return [curleft,curtop];
255 | },
256 |
257 |
258 | /*
259 | * keyboard navigation detection
260 | */
261 | onKeyDown = function(evt) {
262 |
263 | if (!evt) { evt = window.event; }
264 |
265 | if (evt.keyCode == 40) {
266 | downDown = true;
267 | } else if (evt.keyCode == 38) {
268 | upDown = true;
269 | }
270 |
271 | if (evt.keyCode == 39) {
272 | rightDown = true;
273 | } else if (evt.keyCode == 37) {
274 | leftDown = true;
275 | }
276 | },
277 |
278 |
279 | /*
280 | * keyboard navigation detection
281 | */
282 | onKeyUp = function(evt) {
283 |
284 | if (!evt) { evt = window.event; }
285 |
286 | if (evt.keyCode == 40) {
287 | downDown = false;
288 | } else if (evt.keyCode == 38) {
289 | upDown = false;
290 | }
291 |
292 | if (evt.keyCode == 39) {
293 | rightDown = false;
294 | } else if (evt.keyCode == 37) {
295 | leftDown = false;
296 | }
297 | },
298 |
299 |
300 | /*
301 | * mouse navigation detection
302 | */
303 | onMouseMove = function(evt) {
304 |
305 | if (!evt) { evt = window.event; }
306 |
307 | if (evt.pageX > 0 && evt.pageX < visibleXStart + visibleWidth) {
308 | paddleX = Math.max(evt.pageX - visibleXStart - (paddleWidth/2), 0);
309 | paddleX = Math.min(canvas.width - paddleWidth, paddleX);
310 | }
311 |
312 | if (evt.pageY > 0 && evt.pageY < visibleYStart + visibleHeight) {
313 | paddleY = Math.max(evt.pageY - visibleYStart - (paddleWidth/2), 0);
314 | paddleY = Math.min(canvas.height - paddleWidth, paddleY);
315 | }
316 | },
317 |
318 |
319 | /*
320 | * mouse clicking
321 | */
322 | onMouseClick = function(evt) {
323 |
324 | if (!evt) { evt = window.event; }
325 |
326 | // only used to start a game, so detect if we're waiting for this
327 | if (gameState != 'running') {
328 |
329 | if (imagesAcceptable == 0) {
330 | updateGameState('noImages');
331 | } else {
332 | updateGameState('running');
333 | }
334 | }
335 | },
336 |
337 |
338 |
339 | /*
340 | * draw bg
341 | */
342 | drawBg = function() {
343 | drawContext.clearRect(0, 0, canvas.width, canvas.height);
344 | drawContext.fillStyle = 'rgba(0, 0, 0, '+canvasTransparency+')';
345 | drawContext.fillRect(0, 0, canvas.width, canvas.height);
346 | },
347 |
348 |
349 | /*
350 | * create titles
351 | */
352 | drawTitle = function() {
353 | if (!textBlocks.titleBlock) {
354 | textBlocks.titleBlock = new CanvasLetters({
355 | textString:'Browser',
356 | name: 'titleBlock',
357 | x: 25,
358 | y: 25,
359 | blockSize: 10,
360 | animate:true,
361 | ordering:textAnimationTypes[Math.round(Math.random()*textAnimationTypes.length-1)]
362 | });
363 | }
364 | if (!textBlocks.subTitleBlock) {
365 | textBlocks.subTitleBlock = new CanvasLetters({
366 | textString:'Breakout',
367 | name: 'subTitleBlock',
368 | x: 25,
369 | y: 125,
370 | blockSize: 10,
371 | animate:true,
372 | ordering:textAnimationTypes[Math.round(Math.random()*textAnimationTypes.length-1)]
373 | });
374 | }
375 | },
376 |
377 |
378 | /*
379 | * create play now text
380 | */
381 | drawPlayNow = function() {
382 | if (!textBlocks.playNow) {
383 | textBlocks.playNow = new CanvasLetters({
384 | textString:'Click to play',
385 | name:'playNow',
386 | x: 25,
387 | y: 250,
388 | blockSize: 5
389 | });
390 | }
391 | },
392 |
393 |
394 | /*
395 | * create top scores
396 | */
397 | drawTopScores = function() {
398 | if (!textBlocks.lastScore) {
399 | textBlocks.lastScore = new CanvasLetters({
400 | textString:'Last score - ' + lastScore,
401 | name:'lastScore',
402 | x: 25,
403 | y: -25,
404 | blockSize: 2,
405 | clearance: 2
406 | });
407 | }
408 |
409 | if (!textBlocks.topScore) {
410 | textBlocks.topScore = new CanvasLetters({
411 | textString:'Top score - ' + topScore,
412 | name:'topScore',
413 | x: 25,
414 | y: -50,
415 | blockSize: 2,
416 | clearance: 2
417 | });
418 | }
419 | },
420 |
421 |
422 | /*
423 | * create creds
424 | */
425 | drawCreds = function() {
426 | if (!textBlocks.creds) {
427 | textBlocks.creds = new CanvasLetters({
428 | textString:'blah',
429 | name:'creds',
430 | x: -25,
431 | y: -25,
432 | blockSize: 2,
433 | clearance: 2
434 | });
435 | }
436 | },
437 |
438 |
439 | /*
440 | * create in-game scores
441 | */
442 | drawScore = function() {
443 | if (!textBlocks.score) {
444 | textBlocks.score = new CanvasLetters({
445 | textString:'Score - ' + (imagesAcceptable - imagesRemaining),
446 | name:'score',
447 | x: 25,
448 | y: -25,
449 | blockSize: 2,
450 | clearance: 2
451 | });
452 | }
453 |
454 | if (!textBlocks.remaining) {
455 | textBlocks.remaining = new CanvasLetters({
456 | textString:'Blocks Remaining - ' + imagesRemaining,
457 | name:'remaining',
458 | x: -25,
459 | y: -25,
460 | blockSize: 2,
461 | clearance: 2
462 | });
463 | }
464 | },
465 |
466 |
467 | /*
468 | * create game end message
469 | */
470 | drawLoseMessage = function() {
471 | if (!textBlocks.loseMessage) {
472 | textBlocks.loseMessage = new CanvasLetters({
473 | textString:'You Lose',
474 | name:'loseMessage',
475 | x: -25,
476 | y: 25,
477 | blockSize: 5,
478 | clearance: 5
479 | });
480 | }
481 | },
482 |
483 |
484 | /*
485 | * create game end message
486 | */
487 | drawWinMessage = function() {
488 | if (!textBlocks.winMessage) {
489 | textBlocks.winMessage = new CanvasLetters({
490 | textString:'You Win!',
491 | name:'winMessage',
492 | x: -25,
493 | y: 25,
494 | blockSize: 5,
495 | clearance: 5
496 | });
497 | }
498 | },
499 |
500 |
501 | /*
502 | * create no images message
503 | */
504 | drawNoImagesMessage = function() {
505 | if (!textBlocks.noImages) {
506 | textBlocks.noImages = new CanvasLetters({
507 | textString:'No suitable images found',
508 | name:'noImages',
509 | x: 25,
510 | y: 400,
511 | blockSize: 5,
512 | clearance: 5
513 | });
514 | }
515 | },
516 |
517 |
518 | /*
519 | * draw text blocks - called every time by the draw() loop
520 | */
521 | drawBlocks = function() {
522 | for (textBlock in textBlocks){
523 | if (textBlocks[textBlock].isActive()) {
524 | textBlocks[textBlock].drawBlocks();
525 | }
526 | };
527 | },
528 |
529 |
530 | /*
531 | * Change the game state -
532 | * resets blocks and game parameters
533 | */
534 | updateGameState = function(state) {
535 |
536 | // reset game state
537 | drawBg();
538 | removeAllText();
539 | updateHighScore();
540 | gameState = state;
541 | ballX = ballXStart;
542 | ballY = ballYStart;
543 | ballDx = ballDxStart;
544 | ballDy = ballDyStart;
545 |
546 | // reset images
547 | for (var i = images.length - 1; i >= 0; i--){
548 | images[i].state = 1;
549 | images[i].visible = 0;
550 | }
551 |
552 | // reset text
553 | if (state != 'running') {
554 | textBlocks.titleBlock.setActiveVal(1);
555 | textBlocks.subTitleBlock.setActiveVal(1);
556 | textBlocks.creds.setActiveVal(1);
557 | textBlocks.lastScore.updateString('Last score - ' + lastScore);
558 | textBlocks.topScore.updateString('Top score - ' + topScore);
559 |
560 | // condition : show optional extra message
561 | if (state == 'noImages') {
562 | if (!!textBlocks.noImages) {
563 | textBlocks.noImages.setActiveVal(1);
564 | }
565 | } else if (state == 'gameEnded') {
566 | if (!!textBlocks.loseMessage) {
567 | textBlocks.loseMessage.setActiveVal(1);
568 | }
569 | } else if (state == 'victory') {
570 | if (!!textBlocks.winMessage) {
571 | textBlocks.winMessage.setActiveVal(1);
572 | }
573 | }
574 | } else {
575 | if (!!textBlocks.score && !!textBlocks.remaining) {
576 | textBlocks.score.updateString('Score - ' + (imagesAcceptable - imagesRemaining));
577 | textBlocks.remaining.updateString('Blocks Remaining - ' + imagesRemaining);
578 | }
579 | }
580 |
581 | imagesRemaining = imagesAcceptable;
582 | debug('changed gameState to ' + state);
583 | },
584 |
585 |
586 | /*
587 | * Remove all existing text
588 | */
589 | removeAllText = function() {
590 |
591 | // reset all blocks ready to be redrawn
592 | for (textBlock in textBlocks){
593 | textBlocks[textBlock].setActiveVal(0);
594 | }
595 | },
596 |
597 |
598 |
599 | /*
600 | * During the game, when a block has been hit, update the score
601 | */
602 | updateScore = function() {
603 | imagesRemaining--;
604 | textBlocks.score.updateString('Score - ' + (imagesAcceptable - imagesRemaining));
605 | textBlocks.remaining.updateString('Blocks Remaining - ' + imagesRemaining);
606 | },
607 |
608 |
609 | /*
610 | * When the game is complete, check whether this is a high score
611 | */
612 | updateHighScore = function() {
613 | lastScore = imagesAcceptable - imagesRemaining;
614 | if (lastScore > topScore) {
615 | topScore = lastScore;
616 | }
617 | },
618 |
619 |
620 | /*
621 | * draw images
622 | */
623 | drawImages = function() {
624 |
625 | for (var counter = images.length - 1; counter >= 0; counter--){
626 | var image = images[counter];
627 |
628 | // condition : detect collision
629 | if (
630 | image.state == 1 &&
631 | ballX > image.imageXStart && ballX < image.imageXStart + image.imageWidth &&
632 | ballY > image.imageYStart && ballY < image.imageYStart + image.imageHeight
633 | ) {
634 |
635 | // detect hit placement
636 | var hit = [
637 | { dir: 'top', val: ballY - image.imageYStart },
638 | { dir: 'bottom', val: (image.imageYStart+image.imageHeight) - ballY },
639 | { dir: 'left', val: ballX - image.imageXStart },
640 | { dir: 'right', val: (image.imageXStart+image.imageWidth) - ballX }
641 | ];
642 |
643 | // sort to find where it hit
644 | function sortByLowest(a, b) {
645 | var x = a.val;
646 | var y = b.val;
647 | return ((x < y) ? -1 : ((x > y) ? 1 : 0));
648 | }
649 | hit.sort(sortByLowest);
650 |
651 | if (hit[0].dir == 'top' || hit[0].dir == 'bottom') {
652 | ballDy = -ballDy;
653 | } else {
654 | ballDx = -ballDx;
655 | }
656 |
657 |
658 | updateScore();
659 | image.state = 0;
660 | }
661 |
662 |
663 | // condition : if the still image exists, display it!
664 | if (image.state == 1) {
665 | drawContext.drawImage(image.image, image.imageXStart, image.imageYStart);
666 | }
667 | };
668 | },
669 |
670 |
671 | /*
672 | * draw the ball
673 | */
674 | drawBall = function() {
675 |
676 | // condition : adjust velocity for slow-moving ball
677 | var v = ballVelocity;
678 | if (Math.abs(ballDx) < 1 || Math.abs(ballDy) < 1) { v +=1; }
679 | if (Math.abs(ballDx) < 1 && Math.abs(ballDy) < 1) { v +=1; }
680 | if (Math.abs(ballDx) < 0.75 && Math.abs(ballDy) < 0.75) { v +=1; }
681 | if (Math.abs(ballDx) < 0.5 && Math.abs(ballDy) < 0.5) { v +=1; }
682 |
683 | // set new ball position
684 | ballX += v * ballDx;
685 | ballY += v * ballDy;
686 |
687 | // draw new ball
688 | drawContext.fillStyle = ballColour;
689 | drawCircle(ballX, ballY, ballRadius);
690 | },
691 |
692 |
693 | /*
694 | * draw the four paddles around the edge
695 | */
696 | drawPaddles = function() {
697 |
698 | // calculate whether to move paddles
699 | if (rightDown) {
700 | paddleX += 20;
701 | } else if (leftDown) {
702 | paddleX -= 20;
703 | }
704 |
705 | if (downDown) {
706 | paddleY += 20;
707 | } else if (upDown) {
708 | paddleY -= 20;
709 | }
710 |
711 | if (paddleX < 0) { paddleX = 0; } //left
712 | if (paddleX > visibleWidth-paddleWidth) { paddleX=visibleWidth-paddleWidth; } // right
713 | if (paddleY < 0) { paddleY = 0; } // top
714 | if (paddleY > visibleHeight-paddleWidth) { paddleY=visibleHeight-paddleWidth; } // bottom
715 |
716 |
717 | drawContext.fillStyle = 'rgba(0, 0, 0, '+canvasTransparency+')';
718 |
719 |
720 | // calculate bottom paddle hit detection
721 | if (ballY + ballDy + ballRadius > visibleHeight - paddleHeight) {
722 | if (ballX > paddleX && ballX < paddleX + paddleWidth) {
723 | ballDx = 5 * ((ballX - (paddleX + paddleWidth / 2)) / paddleWidth);
724 | ballDy = -ballDy;
725 | debug('Bottom Paddle Hit - Dx: ' + ballDx + '; Dy: ' + ballDy);
726 | }
727 | }
728 |
729 |
730 | // calculate top paddle hit detection
731 | if (ballY + ballDy - ballRadius < paddleHeight) {
732 | if (ballX > paddleX && ballX < paddleX + paddleWidth) {
733 | ballDx = 5 * ((ballX - (paddleX + paddleWidth / 2)) / paddleWidth);
734 | ballDy = -ballDy;
735 | debug('Top Paddle Hit - Dx: ' + ballDx + '; Dy: ' + ballDy);
736 | }
737 | }
738 |
739 |
740 | // calculate right paddle hit detection
741 | if (ballX + ballDx + ballRadius > visibleWidth - paddleHeight) {
742 | if (ballY > paddleY && ballY < paddleY + paddleWidth) {
743 | ballDy = 5 * ((ballY - (paddleY + paddleWidth / 2)) / paddleWidth);
744 | ballDx = -ballDx;
745 | debug('Right Paddle Hit: ' + ballDx + '; Dy: ' + ballDy);
746 | }
747 | }
748 |
749 |
750 | // calculate left paddle hit detection
751 | if (ballX + ballDx - ballRadius < paddleHeight) {
752 | if (ballY > paddleY && ballY < paddleY + paddleWidth) {
753 | ballDy = 5 * ((ballY - (paddleY + paddleWidth / 2)) / paddleWidth);
754 | ballDx = -ballDx;
755 | debug('Left Paddle Hit: ' + ballDx + '; Dy: ' + ballDy);
756 | }
757 | }
758 |
759 |
760 | // draw paddles
761 | drawContext.fillStyle = paddleColour;
762 | drawRectangle(paddleX, 0, paddleWidth, paddleHeight); // top
763 | drawRectangle(paddleX, visibleHeight - paddleHeight, paddleWidth, paddleHeight); // bottom
764 | drawRectangle(0, paddleY, paddleHeight, paddleWidth); // left
765 | drawRectangle(visibleWidth - paddleHeight, paddleY, paddleHeight, paddleWidth); // right
766 | },
767 |
768 |
769 | /*
770 | * draw a circle
771 | */
772 | drawCircle = function(x,y,r) {
773 | drawContext.beginPath();
774 | drawContext.arc(x, y, r, 0, Math.PI*2, true);
775 | drawContext.closePath();
776 | drawContext.fill();
777 | },
778 |
779 |
780 | /*
781 | * draw a rectangle
782 | */
783 | drawRectangle = function(x,y,w,h) {
784 | drawContext.beginPath();
785 | drawContext.rect(x,y,w,h);
786 | drawContext.closePath();
787 | drawContext.fill();
788 | },
789 |
790 |
791 | /*
792 | * Draw method called by the loop
793 | */
794 | draw = function() {
795 |
796 | // condition : detect game state
797 | switch (gameState) {
798 |
799 | // show title screen?
800 | case 'title' : case 'gameEnded' : case 'victory' : case 'noImages' :
801 |
802 | // draw background (with initial fade in)
803 | if (canvasTransparency < targetTransparency) {
804 | canvasTransparency +=0.1;
805 | drawBg();
806 |
807 | // bg is faded in, show title screen
808 | } else {
809 |
810 | // show title
811 | drawTitle();
812 |
813 | // show play now text
814 | drawPlayNow();
815 |
816 | // display scores
817 | drawTopScores();
818 |
819 | // display creds
820 | drawCreds();
821 |
822 | // draw above text
823 | drawBlocks();
824 |
825 |
826 | // condition : if we've just lost, show message
827 | if (gameState == 'gameEnded') {
828 |
829 | drawLoseMessage();
830 |
831 | // show VICTORY message
832 | } else if (gameState == 'victory') {
833 |
834 | drawWinMessage();
835 |
836 | // show NO IMAGES message
837 | } else if (gameState == 'noImages') {
838 |
839 | drawNoImagesMessage();
840 |
841 | }
842 | }
843 |
844 | break;
845 |
846 |
847 | // game is in progress?
848 | case 'running' :
849 |
850 | drawBg();
851 |
852 | // draw ball
853 | drawBall();
854 |
855 | // draw score
856 | drawScore();
857 |
858 | // draw images
859 | drawImages();
860 |
861 | // draw paddles
862 | drawPaddles();
863 |
864 | // if text needs drawing, draw it
865 | drawBlocks();
866 |
867 |
868 | // calculate game state
869 | if (imagesRemaining == 0) {
870 | updateGameState('victory');
871 | }
872 |
873 | if (
874 | ballX + ballDx + ballRadius > visibleWidth || // right
875 | ballX + ballDx - ballRadius < 0 || // left
876 | ballY + ballDy + ballRadius > visibleHeight || // bottom
877 | ballY + ballDy - ballRadius < 0 // top
878 | ) {
879 | updateGameState('gameEnded');
880 | }
881 |
882 |
883 | break;
884 | }
885 | },
886 |
887 |
888 | /*
889 | * Debug
890 | * output debug messages
891 | *
892 | * @return void
893 | * @private
894 | */
895 | debug = function(content) {
896 | if (!!debugMode) {
897 | console.log(content);
898 | clearTimeout(debugTimeout);
899 | debugTimeout = setTimeout(debugSpacer, 2000);
900 | }
901 | },
902 | debugSpacer = function() {
903 | if (!!debugMode) {
904 | console.log('----------------------------------------------------------------------------------');
905 | }
906 | },
907 |
908 |
909 | /*
910 | * initialisation method
911 | */
912 | init = function(){
913 |
914 | debug('init()');
915 |
916 |
917 | // fix browser CSS to disable scrolling
918 | hideOverflow();
919 |
920 |
921 | // calculate browser positions
922 | calculatePositions();
923 |
924 |
925 | // retrieve all images of a reasonable size
926 | collectImages();
927 |
928 |
929 | // create canvas element
930 | if (!canvas) {
931 | createCanvas();
932 | }
933 |
934 |
935 | // set up listeners for keys and mouse...
936 | document.onmousemove = function(e) {
937 | onMouseMove(e);
938 | };
939 | document.onkeydown = function(e) {
940 | onKeyDown(e);
941 | };
942 | document.onkeyup = function(e) {
943 | onKeyUp(e);
944 | };
945 | document.onclick = function(e) {
946 | onMouseClick(e);
947 | };
948 | document.onkeypress = function(e) {
949 | onMouseClick(e);
950 |
951 | // stop browser scrolling on click
952 | if (!e) { e = window.event; }
953 | if (e.keyCode == 40 || e.keyCode == 38 || e.keyCode == 39 || e.keyCode == 37) {
954 | return false;
955 | }
956 |
957 | // escape or X - remove canvas
958 | if (e.keyCode == 27 || e.keyCode == 120) {
959 | canvas.parentNode.removeChild(canvas);
960 | //return false;
961 | }
962 | };
963 |
964 |
965 | // draw!
966 | drawInterval = setInterval(draw, 25);
967 | },
968 |
969 |
970 | /*
971 | * reset game (on browser resize)
972 | */
973 | resetGame = function() {
974 | if (!reseting) {
975 | reseting = true;
976 | clearInterval(drawInterval);
977 | debug('game reset');
978 | updateGameState('title');
979 | canvasTransparency = 0;
980 | images = [];
981 | imagesAcceptable = 0;
982 | calculatePositions();
983 | setCanvasPosition();
984 | init();
985 | }
986 | reseting = false;
987 | };
988 |
989 |
990 | /*
991 | * restart on resize
992 | */
993 | window.onresize = function() {
994 | resetGame();
995 | };
996 |
997 |
998 |
999 |
1000 |
1001 | /*
1002 | * Canvas Letters - used for in-game text
1003 | *
1004 | */
1005 | var CanvasLetters = function(initOptions) {
1006 |
1007 | /*
1008 | * Array of blocks to draw
1009 | */
1010 | var blocks = [],
1011 | blockCount = 0,
1012 |
1013 | /*
1014 | * current block drawing details
1015 | */
1016 | currentX = 0,
1017 | currentY = 0,
1018 | currentBlock = 0,
1019 | lineCount = 1,
1020 |
1021 | /*
1022 | * Character block dimensions
1023 | */
1024 | characterBlockWidth = 5,
1025 | characterBlockHeight = 7,
1026 |
1027 | /*
1028 | * the (potentially modified) text string we're drawing
1029 | */
1030 | textString = '',
1031 |
1032 | /*
1033 | * Characters
1034 | */
1035 | characters = {
1036 | 'a': [0,0,1,0,0,0,1,0,1,0,1,0,0,0,1,1,0,0,0,1,1,1,1,1,1,1,0,0,0,1,1,0,0,0,1],
1037 | 'b': [1,1,1,1,0,1,0,0,0,1,1,0,0,0,1,1,1,1,1,0,1,0,0,0,1,1,0,0,0,1,1,1,1,1,0],
1038 | 'c': [0,1,1,1,0,1,0,0,0,1,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,1,0,1,1,1,0],
1039 | 'd': [1,1,1,0,0,1,0,0,1,0,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,1,0,1,1,1,0,0],
1040 | 'e': [1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,1,1,1,0,0,1,0,0,0,0,1,0,0,0,0,1,1,1,1,1],
1041 | 'f': [1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,1,1,1,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0],
1042 | 'g': [0,1,1,1,0,1,0,0,0,1,1,0,0,0,0,1,0,1,1,1,1,0,0,0,1,1,0,0,0,1,0,1,1,1,1],
1043 | 'h': [1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,1,1,1,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1],
1044 | 'i': [1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,1,1,1,1],
1045 | 'j': [1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,1,1,0,0],
1046 | 'k': [1,0,0,0,1,1,0,0,1,0,1,0,1,0,0,1,1,0,0,0,1,0,1,0,0,1,0,0,1,0,1,0,0,0,1],
1047 | 'l': [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,1,1,1,1],
1048 | 'm': [1,0,0,0,1,1,1,0,1,1,1,0,1,0,1,1,0,1,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1],
1049 | 'n': [1,0,0,0,1,1,0,0,0,1,1,1,0,0,1,1,0,1,0,1,1,0,0,1,1,1,0,0,0,1,1,0,0,0,1],
1050 | 'o': [0,1,1,1,0,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,0,1,1,1,0],
1051 | 'p': [1,1,1,1,0,1,0,0,0,1,1,0,0,0,1,1,1,1,1,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0],
1052 | 'q': [0,1,1,1,0,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,1,0,1,1,0,0,1,0,0,1,1,0,1],
1053 | 'r': [1,1,1,1,0,1,0,0,0,1,1,0,0,0,1,1,1,1,1,0,1,0,1,0,0,1,0,0,1,0,1,0,0,0,1],
1054 | 's': [0,1,1,1,0,1,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,0,0,0,1,0,1,1,1,0],
1055 | 't': [1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0],
1056 | 'u': [1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,0,1,1,1,0],
1057 | 'v': [1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,0,1,0,1,0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,0],
1058 | 'w': [1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,1,0,1,1,0,1,0,1,1,0,1,0,1,0,1,0,1,0],
1059 | 'x': [1,0,0,0,1,1,0,0,0,1,0,1,0,1,0,0,0,1,0,0,0,1,0,1,0,1,0,0,0,1,1,0,0,0,1],
1060 | 'y': [1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,0,1,0,1,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0],
1061 | 'z': [1,1,1,1,1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,1,1,1,1,1],
1062 | '0': [0,1,1,1,0,1,0,0,0,1,1,0,0,1,1,1,0,1,0,1,1,1,0,0,1,1,0,0,0,1,0,1,1,1,0],
1063 | '1': [0,0,1,0,0,0,1,1,0,0,1,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,1,1,1,1],
1064 | '2': [0,1,1,1,0,1,0,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1],
1065 | '3': [0,1,1,1,0,1,0,0,0,1,0,0,0,0,1,0,0,1,1,0,0,0,0,0,1,1,0,0,0,1,0,1,1,1,0],
1066 | '4': [0,0,0,1,0,0,0,1,1,0,0,1,0,1,0,1,0,0,1,0,1,1,1,1,1,0,0,0,1,0,0,0,0,1,0],
1067 | '5': [1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,0,0,0,1,0,1,1,1,0],
1068 | '6': [0,0,1,1,0,0,1,0,0,0,1,0,0,0,0,1,1,1,1,0,1,0,0,0,1,1,0,0,0,1,0,1,1,1,0],
1069 | '7': [1,1,1,1,1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0],
1070 | '8': [0,1,1,1,0,1,0,0,0,1,1,0,0,0,1,0,1,1,1,0,1,0,0,0,1,1,0,0,0,1,0,1,1,1,0],
1071 | '9': [0,1,1,1,0,1,0,0,0,1,1,0,0,0,1,0,1,1,1,1,0,0,0,0,1,0,0,0,1,0,0,1,1,0,0],
1072 | '-': [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
1073 | '?': [0,1,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0],
1074 | '!': [0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0],
1075 | '@': [0,1,1,1,0,1,0,0,0,1,1,0,1,1,1,1,0,1,0,1,1,0,1,1,0,1,0,0,0,1,0,1,1,1,0],
1076 | '&': [0,1,1,0,0,1,0,0,1,0,1,0,1,0,0,0,1,0,0,0,1,0,1,0,1,1,0,0,1,0,0,1,1,0,1],
1077 | '.': [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0],
1078 | ' ': [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
1079 | },
1080 |
1081 |
1082 |
1083 | /*
1084 | * default options
1085 | * (the ones to copy from if an option isn't specified specifically)
1086 | */
1087 | defaults = {
1088 | blockColour : 'ff9900',
1089 | blockSize : 10,
1090 | textString : '...',
1091 | clearance : 10,
1092 | ordering : 'default',
1093 | animate : false,
1094 | name : null,
1095 | active : 1,
1096 | x : 0,
1097 | y : 0
1098 | },
1099 |
1100 | /*
1101 | * config options
1102 | * (the combined options, the ones to use)
1103 | */
1104 | options = {},
1105 |
1106 |
1107 | /*
1108 | * save any options sent through to the intialisation script, if set
1109 | */
1110 | saveOptions = function() {
1111 | for (var option in defaults) {
1112 | if (!!initOptions[option]) {
1113 | options[option] = initOptions[option];
1114 | } else {
1115 | options[option] = defaults[option];
1116 | }
1117 | }
1118 | },
1119 |
1120 |
1121 | /*
1122 | * condition : is this textBlock active?
1123 | */
1124 | isActive = function() {
1125 | return options.active;
1126 | },
1127 |
1128 |
1129 | /*
1130 | * condition : is this textBlock active?
1131 | */
1132 | setActiveVal = function(val) {
1133 | options.active = val;
1134 | if (!!options.animate) {
1135 | currentBlock = 0;
1136 | }
1137 | },
1138 |
1139 |
1140 | /*
1141 | * condition : redraw new string
1142 | */
1143 | updateString = function(str) {
1144 | debug('updateString');
1145 | options.textString = str;
1146 | setActiveVal(1);
1147 | startLetters();
1148 | },
1149 |
1150 |
1151 | /*
1152 | * Start letters
1153 | */
1154 | startLetters = function() {
1155 |
1156 | // init values
1157 | lineCount = 1;
1158 | currentBlock = 0;
1159 | blocks = [];
1160 | blockCount = 0;
1161 |
1162 | if (options.x < 0) {
1163 | currentX = visibleWidth - (options.blockSize*characterBlockHeight*(options.textString.length-1)) + options.x;
1164 | } else {
1165 | currentX = options.x + 0;
1166 | }
1167 |
1168 | if (options.y < 0) {
1169 | currentY = visibleHeight - options.clearance - (options.blockSize*characterBlockHeight) + options.y;
1170 | } else {
1171 | currentY = options.y + 0;
1172 | }
1173 |
1174 | fixTextLength();
1175 | calculateBlockPositions();
1176 |
1177 | // if we're not animating, show everything at once
1178 | if (!options.animate) {
1179 | currentBlock = blocks.length;
1180 | }
1181 |
1182 | // add this into the queue of text to be drawn
1183 | if (!!textBlocks['options.name']) {
1184 | textBlocks['options.name'].setActiveVal(1);
1185 | }
1186 | debug('adding ' + options.name + ' to queue');
1187 | },
1188 |
1189 |
1190 |
1191 | /*
1192 | *
1193 | */
1194 | fixTextLength = function() {
1195 |
1196 | textString = options.textString.toLowerCase();
1197 |
1198 | // calculate line length
1199 | var lineLength = Math.floor( ( canvas.width - options.clearance ) / ( ( characterBlockWidth * options.blockSize ) + options.clearance ) );
1200 |
1201 | // test each word invidivually
1202 | textStringArray = textString.split(' ');
1203 | for (var counter = textStringArray.length - 1; counter >= 0; counter--){
1204 |
1205 | // if any words are longer than the line-length, hyphenate
1206 | if (textStringArray[counter].length > lineLength) {
1207 |
1208 | var originalWord = word = textStringArray[counter];
1209 | var wordArray = [];
1210 |
1211 | // split the word every time it hits the line length
1212 | while (word.length > lineLength) {
1213 | wordArray.push(word.substr(0, lineLength-1));
1214 | word = word.substr(lineLength-1);
1215 | }
1216 | wordArray.push(word);
1217 |
1218 | textString = textString.replace(originalWord, wordArray.join('- '));
1219 | }
1220 | };
1221 | },
1222 |
1223 |
1224 |
1225 | /*
1226 | *
1227 | */
1228 | calculateBlockPositions = function() {
1229 |
1230 | // draw the text string
1231 | for (var character = 0, textStringLength = textString.length; character < textString.length; character++) {
1232 |
1233 | // if we can draw this letter, begin
1234 | if (!!characters[textString[character]]) {
1235 |
1236 | // if this isn't the first character, work out how far along the line to put it
1237 | if (character > 0) {
1238 | currentX += (options.blockSize * characterBlockWidth) + options.clearance;
1239 | }
1240 |
1241 | // find the position of the next space (to calculate the word length)
1242 | var nextSpacePosition = textString.indexOf(' ', character);
1243 | if (nextSpacePosition == -1) { nextSpacePosition = textStringLength; }
1244 |
1245 | // start working out where to place the new letter/word
1246 | var newLineRequired = false;
1247 |
1248 |
1249 | // condition : is this word going to fit on the current line?
1250 | if (currentX + (options.blockSize * (characterBlockWidth*(nextSpacePosition-character))) + (options.clearance*(nextSpacePosition-character)) > canvas.width - options.clearance) {
1251 | newLineRequired = true;
1252 | }
1253 |
1254 |
1255 | // condition : start a new line?
1256 | if (newLineRequired && textString[character] != ' ') {
1257 | currentX = options.clearance;
1258 | currentY = (lineCount*(characterBlockHeight*options.blockSize)) + (options.clearance*++lineCount);
1259 | }
1260 |
1261 |
1262 | // get the blocks for this character
1263 | var blockArray = characters[textString[character]];
1264 |
1265 | // for each block within a character
1266 | for (var block = 0, blockArrayLength = blockArray.length; block < blockArrayLength; block++) {
1267 |
1268 | // calculate X & Y positions for each block
1269 | var x = currentX;
1270 | var y = currentY;
1271 | x += (options.blockSize * (block % characterBlockWidth));
1272 | if (block >= characterBlockWidth) {
1273 | y += (options.blockSize*(Math.floor(block/characterBlockWidth)));
1274 | }
1275 |
1276 | // if we're drawing a block, add it to the array
1277 | if (blockArray[block] == 1) {
1278 | blocks.push({x:x,y:y,opacity:0});
1279 | }
1280 | }
1281 | }
1282 | }
1283 |
1284 | // condition : change order of appearing blocks
1285 | switch (options.ordering) {
1286 | case 'vertical':
1287 | function vertical(a, b) { return a.y - b.y; }
1288 | blocks.sort(vertical);
1289 | break;
1290 |
1291 | case 'horizontal':
1292 | function horizontal(a, b) { return a.x - b.x; }
1293 | blocks.sort(horizontal);
1294 | break;
1295 |
1296 | case 'reverse':
1297 | blocks.reverse();
1298 | break;
1299 |
1300 | case 'random':
1301 | function randOrd(){ return (Math.round(Math.random())-0.5); }
1302 | blocks.sort(randOrd);
1303 | break;
1304 | }
1305 |
1306 |
1307 | blockCount = blocks.length;
1308 | },
1309 |
1310 |
1311 | /*
1312 | *
1313 | */
1314 | drawBlocks = function() {
1315 |
1316 | // normal direction, add blocks
1317 | var drawColour = options.blockColour;
1318 |
1319 | // calculate which blocks to work on
1320 | var animateLimit = (!!options.animate) ? currentBlock-10 : 0;
1321 |
1322 | // loop through blocks and draw!
1323 | for (var counter = animateLimit; counter < currentBlock; counter++) {
1324 | if (!!blocks[counter]) {
1325 | if (blocks[counter].opacity < 1) { blocks[counter].opacity += 0.1; }
1326 | drawContext.fillStyle = 'rgba('+HexToRGB(drawColour)+', '+blocks[counter].opacity+')';
1327 | drawRectangle(blocks[counter].x, blocks[counter].y, options.blockSize, options.blockSize);
1328 | }
1329 | };
1330 |
1331 | // add one to loop
1332 | currentBlock++;
1333 |
1334 | // calculate whether to end the drawing
1335 | if (currentBlock == blockCount+10) {
1336 |
1337 | // remove this from the queue of to be drawn
1338 | debug('finished drawing ' + options.textString);
1339 | }
1340 | },
1341 |
1342 |
1343 | /*
1344 | * Turn Hex into RGB, for block colour
1345 | */
1346 | HexToRGB = function(h) {return HexToR(h) +','+HexToG(h)+','+HexToB(h);},
1347 | HexToR = function(h) {return parseInt((cutHex(h)).substring(0,2),16);},
1348 | HexToG = function(h) {return parseInt((cutHex(h)).substring(2,4),16);},
1349 | HexToB = function(h) {return parseInt((cutHex(h)).substring(4,6),16);},
1350 | cutHex = function(h) {return (h.charAt(0)=='#') ? h.substring(1,7):h;},
1351 |
1352 |
1353 |
1354 | /*
1355 | * initialisation method
1356 | */
1357 | __init = function(){
1358 |
1359 | // save the init options
1360 | saveOptions();
1361 |
1362 | debug('drawing ' + options.textString);
1363 |
1364 | // init canvas set-up
1365 | startLetters();
1366 |
1367 | }();
1368 |
1369 |
1370 | /*
1371 | * expose public methods
1372 | */
1373 | return {
1374 | init: init,
1375 | isActive : isActive,
1376 | setActiveVal : setActiveVal,
1377 | drawBlocks: drawBlocks,
1378 | updateString: updateString
1379 | };
1380 | };
1381 |
1382 |
1383 | // start the game!
1384 | init();
1385 |
1386 | }();
--------------------------------------------------------------------------------