Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(89)

Side by Side Diff: chrome/renderer/resources/offline.js

Issue 545973003: Update network error template to new design (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Revert details link back to a button Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 (function() {
5 'use strict';
6 /**
7 * T-Rex runner.
8 * @param {string} outerContainerId Outer containing element id.
9 * @param {object} opt_config
10 * @constructor
11 * @export
12 */
13 function Runner(outerContainerId, opt_config) {
14 // Singleton
15 if (Runner.instance_) {
16 return Runner.instance_;
17 }
18 Runner.instance_ = this;
19
20 this.outerContainerEl = document.querySelector(outerContainerId);
21 this.containerEl = null;
22
23 this.config = opt_config || Runner.config;
24
25 this.dimensions = Runner.defaultDimensions;
26
27 this.canvas = null;
28 this.canvasCtx = null;
29
30 this.tRex = null;
31
32 this.distanceMeter = null;
33 this.distanceRan = 0;
34
35 this.highestScore = 0;
36
37 this.time = 0;
38 this.runningTime = 0;
39 this.msPerFrame = 1000 / FPS;
40 this.currentSpeed = this.config.SPEED;
41
42 this.obstacles = [];
43
44 this.started = false;
45 this.activated = false;
46 this.crashed = false;
47 this.paused = false;
48
49 this.resizeTimerId_ = null;
50
51 this.playCount = 0;
52
53 // Sound FX.
54 this.audioBuffer = null;
55 this.soundFx = {};
56
57 // Global web audio context for playing sounds.
58 this.audioContext = new AudioContext();
59
60 // Images.
61 this.images = {};
62 this.imagesLoaded = 0;
63 this.loadImages();
64 }
65 window['Runner'] = Runner;
66
67
68 /**
69 * Default game width.
70 * @const
71 */
72 var DEFAULT_WIDTH = 600;
73
74 /**
75 * Frames per second.
76 * @const
77 */
78 var FPS = 60;
79
80 /** @const */
81 var IS_HIDPI = window.devicePixelRatio > 1;
82
83 /** @const */
84 var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1;
85
86 /** @const */
87 var IS_TOUCH_ENABLED = 'ontouchstart' in window;
88
89
90 /**
91 * Default game configuration.
92 * @enum {number}
93 */
94 Runner.config = {
95 ACCELERATION: 0.001,
96 BG_CLOUD_SPEED: 0.2,
97 BOTTOM_PAD: 10,
98 CLEAR_TIME: 3000,
99 CLOUD_FREQUENCY: 0.5,
100 GAMEOVER_CLEAR_TIME: 750,
101 GAP_COEFFICIENT: 0.6,
102 GRAVITY: 0.6,
103 INITIAL_JUMP_VELOCITY: 12,
104 MAX_CLOUDS: 6,
105 MAX_OBSTACLE_LENGTH: 3,
106 MAX_SPEED: 12,
107 MIN_JUMP_HEIGHT: 35,
108 MOBILE_SPEED_COEFFICIENT: 1.2,
109 RESOURCE_TEMPLATE_ID: 'audio-resources',
110 SPEED: 6,
111 SPEED_DROP_COEFFICIENT: 3
112 };
113
114
115 /**
116 * Default dimensions.
117 * @enum {string}
118 */
119 Runner.defaultDimensions = {
120 WIDTH: DEFAULT_WIDTH,
121 HEIGHT: 150
122 };
123
124
125 /**
126 * CSS class names.
127 * @enum {string}
128 */
129 Runner.classes = {
130 CANVAS: 'runner-canvas',
131 CONTAINER: 'runner-container',
132 CRASHED: 'crashed',
133 ICON: 'icon-offline',
134 TOUCH_CONTROLLER: 'controller'
135 };
136
137
138 /**
139 * Image source urls.
140 * @enum {array.<object>}
141 */
142 Runner.imageSources = {
143 LDPI: [
144 {name: 'CACTUS_LARGE', id: '1x-obstacle-large'},
145 {name: 'CACTUS_SMALL', id: '1x-obstacle-small'},
146 {name: 'CLOUD', id: '1x-cloud'},
147 {name: 'HORIZON', id: '1x-horizon'},
148 {name: 'RESTART', id: '1x-restart'},
149 {name: 'TEXT_SPRITE', id: '1x-text'},
150 {name: 'TREX', id: '1x-trex'}
151 ],
152 HDPI: [
153 {name: 'CACTUS_LARGE', id: '2x-obstacle-large'},
154 {name: 'CACTUS_SMALL', id: '2x-obstacle-small'},
155 {name: 'CLOUD', id: '2x-cloud'},
156 {name: 'HORIZON', id: '2x-horizon'},
157 {name: 'RESTART', id: '2x-restart'},
158 {name: 'TEXT_SPRITE', id: '2x-text'},
159 {name: 'TREX', id: '2x-trex'}
160 ]
161 };
162
163
164 /**
165 * Sound FX. Reference to the ID of the audio tag on interstitial page.
166 * @enum {string}
167 */
168 Runner.sounds = {
169 BUTTON_PRESS: 'offline-sound-press',
170 HIT: 'offline-sound-hit',
171 SCORE: 'offline-sound-reached'
172 };
173
174
175 /**
176 * Key code mapping.
177 * @enum {object}
178 */
179 Runner.keycodes = {
180 JUMP: {'38': 1, '32': 1}, // Up, spacebar
181 DUCK: {'40': 1}, // Down
182 RESTART: {'13': 1} // Enter
183 };
184
185
186 /**
187 * Runner event names.
188 * @enum {string}
189 */
190 Runner.events = {
191 ANIM_END: 'webkitAnimationEnd',
192 CLICK: 'click',
193 KEYDOWN: 'keydown',
194 KEYUP: 'keyup',
195 MOUSEDOWN: 'mousedown',
196 MOUSEUP: 'mouseup',
197 RESIZE: 'resize',
198 TOUCHEND: 'touchend',
199 TOUCHSTART: 'touchstart',
200 VISIBILITY: 'visibilitychange',
201 BLUR: 'blur',
202 FOCUS: 'focus',
203 LOAD: 'load'
204 };
205
206
207 Runner.prototype = {
208 /**
209 * Setting individual settings for debugging.
210 * @param {string} setting
211 * @param {*} value
212 */
213 updateConfigSetting: function(setting, value) {
214 if (setting in this.config && value != undefined) {
215 this.config[setting] = value;
216
217 switch (setting) {
218 case 'GRAVITY':
219 case 'MIN_JUMP_HEIGHT':
220 case 'SPEED_DROP_COEFFICIENT':
221 this.tRex.config[setting] = value;
222 break;
223 case 'INITIAL_JUMP_VELOCITY':
224 this.tRex.setJumpVelocity(value);
225 break;
226 case 'SPEED':
227 this.setSpeed(value);
228 break;
229 }
230 }
231 },
232
233 /**
234 * Load and cache the image assets from the page.
235 */
236 loadImages: function() {
237 var imageSources = IS_HIDPI ? Runner.imageSources.HDPI :
238 Runner.imageSources.LDPI;
239
240 var numImages = imageSources.length;
241
242 for (var i = numImages - 1; i >= 0; i--) {
243 var imgSource = imageSources[i];
244 this.images[imgSource.name] = document.getElementById(imgSource.id);
245 }
246 this.init();
247 },
248
249 /**
250 * Load and decode base 64 encoded sounds.
251 */
252 loadSounds: function() {
253 var resourceTemplate =
254 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
255
256 for (var sound in Runner.sounds) {
257 var soundSrc = resourceTemplate.getElementById(Runner.sounds[sound]).src;
258 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
259 var buffer = decodeBase64ToArrayBuffer(soundSrc);
260
261 // Async, so no guarantee of order in array.
262 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
263 this.soundFx[index] = audioData;
264 }.bind(this, sound));
265 }
266 },
267
268 /**
269 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
270 * @param {number} opt_speed
271 */
272 setSpeed: function(opt_speed) {
273 var speed = opt_speed || this.currentSpeed;
274
275 // Reduce the speed on smaller mobile screens.
276 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
277 var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
278 this.config.MOBILE_SPEED_COEFFICIENT;
279 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
280 } else if (opt_speed) {
281 this.currentSpeed = opt_speed;
282 }
283 },
284
285 /**
286 * Game initialiser.
287 */
288 init: function() {
289 // Hide the static icon.
290 document.querySelector('.' + Runner.classes.ICON).style.visibility =
291 'hidden';
292
293 this.adjustDimensions();
294 this.setSpeed();
295
296 this.containerEl = document.createElement('div');
297 this.containerEl.className = Runner.classes.CONTAINER;
298
299 // Player canvas container.
300 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
301 this.dimensions.HEIGHT, Runner.classes.PLAYER);
302
303 this.canvasCtx = this.canvas.getContext('2d');
304 this.canvasCtx.fillStyle = '#f7f7f7';
305 this.canvasCtx.fill();
306 Runner.updateCanvasScaling(this.canvas);
307
308 // Horizon contains clouds, obstacles and the ground.
309 this.horizon = new Horizon(this.canvas, this.images, this.dimensions,
310 this.config.GAP_COEFFICIENT);
311
312 // Distance meter
313 this.distanceMeter = new DistanceMeter(this.canvas,
314 this.images.TEXT_SPRITE, this.dimensions.WIDTH);
315
316 // Draw t-rex
317 this.tRex = new Trex(this.canvas, this.images.TREX);
318
319 this.outerContainerEl.appendChild(this.containerEl);
320
321 if (IS_MOBILE) {
322 this.createTouchController();
323 }
324
325 this.startListening();
326 this.update();
327 this.loadSounds();
328
329 window.addEventListener(Runner.events.RESIZE,
330 this.debounceResize.bind(this), false);
331 },
332
333 /**
334 * Create the touch controller. A div that covers whole screen.
335 */
336 createTouchController: function() {
337 this.touchController = document.createElement('div');
338 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
339 },
340
341 /**
342 * Debounce the resize event.
343 */
344 debounceResize: function() {
345 if (!this.resizeTimerId_) {
346 this.resizeTimerId_ =
347 setInterval(this.adjustDimensions.bind(this), 250);
348 }
349 },
350
351 /**
352 * Adjust game space dimensions on resize.
353 */
354 adjustDimensions: function() {
355 clearInterval(this.resizeTimerId_);
356 this.resizeTimerId_ = null;
357
358 var boxStyles = window.getComputedStyle(this.outerContainerEl);
359 var padding = Number(boxStyles.paddingLeft.substr(0,
360 boxStyles.paddingLeft.length - 2));
361
362 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
363
364 // Redraw the elements back onto the canvas.
365 if (this.canvas) {
366 this.canvas.width = this.dimensions.WIDTH;
367 this.canvas.height = this.dimensions.HEIGHT;
368
369 Runner.updateCanvasScaling(this.canvas);
370
371 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
372 this.clearCanvas();
373 this.horizon.update(0, 0, true);
374 this.tRex.update(0);
375
376 // Outer container and distance meter.
377 if (this.activated || this.crashed) {
378 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
379 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
380 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
381 this.stop();
382 } else {
383 this.tRex.draw(0, 0);
384 }
385
386 // Game over panel.
387 if (this.crashed && this.gameOverPanel) {
388 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
389 this.gameOverPanel.draw();
390 }
391 }
392 },
393
394 /**
395 * Play the game intro.
396 * Canvas container width expands out to the full width.
397 */
398 playIntro: function() {
399 if (!this.started && !this.crashed) {
400 this.playingIntro = true;
401 this.tRex.playingIntro = true;
402
403 // CSS animation definition.
404 var keyframes = '@-webkit-keyframes intro { ' +
405 'from { width:' + Trex.config.WIDTH + 'px }' +
406 'to { width: ' + this.dimensions.WIDTH + 'px }' +
407 '}';
408 document.styleSheets[0].insertRule(keyframes, 0);
409
410 this.containerEl.addEventListener(Runner.events.ANIM_END,
411 this.startGame.bind(this));
412
413 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
414 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
415
416 if (this.touchController) {
417 this.outerContainerEl.appendChild(this.touchController);
418 }
419 this.activated = true;
420 this.started = true;
421 } else if (this.crashed) {
422 this.restart();
423 }
424 },
425
426
427 /**
428 * Update the game status to started.
429 */
430 startGame: function() {
431 this.runningTime = 0;
432 this.playingIntro = false;
433 this.tRex.playingIntro = false;
434 this.containerEl.style.webkitAnimation = '';
435 this.playCount++;
436
437 // Handle tabbing off the page. Pause the current game.
438 window.addEventListener(Runner.events.VISIBILITY,
439 this.onVisibilityChange.bind(this), false);
440
441 window.addEventListener(Runner.events.BLUR,
442 this.onVisibilityChange.bind(this), false);
443
444 window.addEventListener(Runner.events.FOCUS,
445 this.onVisibilityChange.bind(this), false);
446 },
447
448 clearCanvas: function() {
449 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
450 this.dimensions.HEIGHT);
451 },
452
453 /**
454 * Update the game frame.
455 */
456 update: function() {
457 this.drawPending = false;
458
459 var now = performance.now();
460 var deltaTime = now - (this.time || now);
461 this.time = now;
462
463 if (this.activated) {
464 this.clearCanvas();
465
466 if (this.tRex.jumping) {
467 this.tRex.updateJump(deltaTime, this.config);
468 }
469
470 this.runningTime += deltaTime;
471 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
472
473 // First jump triggers the intro.
474 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
475 this.playIntro();
476 }
477
478 // The horizon doesn't move until the intro is over.
479 if (this.playingIntro) {
480 this.horizon.update(0, this.currentSpeed, hasObstacles);
481 } else {
482 deltaTime = !this.started ? 0 : deltaTime;
483 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
484 }
485
486 // Check for collisions.
487 var collision = hasObstacles &&
488 checkForCollision(this.horizon.obstacles[0], this.tRex);
489
490 if (!collision) {
491 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
492
493 if (this.currentSpeed < this.config.MAX_SPEED) {
494 this.currentSpeed += this.config.ACCELERATION;
495 }
496 } else {
497 this.gameOver();
498 }
499
500 if (this.distanceMeter.getActualDistance(this.distanceRan) >
501 this.distanceMeter.maxScore) {
502 this.distanceRan = 0;
503 }
504
505 var playAcheivementSound = this.distanceMeter.update(deltaTime,
506 Math.ceil(this.distanceRan));
507
508 if (playAcheivementSound) {
509 this.playSound(this.soundFx.SCORE);
510 }
511 }
512
513 if (!this.crashed) {
514 this.tRex.update(deltaTime);
515 this.raq();
516 }
517 },
518
519 /**
520 * Event handler.
521 */
522 handleEvent: function(e) {
523 return (function(evtType, events) {
524 switch (evtType) {
525 case events.KEYDOWN:
526 case events.TOUCHSTART:
527 case events.MOUSEDOWN:
528 this.onKeyDown(e);
529 break;
530 case events.KEYUP:
531 case events.TOUCHEND:
532 case events.MOUSEUP:
533 this.onKeyUp(e);
534 break;
535 }
536 }.bind(this))(e.type, Runner.events);
537 },
538
539 /**
540 * Bind relevant key / mouse / touch listeners.
541 */
542 startListening: function() {
543 // Keys.
544 document.addEventListener(Runner.events.KEYDOWN, this);
545 document.addEventListener(Runner.events.KEYUP, this);
546
547 if (IS_MOBILE) {
548 // Mobile only touch devices.
549 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
550 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
551 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
552 } else {
553 // Mouse.
554 document.addEventListener(Runner.events.MOUSEDOWN, this);
555 document.addEventListener(Runner.events.MOUSEUP, this);
556 }
557 },
558
559 /**
560 * Remove all listeners.
561 */
562 stopListening: function() {
563 document.removeEventListener(Runner.events.KEYDOWN, this);
564 document.removeEventListener(Runner.events.KEYUP, this);
565
566 if (IS_MOBILE) {
567 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
568 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
569 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
570 } else {
571 document.removeEventListener(Runner.events.MOUSEDOWN, this);
572 document.removeEventListener(Runner.events.MOUSEUP, this);
573 }
574 },
575
576 /**
577 * Process keydown.
578 * @param {Event} e
579 */
580 onKeyDown: function(e) {
581 if (!this.crashed && (Runner.keycodes.JUMP[String(e.keyCode)] ||
582 e.type == Runner.events.TOUCHSTART)) {
583 if (!this.activated) {
584 this.activated = true;
585 }
586
587 if (!this.tRex.jumping) {
588 this.playSound(this.soundFx.BUTTON_PRESS);
589 this.tRex.startJump();
590 }
591 }
592
593 if (this.crashed && e.type == Runner.events.TOUCHSTART &&
594 e.currentTarget == this.containerEl) {
595 this.restart();
596 }
597
598 // Speed drop, activated only when jump key is not pressed.
599 if (Runner.keycodes.DUCK[e.keyCode] && this.tRex.jumping) {
600 e.preventDefault();
601 this.tRex.setSpeedDrop();
602 }
603 },
604
605
606 /**
607 * Process key up.
608 * @param {Event} e
609 */
610 onKeyUp: function(e) {
611 var keyCode = String(e.keyCode);
612 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
613 e.type == Runner.events.TOUCHEND ||
614 e.type == Runner.events.MOUSEDOWN;
615
616 if (this.isRunning() && isjumpKey) {
617 this.tRex.endJump();
618 } else if (Runner.keycodes.DUCK[keyCode]) {
619 this.tRex.speedDrop = false;
620 } else if (this.crashed) {
621 // Check that enough time has elapsed before allowing jump key to restart.
622 var deltaTime = performance.now() - this.time;
623
624 if (Runner.keycodes.RESTART[keyCode] ||
625 (e.type == Runner.events.MOUSEUP && e.target == this.canvas) ||
626 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
627 Runner.keycodes.JUMP[keyCode])) {
628 this.restart();
629 }
630 } else if (this.paused && isjumpKey) {
631 this.play();
632 }
633 },
634
635 /**
636 * RequestAnimationFrame wrapper.
637 */
638 raq: function() {
639 if (!this.drawPending) {
640 this.drawPending = true;
641 this.raqId = requestAnimationFrame(this.update.bind(this));
642 }
643 },
644
645 /**
646 * Whether the game is running.
647 * @return {boolean}
648 */
649 isRunning: function() {
650 return !!this.raqId;
651 },
652
653 /**
654 * Game over state.
655 */
656 gameOver: function() {
657 this.playSound(this.soundFx.HIT);
658 vibrate(200);
659
660 this.stop();
661 this.crashed = true;
662 this.distanceMeter.acheivement = false;
663
664 this.tRex.update(100, Trex.status.CRASHED);
665
666 // Game over panel.
667 if (!this.gameOverPanel) {
668 this.gameOverPanel = new GameOverPanel(this.canvas,
669 this.images.TEXT_SPRITE, this.images.RESTART,
670 this.dimensions);
671 } else {
672 this.gameOverPanel.draw();
673 }
674
675 // Update the high score.
676 if (this.distanceRan > this.highestScore) {
677 this.highestScore = Math.ceil(this.distanceRan);
678 this.distanceMeter.setHighScore(this.highestScore);
679 }
680
681 // Reset the time clock.
682 this.time = performance.now();
683 },
684
685 stop: function() {
686 this.activated = false;
687 this.paused = true;
688 cancelAnimationFrame(this.raqId);
689 this.raqId = 0;
690 },
691
692 play: function() {
693 if (!this.crashed) {
694 this.activated = true;
695 this.paused = false;
696 this.tRex.update(0, Trex.status.RUNNING);
697 this.time = performance.now();
698 this.update();
699 }
700 },
701
702 restart: function() {
703 if (!this.raqId) {
704 this.playCount++;
705 this.runningTime = 0;
706 this.activated = true;
707 this.crashed = false;
708 this.distanceRan = 0;
709 this.setSpeed(this.config.SPEED);
710
711 this.time = performance.now();
712 this.containerEl.classList.remove(Runner.classes.CRASHED);
713 this.clearCanvas();
714 this.distanceMeter.reset(this.highestScore);
715 this.horizon.reset();
716 this.tRex.reset();
717 this.playSound(this.soundFx.BUTTON_PRESS);
718
719 this.update();
720 }
721 },
722
723 /**
724 * Pause the game if the tab is not in focus.
725 */
726 onVisibilityChange: function(e) {
727 if (document.hidden || document.webkitHidden || e.type == 'blur') {
728 this.stop();
729 } else {
730 this.play();
731 }
732 },
733
734 /**
735 * Play a sound.
736 * @param {SoundBuffer} soundBuffer
737 */
738 playSound: function(soundBuffer) {
739 if (soundBuffer) {
740 var sourceNode = this.audioContext.createBufferSource();
741 sourceNode.buffer = soundBuffer;
742 sourceNode.connect(this.audioContext.destination);
743 sourceNode.start(0);
744 }
745 }
746 };
747
748
749 /**
750 * Updates the canvas size taking into
751 * account the backing store pixel ratio and
752 * the device pixel ratio.
753 *
754 * See article by Paul Lewis:
755 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
756 *
757 * @param {HTMLCanvasElement} canvas
758 * @param {number} opt_width
759 * @param {number} opt_height
760 * @return {boolean} Whether the canvas was scaled.
761 */
762 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
763 var context = canvas.getContext('2d');
764
765 // Query the various pixel ratios
766 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
767 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
768 var ratio = devicePixelRatio / backingStoreRatio;
769
770 // Upscale the canvas if the two ratios don't match
771 if (devicePixelRatio !== backingStoreRatio) {
772
773 var oldWidth = opt_width || canvas.width;
774 var oldHeight = opt_height || canvas.height;
775
776 canvas.width = oldWidth * ratio;
777 canvas.height = oldHeight * ratio;
778
779 canvas.style.width = oldWidth + 'px';
780 canvas.style.height = oldHeight + 'px';
781
782 // Scale the context to counter the fact that we've manually scaled
783 // our canvas element.
784 context.scale(ratio, ratio);
785 return true;
786 }
787 return false;
788 };
789
790
791 /**
792 * Get random number.
793 * @param {number} min
794 * @param {number} max
795 * @param {number}
796 */
797 function getRandomNum(min, max) {
798 return Math.floor(Math.random() * (max - min + 1)) + min;
799 }
800
801
802 /**
803 * Vibrate on mobile devices.
804 * @param {number} duration Duration of the vibration in milliseconds.
805 */
806 function vibrate(duration) {
807 if (IS_MOBILE) {
808 window.navigator['vibrate'](duration);
809 }
810 }
811
812
813 /**
814 * Create canvas element.
815 * @param {HTMLElement} container Element to append canvas to.
816 * @param {number} width
817 * @param {number} height
818 * @param {string} opt_classname
819 * @return {HTMLCanvasElement}
820 */
821 function createCanvas(container, width, height, opt_classname) {
822 var canvas = document.createElement('canvas');
823 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
824 opt_classname : Runner.classes.CANVAS;
825 canvas.width = width;
826 canvas.height = height;
827 container.appendChild(canvas);
828
829 return canvas;
830 }
831
832
833 /**
834 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
835 * @param {string} base64String
836 */
837 function decodeBase64ToArrayBuffer(base64String) {
838 var len = (base64String.length / 4) * 3;
839 var str = atob(base64String);
840 var arrayBuffer = new ArrayBuffer(len);
841 var bytes = new Uint8Array(arrayBuffer);
842
843 for (var i = 0; i < len; i++) {
844 bytes[i] = str.charCodeAt(i);
845 }
846 return bytes.buffer;
847 }
848
849
850 //******************************************************************************
851
852
853 /**
854 * Game over panel.
855 * @param {!HTMLCanvasElement} canvas
856 * @param {!HTMLImage} textSprite
857 * @param {!HTMLImage} restartImg
858 * @param {!Object} dimensions Canvas dimensions.
859 * @constructor
860 */
861 function GameOverPanel(canvas, textSprite, restartImg, dimensions) {
862 this.canvas = canvas;
863 this.canvasCtx = canvas.getContext('2d');
864 this.canvasDimensions = dimensions;
865 this.textSprite = textSprite;
866 this.restartImg = restartImg;
867 this.draw();
868 };
869
870
871 /**
872 * Dimensions used in the panel.
873 * @enum {number}
874 */
875 GameOverPanel.dimensions = {
876 TEXT_X: 0,
877 TEXT_Y: 13,
878 TEXT_WIDTH: 191,
879 TEXT_HEIGHT: 11,
880 RESTART_WIDTH: 36,
881 RESTART_HEIGHT: 32
882 };
883
884
885 GameOverPanel.prototype = {
886 /**
887 * Update the panel dimensions.
888 * @param {number} width New canvas width.
889 * @param {number} opt_height Optional new canvas height.
890 */
891 updateDimensions: function(width, opt_height) {
892 this.canvasDimensions.WIDTH = width;
893 if (opt_height) {
894 this.canvasDimensions.HEIGHT = opt_height;
895 }
896 },
897
898 /**
899 * Draw the panel.
900 */
901 draw: function() {
902 var dimensions = GameOverPanel.dimensions;
903
904 var centerX = this.canvasDimensions.WIDTH / 2;
905
906 // Game over text.
907 var textSourceX = dimensions.TEXT_X;
908 var textSourceY = dimensions.TEXT_Y;
909 var textSourceWidth = dimensions.TEXT_WIDTH;
910 var textSourceHeight = dimensions.TEXT_HEIGHT;
911
912 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
913 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
914 var textTargetWidth = dimensions.TEXT_WIDTH;
915 var textTargetHeight = dimensions.TEXT_HEIGHT;
916
917 var restartSourceWidth = dimensions.RESTART_WIDTH;
918 var restartSourceHeight = dimensions.RESTART_HEIGHT;
919 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
920 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
921
922 if (IS_HIDPI) {
923 textSourceY *= 2;
924 textSourceX *= 2;
925 textSourceWidth *= 2;
926 textSourceHeight *= 2;
927 restartSourceWidth *= 2;
928 restartSourceHeight *= 2;
929 }
930
931 // Game over text from sprite.
932 this.canvasCtx.drawImage(this.textSprite,
933 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
934 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
935
936 // Restart button.
937 this.canvasCtx.drawImage(this.restartImg, 0, 0,
938 restartSourceWidth, restartSourceHeight,
939 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
940 dimensions.RESTART_HEIGHT);
941 }
942 };
943
944
945 //******************************************************************************
946
947 /**
948 * Check for a collision.
949 * @param {!Obstacle} obstacle
950 * @param {!Trex} tRex T-rex object.
951 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
952 * collision boxes.
953 * @return {Array.<CollisionBox>}
954 */
955 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
956 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
957
958 // Adjustments are made to the bounding box as there is a 1 pixel white
959 // border around the t-rex and obstacles.
960 var tRexBox = new CollisionBox(
961 tRex.xPos + 1,
962 tRex.yPos + 1,
963 tRex.config.WIDTH - 2,
964 tRex.config.HEIGHT - 2);
965
966 var obstacleBox = new CollisionBox(
967 obstacle.xPos + 1,
968 obstacle.yPos + 1,
969 obstacle.typeConfig.width * obstacle.size - 2,
970 obstacle.typeConfig.height - 2);
971
972 // Debug outer box
973 if (opt_canvasCtx) {
974 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
975 }
976
977 // Simple outer bounds check.
978 if (boxCompare(tRexBox, obstacleBox)) {
979 var collisionBoxes = obstacle.collisionBoxes;
980 var tRexCollisionBoxes = Trex.collisionBoxes;
981
982 // Detailed axis aligned box check.
983 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
984 for (var i = 0; i < collisionBoxes.length; i++) {
985 // Adjust the box to actual positions.
986 var adjTrexBox =
987 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
988 var adjObstacleBox =
989 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
990 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
991
992 // Draw boxes for debug.
993 if (opt_canvasCtx) {
994 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
995 }
996
997 if (crashed) {
998 return [adjTrexBox, adjObstacleBox];
999 }
1000 }
1001 }
1002 }
1003 return false;
1004 };
1005
1006
1007 /**
1008 * Adjust the collision box.
1009 * @param {!CollisionBox} box The original box.
1010 * @param {!CollisionBox} adjustment Adjustment box.
1011 * @return {CollisionBox} The adjusted collision box object.
1012 */
1013 function createAdjustedCollisionBox(box, adjustment) {
1014 return new CollisionBox(
1015 box.x + adjustment.x,
1016 box.y + adjustment.y,
1017 box.width,
1018 box.height);
1019 };
1020
1021
1022 /**
1023 * Draw the collision boxes for debug.
1024 */
1025 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1026 canvasCtx.save();
1027 canvasCtx.strokeStyle = '#f00';
1028 canvasCtx.strokeRect(tRexBox.x, tRexBox.y,
1029 tRexBox.width, tRexBox.height);
1030
1031 canvasCtx.strokeStyle = '#0f0';
1032 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1033 obstacleBox.width, obstacleBox.height);
1034 canvasCtx.restore();
1035 };
1036
1037
1038 /**
1039 * Compare two collision boxes for a collision.
1040 * @param {CollisionBox} tRexBox
1041 * @param {CollisionBox} obstacleBox
1042 * @return {boolean} Whether the boxes intersected.
1043 */
1044 function boxCompare(tRexBox, obstacleBox) {
1045 var crashed = false;
1046 var tRexBoxX = tRexBox.x;
1047 var tRexBoxY = tRexBox.y;
1048
1049 var obstacleBoxX = obstacleBox.x;
1050 var obstacleBoxY = obstacleBox.y;
1051
1052 // Axis-Aligned Bounding Box method.
1053 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1054 tRexBox.x + tRexBox.width > obstacleBoxX &&
1055 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1056 tRexBox.height + tRexBox.y > obstacleBox.y) {
1057 crashed = true;
1058 }
1059
1060 return crashed;
1061 };
1062
1063
1064 //******************************************************************************
1065
1066 /**
1067 * Collision box object.
1068 * @param {number} x X position.
1069 * @param {number} y Y Position.
1070 * @param {number} w Width.
1071 * @param {number} h Height.
1072 */
1073 function CollisionBox(x, y, w, h) {
1074 this.x = x;
1075 this.y = y;
1076 this.width = w;
1077 this.height = h;
1078 };
1079
1080
1081 //******************************************************************************
1082
1083 /**
1084 * Obstacle.
1085 * @param {HTMLCanvasCtx} canvasCtx
1086 * @param {Obstacle.type} type
1087 * @param {image} obstacleImg Image sprite.
1088 * @param {Object} dimensions
1089 * @param {number} gapCoefficient Mutipler in determining the gap.
1090 * @param {number} speed
1091 */
1092 function Obstacle(canvasCtx, type, obstacleImg, dimensions,
1093 gapCoefficient, speed) {
1094
1095 this.canvasCtx = canvasCtx;
1096 this.image = obstacleImg;
1097 this.typeConfig = type;
1098 this.gapCoefficient = gapCoefficient;
1099 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1100 this.dimensions = dimensions;
1101 this.remove = false;
1102 this.xPos = 0;
1103 this.yPos = this.typeConfig.yPos;
1104 this.width = 0;
1105 this.collisionBoxes = [];
1106 this.gap = 0;
1107
1108 this.init(speed);
1109 };
1110
1111 /**
1112 * Coefficient for calculating the maximum gap.
1113 * @const
1114 */
1115 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1116
1117 /**
1118 * Maximum obstacle grouping count.
1119 * @const
1120 */
1121 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1122
1123
1124 Obstacle.prototype = {
1125 /**
1126 * Initialise the DOM for the obstacle.
1127 * @param {number} speed
1128 */
1129 init: function(speed) {
1130 this.cloneCollisionBoxes();
1131
1132 // Only allow sizing if we're at the right speed.
1133 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1134 this.size = 1;
1135 }
1136
1137 this.width = this.typeConfig.width * this.size;
1138 this.xPos = this.dimensions.WIDTH - this.width;
1139
1140 this.draw();
1141
1142 // Make collision box adjustments,
1143 // Central box is adjusted to the size as one box.
1144 // ____ ______ ________
1145 // _| |-| _| |-| _| |-|
1146 // | |<->| | | |<--->| | | |<----->| |
1147 // | | 1 | | | | 2 | | | | 3 | |
1148 // |_|___|_| |_|_____|_| |_|_______|_|
1149 //
1150 if (this.size > 1) {
1151 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1152 this.collisionBoxes[2].width;
1153 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1154 }
1155
1156 this.gap = this.getGap(this.gapCoefficient, speed);
1157 },
1158
1159 /**
1160 * Draw and crop based on size.
1161 */
1162 draw: function() {
1163 var sourceWidth = this.typeConfig.width;
1164 var sourceHeight = this.typeConfig.height;
1165
1166 if (IS_HIDPI) {
1167 sourceWidth = sourceWidth * 2;
1168 sourceHeight = sourceHeight * 2;
1169 }
1170
1171 // Sprite
1172 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1));
1173 this.canvasCtx.drawImage(this.image,
1174 sourceX, 0,
1175 sourceWidth * this.size, sourceHeight,
1176 this.xPos, this.yPos,
1177 this.typeConfig.width * this.size, this.typeConfig.height);
1178 },
1179
1180 /**
1181 * Obstacle frame update.
1182 * @param {number} deltaTime
1183 * @param {number} speed
1184 */
1185 update: function(deltaTime, speed) {
1186 if (!this.remove) {
1187 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1188 this.draw();
1189
1190 if (!this.isVisible()) {
1191 this.remove = true;
1192 }
1193 }
1194 },
1195
1196 /**
1197 * Calculate a random gap size.
1198 * - Minimum gap gets wider as speed increses
1199 * @param {number} gapCoefficient
1200 * @param {number} speed
1201 * @return {number} The gap size.
1202 */
1203 getGap: function(gapCoefficient, speed) {
1204 var minGap = Math.round(this.width * speed +
1205 this.typeConfig.minGap * gapCoefficient);
1206 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1207 return getRandomNum(minGap, maxGap);
1208 },
1209
1210 /**
1211 * Check if obstacle is visible.
1212 * @return {boolean} Whether the obstacle is in the game area.
1213 */
1214 isVisible: function() {
1215 return this.xPos + this.width > 0;
1216 },
1217
1218 /**
1219 * Make a copy of the collision boxes, since these will change based on
1220 * obstacle type and size.
1221 */
1222 cloneCollisionBoxes: function() {
1223 var collisionBoxes = this.typeConfig.collisionBoxes;
1224
1225 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1226 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1227 collisionBoxes[i].y, collisionBoxes[i].width,
1228 collisionBoxes[i].height);
1229 }
1230 }
1231 };
1232
1233
1234 /**
1235 * Obstacle definitions.
1236 * minGap: minimum pixel space betweeen obstacles.
1237 * multipleSpeed: Speed at which multiples are allowed.
1238 */
1239 Obstacle.types = [
1240 {
1241 type: 'CACTUS_SMALL',
1242 className: ' cactus cactus-small ',
1243 width: 17,
1244 height: 35,
1245 yPos: 105,
1246 multipleSpeed: 3,
1247 minGap: 120,
1248 collisionBoxes: [
1249 new CollisionBox(0, 7, 5, 27),
1250 new CollisionBox(4, 0, 6, 34),
1251 new CollisionBox(10, 4, 7, 14)
1252 ]
1253 },
1254 {
1255 type: 'CACTUS_LARGE',
1256 className: ' cactus cactus-large ',
1257 width: 25,
1258 height: 50,
1259 yPos: 90,
1260 multipleSpeed: 6,
1261 minGap: 120,
1262 collisionBoxes: [
1263 new CollisionBox(0, 12, 7, 38),
1264 new CollisionBox(8, 0, 7, 49),
1265 new CollisionBox(13, 10, 10, 38)
1266 ]
1267 }
1268 ];
1269
1270
1271 //******************************************************************************
1272 /**
1273 * T-rex game character.
1274 * @param {HTMLCanvas} canvas
1275 * @param {HTMLImage} image Character image.
1276 * @constructor
1277 */
1278 function Trex(canvas, image) {
1279 this.canvas = canvas;
1280 this.canvasCtx = canvas.getContext('2d');
1281 this.image = image;
1282 this.xPos = 0;
1283 this.yPos = 0;
1284 // Position when on the ground.
1285 this.groundYPos = 0;
1286 this.currentFrame = 0;
1287 this.currentAnimFrames = [];
1288 this.blinkDelay = 0;
1289 this.animStartTime = 0;
1290 this.timer = 0;
1291 this.msPerFrame = 1000 / FPS;
1292 this.config = Trex.config;
1293 // Current status.
1294 this.status = Trex.status.WAITING;
1295
1296 this.jumping = false;
1297 this.jumpVelocity = 0;
1298 this.reachedMinHeight = false;
1299 this.speedDrop = false;
1300 this.jumpCount = 0;
1301 this.jumpspotX = 0;
1302
1303 this.init();
1304 };
1305
1306
1307 /**
1308 * T-rex player config.
1309 * @enum {number}
1310 */
1311 Trex.config = {
1312 DROP_VELOCITY: -5,
1313 GRAVITY: 0.6,
1314 HEIGHT: 47,
1315 INIITAL_JUMP_VELOCITY: -10,
1316 INTRO_DURATION: 1500,
1317 MAX_JUMP_HEIGHT: 30,
1318 MIN_JUMP_HEIGHT: 30,
1319 SPEED_DROP_COEFFICIENT: 3,
1320 SPRITE_WIDTH: 262,
1321 START_X_POS: 50,
1322 WIDTH: 44
1323 };
1324
1325
1326 /**
1327 * Used in collision detection.
1328 * @type {Array.<CollisionBox>}
1329 */
1330 Trex.collisionBoxes = [
1331 new CollisionBox(1, -1, 30, 26),
1332 new CollisionBox(32, 0, 8, 16),
1333 new CollisionBox(10, 35, 14, 8),
1334 new CollisionBox(1, 24, 29, 5),
1335 new CollisionBox(5, 30, 21, 4),
1336 new CollisionBox(9, 34, 15, 4)
1337 ];
1338
1339
1340 /**
1341 * Animation states.
1342 * @enum {string}
1343 */
1344 Trex.status = {
1345 CRASHED: 'CRASHED',
1346 JUMPING: 'JUMPING',
1347 RUNNING: 'RUNNING',
1348 WAITING: 'WAITING'
1349 };
1350
1351 /**
1352 * Blinking coefficient.
1353 * @const
1354 */
1355 Trex.BLINK_TIMING = 7000;
1356
1357
1358 /**
1359 * Animation config for different states.
1360 * @enum {object}
1361 */
1362 Trex.animFrames = {
1363 WAITING: {
1364 frames: [44, 0],
1365 msPerFrame: 1000 / 3
1366 },
1367 RUNNING: {
1368 frames: [88, 132],
1369 msPerFrame: 1000 / 12
1370 },
1371 CRASHED: {
1372 frames: [220],
1373 msPerFrame: 1000 / 60
1374 },
1375 JUMPING: {
1376 frames: [0],
1377 msPerFrame: 1000 / 60
1378 }
1379 };
1380
1381
1382 Trex.prototype = {
1383 /**
1384 * T-rex player initaliser.
1385 * Sets the t-rex to blink at random intervals.
1386 */
1387 init: function() {
1388 this.blinkDelay = this.setBlinkDelay();
1389 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1390 Runner.config.BOTTOM_PAD;
1391 this.yPos = this.groundYPos;
1392 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1393
1394 this.draw(0, 0);
1395 this.update(0, Trex.status.WAITING);
1396 },
1397
1398 /**
1399 * Setter for the jump velocity.
1400 * The approriate drop velocity is also set.
1401 */
1402 setJumpVelocity: function(setting) {
1403 this.config.INIITAL_JUMP_VELOCITY = -setting;
1404 this.config.DROP_VELOCITY = -setting / 2;
1405 },
1406
1407 /**
1408 * Set the animation status.
1409 * @param {!number} deltaTime
1410 * @param {Trex.status} status Optional status to switch to.
1411 */
1412 update: function(deltaTime, opt_status) {
1413 this.timer += deltaTime;
1414
1415 // Update the status.
1416 if (opt_status) {
1417 this.status = opt_status;
1418 this.currentFrame = 0;
1419 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1420 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1421
1422 if (opt_status == Trex.status.WAITING) {
1423 this.animStartTime = performance.now();
1424 this.setBlinkDelay();
1425 }
1426 }
1427
1428 // Game intro animation, T-rex moves in from the left.
1429 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1430 this.xPos += Math.round((this.config.START_X_POS /
1431 this.config.INTRO_DURATION) * deltaTime);
1432 }
1433
1434 if (this.status == Trex.status.WAITING) {
1435 this.blink(performance.now());
1436 } else {
1437 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1438 }
1439
1440 // Update the frame position.
1441 if (this.timer >= this.msPerFrame) {
1442 this.currentFrame = this.currentFrame ==
1443 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1444 this.timer = 0;
1445 }
1446 },
1447
1448 /**
1449 * Draw the t-rex to a particular position.
1450 * @param {number} x
1451 * @param {number} y
1452 */
1453 draw: function(x, y) {
1454 var sourceX = x;
1455 var sourceY = y;
1456 var sourceWidth = this.config.WIDTH;
1457 var sourceHeight = this.config.HEIGHT;
1458
1459 if (IS_HIDPI) {
1460 sourceX *= 2;
1461 sourceY *= 2;
1462 sourceWidth *= 2;
1463 sourceHeight *= 2;
1464 }
1465
1466 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1467 sourceWidth, sourceHeight,
1468 this.xPos, this.yPos,
1469 this.config.WIDTH, this.config.HEIGHT);
1470 },
1471
1472 /**
1473 * Sets a random time for the blink to happen.
1474 */
1475 setBlinkDelay: function() {
1476 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1477 },
1478
1479 /**
1480 * Make t-rex blink at random intervals.
1481 * @param {number} time Current time in milliseconds.
1482 */
1483 blink: function(time) {
1484 var deltaTime = time - this.animStartTime;
1485
1486 if (deltaTime >= this.blinkDelay) {
1487 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1488
1489 if (this.currentFrame == 1) {
1490 // Set new random delay to blink.
1491 this.setBlinkDelay();
1492 this.animStartTime = time;
1493 }
1494 }
1495 },
1496
1497 /**
1498 * Initialise a jump.
1499 */
1500 startJump: function() {
1501 if (!this.jumping) {
1502 this.update(0, Trex.status.JUMPING);
1503 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY;
1504 this.jumping = true;
1505 this.reachedMinHeight = false;
1506 this.speedDrop = false;
1507 }
1508 },
1509
1510 /**
1511 * Jump is complete, falling down.
1512 */
1513 endJump: function() {
1514 if (this.reachedMinHeight &&
1515 this.jumpVelocity < this.config.DROP_VELOCITY) {
1516 this.jumpVelocity = this.config.DROP_VELOCITY;
1517 }
1518 },
1519
1520 /**
1521 * Update frame for a jump.
1522 * @param {number} deltaTime
1523 */
1524 updateJump: function(deltaTime) {
1525 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1526 var framesElapsed = deltaTime / msPerFrame;
1527
1528 // Speed drop makes Trex fall faster.
1529 if (this.speedDrop) {
1530 this.yPos += Math.round(this.jumpVelocity *
1531 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1532 } else {
1533 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1534 }
1535
1536 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1537
1538 // Minimum height has been reached.
1539 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1540 this.reachedMinHeight = true;
1541 }
1542
1543 // Reached max height
1544 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1545 this.endJump();
1546 }
1547
1548 // Back down at ground level. Jump completed.
1549 if (this.yPos > this.groundYPos) {
1550 this.reset();
1551 this.jumpCount++;
1552 }
1553
1554 this.update(deltaTime);
1555 },
1556
1557 /**
1558 * Set the speed drop. Immediately cancels the current jump.
1559 */
1560 setSpeedDrop: function() {
1561 this.speedDrop = true;
1562 this.jumpVelocity = 1;
1563 },
1564
1565 /**
1566 * Reset the t-rex to running at start of game.
1567 */
1568 reset: function() {
1569 this.yPos = this.groundYPos;
1570 this.jumpVelocity = 0;
1571 this.jumping = false;
1572 this.update(0, Trex.status.RUNNING);
1573 this.midair = false;
1574 this.speedDrop = false;
1575 this.jumpCount = 0;
1576 }
1577 };
1578
1579
1580 //******************************************************************************
1581
1582 /**
1583 * Handles displaying the distance meter.
1584 * @param {!HTMLCanvasElement} canvas
1585 * @param {!HTMLImage} spriteSheet Image sprite.
1586 * @param {number} canvasWidth
1587 * @constructor
1588 */
1589 function DistanceMeter(canvas, spriteSheet, canvasWidth) {
1590 this.canvas = canvas;
1591 this.canvasCtx = canvas.getContext('2d');
1592 this.image = spriteSheet;
1593 this.x = 0;
1594 this.y = 5;
1595
1596 this.currentDistance = 0;
1597 this.maxScore = 0;
1598 this.highScore = 0;
1599 this.container = null;
1600
1601 this.digits = [];
1602 this.acheivement = false;
1603 this.defaultString = '';
1604 this.flashTimer = 0;
1605 this.flashIterations = 0;
1606
1607 this.config = DistanceMeter.config;
1608 this.init(canvasWidth);
1609 };
1610
1611
1612 /**
1613 * @enum {number}
1614 */
1615 DistanceMeter.dimensions = {
1616 WIDTH: 10,
1617 HEIGHT: 13,
1618 DEST_WIDTH: 11
1619 };
1620
1621
1622 /**
1623 * Y positioning of the digits in the sprite sheet.
1624 * X position is always 0.
1625 * @type {array.<number>}
1626 */
1627 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1628
1629
1630 /**
1631 * Distance meter config.
1632 * @enum {number}
1633 */
1634 DistanceMeter.config = {
1635 // Number of digits.
1636 MAX_DISTANCE_UNITS: 5,
1637
1638 // Distance that causes achievement animation.
1639 ACHIEVEMENT_DISTANCE: 100,
1640
1641 // Used for conversion from pixel distance to a scaled unit.
1642 COEFFICIENT: 0.025,
1643
1644 // Flash duration in milliseconds.
1645 FLASH_DURATION: 1000 / 4,
1646
1647 // Flash iterations for achievement animation.
1648 FLASH_ITERATIONS: 3
1649 };
1650
1651
1652 DistanceMeter.prototype = {
1653 /**
1654 * Initialise the distance meter to '00000'.
1655 * @param {number} width Canvas width in px.
1656 */
1657 init: function(width) {
1658 var maxDistanceStr = '';
1659
1660 this.calcXPos(width);
1661 this.maxScore = this.config.MAX_DISTANCE_UNITS;
1662 for (var i = 0; i < this.config.MAX_DISTANCE_UNITS; i++) {
1663 this.draw(i, 0);
1664 this.defaultString += '0';
1665 maxDistanceStr += '9';
1666 }
1667
1668 this.maxScore = parseInt(maxDistanceStr);
1669 },
1670
1671 /**
1672 * Calculate the xPos in the canvas.
1673 * @param {number} canvasWidth
1674 */
1675 calcXPos: function(canvasWidth) {
1676 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1677 (this.config.MAX_DISTANCE_UNITS + 1));
1678 },
1679
1680 /**
1681 * Draw a digit to canvas.
1682 * @param {number} digitPos Position of the digit.
1683 * @param {number} value Digit value 0-9.
1684 * @param {boolean} opt_highScore Whether drawing the high score.
1685 */
1686 draw: function(digitPos, value, opt_highScore) {
1687 var sourceWidth = DistanceMeter.dimensions.WIDTH;
1688 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1689 var sourceX = DistanceMeter.dimensions.WIDTH * value;
1690
1691 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1692 var targetY = this.y;
1693 var targetWidth = DistanceMeter.dimensions.WIDTH;
1694 var targetHeight = DistanceMeter.dimensions.HEIGHT;
1695
1696 // For high DPI we 2x source values.
1697 if (IS_HIDPI) {
1698 sourceWidth *= 2;
1699 sourceHeight *= 2;
1700 sourceX *= 2;
1701 }
1702
1703 this.canvasCtx.save();
1704
1705 if (opt_highScore) {
1706 // Left of the current score.
1707 var highScoreX = this.x - (this.config.MAX_DISTANCE_UNITS * 2) *
1708 DistanceMeter.dimensions.WIDTH;
1709 this.canvasCtx.translate(highScoreX, this.y);
1710 } else {
1711 this.canvasCtx.translate(this.x, this.y);
1712 }
1713
1714 this.canvasCtx.drawImage(this.image, sourceX, 0,
1715 sourceWidth, sourceHeight,
1716 targetX, targetY,
1717 targetWidth, targetHeight
1718 );
1719
1720 this.canvasCtx.restore();
1721 },
1722
1723 /**
1724 * Covert pixel distance to a 'real' distance.
1725 * @param {number} distance Pixel distance ran.
1726 * @return {number} The 'real' distance ran.
1727 */
1728 getActualDistance: function(distance) {
1729 return distance ?
1730 Math.round(distance * this.config.COEFFICIENT) : 0;
1731 },
1732
1733 /**
1734 * Update the distance meter.
1735 * @param {number} deltaTime
1736 * @param {number} distance
1737 * @return {boolean} Whether the acheivement sound fx should be played.
1738 */
1739 update: function(deltaTime, distance) {
1740 var paint = true;
1741 var playSound = false;
1742
1743 if (!this.acheivement) {
1744 distance = this.getActualDistance(distance);
1745
1746 if (distance > 0) {
1747 // Acheivement unlocked
1748 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1749 // Flash score and play sound.
1750 this.acheivement = true;
1751 this.flashTimer = 0;
1752 playSound = true;
1753 }
1754
1755 // Create a string representation of the distance with leading 0.
1756 var distanceStr = (this.defaultString +
1757 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1758 this.digits = distanceStr.split('');
1759 } else {
1760 this.digits = this.defaultString.split('');
1761 }
1762 } else {
1763 // Control flashing of the score on reaching acheivement.
1764 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1765 this.flashTimer += deltaTime;
1766
1767 if (this.flashTimer < this.config.FLASH_DURATION) {
1768 paint = false;
1769 } else if (this.flashTimer >
1770 this.config.FLASH_DURATION * 2) {
1771 this.flashTimer = 0;
1772 this.flashIterations++;
1773 }
1774 } else {
1775 this.acheivement = false;
1776 this.flashIterations = 0;
1777 this.flashTimer = 0;
1778 }
1779 }
1780
1781 // Draw the digits if not flashing.
1782 if (paint) {
1783 for (var i = this.digits.length - 1; i >= 0; i--) {
1784 this.draw(i, parseInt(this.digits[i]));
1785 }
1786 }
1787
1788 this.drawHighScore();
1789
1790 return playSound;
1791 },
1792
1793 /**
1794 * Draw the high score.
1795 */
1796 drawHighScore: function() {
1797 this.canvasCtx.save();
1798 this.canvasCtx.globalAlpha = .8;
1799 for (var i = this.highScore.length - 1; i >= 0; i--) {
1800 this.draw(i, parseInt(this.highScore[i], 10), true);
1801 }
1802 this.canvasCtx.restore();
1803 },
1804
1805 /**
1806 * Set the highscore as a array string.
1807 * Position of char in the sprite: H - 10, I - 11.
1808 * @param {number} distance Distance ran in pixels.
1809 */
1810 setHighScore: function(distance) {
1811 distance = this.getActualDistance(distance);
1812 var highScoreStr = (this.defaultString +
1813 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1814
1815 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
1816 },
1817
1818 /**
1819 * Reset the distance meter back to '00000'.
1820 */
1821 reset: function() {
1822 this.update(0);
1823 this.acheivement = false;
1824 }
1825 };
1826
1827
1828 //******************************************************************************
1829
1830 /**
1831 * Cloud background item.
1832 * Similar to an obstacle object but without collision boxes.
1833 * @param {HTMLCanvasElement} canvas Canvas element.
1834 * @param {Image} cloudImg
1835 * @param {number} containerWidth
1836 */
1837 function Cloud(canvas, cloudImg, containerWidth) {
1838 this.canvas = canvas;
1839 this.canvasCtx = this.canvas.getContext('2d');
1840 this.image = cloudImg;
1841 this.containerWidth = containerWidth;
1842 this.xPos = containerWidth;
1843 this.yPos = 0;
1844 this.remove = false;
1845 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
1846 Cloud.config.MAX_CLOUD_GAP);
1847
1848 this.init();
1849 };
1850
1851
1852 /**
1853 * Cloud object config.
1854 * @enum {number}
1855 */
1856 Cloud.config = {
1857 HEIGHT: 13,
1858 MAX_CLOUD_GAP: 400,
1859 MAX_SKY_LEVEL: 30,
1860 MIN_CLOUD_GAP: 100,
1861 MIN_SKY_LEVEL: 71,
1862 WIDTH: 46
1863 };
1864
1865
1866 Cloud.prototype = {
1867 /**
1868 * Initialise the cloud. Sets the Cloud height.
1869 */
1870 init: function() {
1871 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
1872 Cloud.config.MIN_SKY_LEVEL);
1873 this.draw();
1874 },
1875
1876 /**
1877 * Draw the cloud.
1878 */
1879 draw: function() {
1880 this.canvasCtx.save();
1881 var sourceWidth = Cloud.config.WIDTH;
1882 var sourceHeight = Cloud.config.HEIGHT;
1883
1884 if (IS_HIDPI) {
1885 sourceWidth = sourceWidth * 2;
1886 sourceHeight = sourceHeight * 2;
1887 }
1888
1889 this.canvasCtx.drawImage(this.image, 0, 0,
1890 sourceWidth, sourceHeight,
1891 this.xPos, this.yPos,
1892 Cloud.config.WIDTH, Cloud.config.HEIGHT);
1893
1894 this.canvasCtx.restore();
1895 },
1896
1897 /**
1898 * Update the cloud position.
1899 * @param {number} speed
1900 */
1901 update: function(speed) {
1902 if (!this.remove) {
1903 this.xPos -= Math.ceil(speed);
1904 this.draw();
1905
1906 // Mark as removeable if no longer in the canvas.
1907 if (!this.isVisible()) {
1908 this.remove = true;
1909 }
1910 }
1911 },
1912
1913 /**
1914 * Check if the cloud is visible on the stage.
1915 * @return {boolean}
1916 */
1917 isVisible: function() {
1918 return this.xPos + Cloud.config.WIDTH > 0;
1919 }
1920 };
1921
1922
1923 //******************************************************************************
1924
1925 /**
1926 * Horizon Line.
1927 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
1928 * @param {HTMLCanvasElement} canvas
1929 * @param {HTMLImage} bgImg Horizon line sprite.
1930 * @constructor
1931 */
1932 function HorizonLine(canvas, bgImg) {
1933 this.image = bgImg;
1934 this.canvas = canvas;
1935 this.canvasCtx = canvas.getContext('2d');
1936 this.sourceDimensions = {};
1937 this.dimensions = HorizonLine.dimensions;
1938 this.sourceXPos = [0, this.dimensions.WIDTH];
1939 this.xPos = [];
1940 this.yPos = 0;
1941 this.bumpThreshold = 0.5;
1942
1943 this.setSourceDimensions();
1944 this.draw();
1945 };
1946
1947
1948 /**
1949 * Horizon line dimensions.
1950 * @enum {number}
1951 */
1952 HorizonLine.dimensions = {
1953 WIDTH: 600,
1954 HEIGHT: 12,
1955 YPOS: 127
1956 };
1957
1958
1959 HorizonLine.prototype = {
1960 /**
1961 * Set the source dimensions of the horizon line.
1962 */
1963 setSourceDimensions: function() {
1964
1965 for (var dimension in HorizonLine.dimensions) {
1966 if (IS_HIDPI) {
1967 if (dimension != 'YPOS') {
1968 this.sourceDimensions[dimension] =
1969 HorizonLine.dimensions[dimension] * 2;
1970 }
1971 } else {
1972 this.sourceDimensions[dimension] =
1973 HorizonLine.dimensions[dimension];
1974 }
1975 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
1976 }
1977
1978 this.xPos = [0, HorizonLine.dimensions.WIDTH];
1979 this.yPos = HorizonLine.dimensions.YPOS;
1980 },
1981
1982 /**
1983 * Return the crop x position of a type.
1984 */
1985 getRandomType: function() {
1986 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
1987 },
1988
1989 /**
1990 * Draw the horizon line.
1991 */
1992 draw: function() {
1993 this.canvasCtx.drawImage(this.image, this.sourceXPos[0], 0,
1994 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
1995 this.xPos[0], this.yPos,
1996 this.dimensions.WIDTH, this.dimensions.HEIGHT);
1997
1998 this.canvasCtx.drawImage(this.image, this.sourceXPos[1], 0,
1999 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2000 this.xPos[1], this.yPos,
2001 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2002 },
2003
2004 /**
2005 * Update the x position of an indivdual piece of the line.
2006 * @param {number} pos Line position.
2007 * @param {number} increment
2008 */
2009 updateXPos: function(pos, increment) {
2010 var line1 = pos;
2011 var line2 = pos == 0 ? 1 : 0;
2012
2013 this.xPos[line1] -= increment;
2014 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2015
2016 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2017 this.xPos[line1] += this.dimensions.WIDTH * 2;
2018 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2019 this.sourceXPos[line1] = this.getRandomType();
2020 }
2021 },
2022
2023 /**
2024 * Update the horizon line.
2025 * @param {number} deltaTime
2026 * @param {number} speed
2027 */
2028 update: function(deltaTime, speed) {
2029 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2030
2031 if (this.xPos[0] <= 0) {
2032 this.updateXPos(0, increment);
2033 } else {
2034 this.updateXPos(1, increment);
2035 }
2036 this.draw();
2037 },
2038
2039 /**
2040 * Reset horizon to the starting position.
2041 */
2042 reset: function() {
2043 this.xPos[0] = 0;
2044 this.xPos[1] = HorizonLine.dimensions.WIDTH;
2045 }
2046 };
2047
2048
2049 //******************************************************************************
2050
2051 /**
2052 * Horizon background class.
2053 * @param {HTMLCanvasElement} canvas
2054 * @param {Array.<HTMLImageElement>} images
2055 * @param {object} dimensions Canvas dimensions.
2056 * @param {number} gapCoefficient
2057 * @constructor
2058 */
2059 function Horizon(canvas, images, dimensions, gapCoefficient) {
2060 this.canvas = canvas;
2061 this.canvasCtx = this.canvas.getContext('2d');
2062 this.config = Horizon.config;
2063 this.dimensions = dimensions;
2064 this.gapCoefficient = gapCoefficient;
2065 this.obstacles = [];
2066 this.horizonOffsets = [0, 0];
2067 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2068
2069 // Cloud
2070 this.clouds = [];
2071 this.cloudImg = images.CLOUD;
2072 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2073
2074 // Horizon
2075 this.horizonImg = images.HORIZON;
2076 this.horizonLine = null;
2077
2078 // Obstacles
2079 this.obstacleImgs = {
2080 CACTUS_SMALL: images.CACTUS_SMALL,
2081 CACTUS_LARGE: images.CACTUS_LARGE
2082 };
2083
2084 this.init();
2085 };
2086
2087
2088 /**
2089 * Horizon config.
2090 * @enum {number}
2091 */
2092 Horizon.config = {
2093 BG_CLOUD_SPEED: 0.2,
2094 BUMPY_THRESHOLD: .3,
2095 CLOUD_FREQUENCY: .5,
2096 HORIZON_HEIGHT: 16,
2097 MAX_CLOUDS: 6
2098 };
2099
2100
2101 Horizon.prototype = {
2102 /**
2103 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2104 */
2105 init: function() {
2106 this.addCloud();
2107 this.horizonLine = new HorizonLine(this.canvas, this.horizonImg);
2108 },
2109
2110 /**
2111 * @param {number} deltaTime
2112 * @param {number} currentSpeed
2113 * @param {boolean} updateObstacles Used as an override to prevent
2114 * the obstacles from being updated / added. This happens in the
2115 * ease in section.
2116 */
2117 update: function(deltaTime, currentSpeed, updateObstacles) {
2118 this.runningTime += deltaTime;
2119 this.horizonLine.update(deltaTime, currentSpeed);
2120 this.updateClouds(deltaTime, currentSpeed);
2121
2122 if (updateObstacles) {
2123 this.updateObstacles(deltaTime, currentSpeed);
2124 }
2125 },
2126
2127 /**
2128 * Update the cloud positions.
2129 * @param {number} deltaTime
2130 * @param {number} currentSpeed
2131 */
2132 updateClouds: function(deltaTime, speed) {
2133 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2134 var numClouds = this.clouds.length;
2135
2136 if (numClouds) {
2137 for (var i = numClouds - 1; i >= 0; i--) {
2138 this.clouds[i].update(cloudSpeed);
2139 }
2140
2141 var lastCloud = this.clouds[numClouds - 1];
2142
2143 // Check for adding a new cloud.
2144 if (numClouds < this.config.MAX_CLOUDS &&
2145 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2146 this.cloudFrequency > Math.random()) {
2147 this.addCloud();
2148 }
2149
2150 // Remove expired clouds.
2151 this.clouds = this.clouds.filter(function(obj) {
2152 return !obj.remove;
2153 });
2154 }
2155 },
2156
2157 /**
2158 * Update the obstacle positions.
2159 * @param {number} deltaTime
2160 * @param {number} currentSpeed
2161 */
2162 updateObstacles: function(deltaTime, currentSpeed) {
2163 // Obstacles, move to Horizon layer.
2164 var updatedObstacles = this.obstacles.slice(0);
2165
2166 for (var i = 0; i < this.obstacles.length; i++) {
2167 var obstacle = this.obstacles[i];
2168 obstacle.update(deltaTime, currentSpeed);
2169
2170 // Clean up existing obstacles.
2171 if (obstacle.remove) {
2172 updatedObstacles.shift();
2173 }
2174 }
2175 this.obstacles = updatedObstacles;
2176
2177 if (this.obstacles.length > 0) {
2178 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2179
2180 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2181 lastObstacle.isVisible() &&
2182 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2183 this.dimensions.WIDTH) {
2184 this.addNewObstacle(currentSpeed);
2185 lastObstacle.followingObstacleCreated = true;
2186 }
2187 } else {
2188 // Create new obstacles.
2189 this.addNewObstacle(currentSpeed);
2190 }
2191 },
2192
2193 /**
2194 * Add a new obstacle.
2195 * @param {number} currentSpeed
2196 */
2197 addNewObstacle: function(currentSpeed) {
2198 var obstacleTypeIndex =
2199 getRandomNum(0, Obstacle.types.length - 1);
2200 var obstacleType = Obstacle.types[obstacleTypeIndex];
2201 var obstacleImg = this.obstacleImgs[obstacleType.type];
2202
2203 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2204 obstacleImg, this.dimensions, this.gapCoefficient, currentSpeed));
2205 },
2206
2207 /**
2208 * Reset the horizon layer.
2209 * Remove existing obstacles and reposition the horizon line.
2210 */
2211 reset: function() {
2212 this.obstacles = [];
2213 this.horizonLine.reset();
2214 },
2215
2216 /**
2217 * Update the canvas width and scaling.
2218 * @param {number} width Canvas width.
2219 * @param {number} height Canvas height.
2220 */
2221 resize: function(width, height) {
2222 this.canvas.width = width;
2223 this.canvas.height = height;
2224 },
2225
2226 /**
2227 * Add a new cloud to the horizon.
2228 */
2229 addCloud: function() {
2230 this.clouds.push(new Cloud(this.canvas, this.cloudImg,
2231 this.dimensions.WIDTH));
2232 }
2233 };
2234 })();
OLDNEW
« no previous file with comments | « chrome/renderer/resources/neterror.js ('k') | chrome/renderer/resources/sounds/button-press.mp3 » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698