Explicitly add python-numpy dependency to install-build-deps.
[chromium-blink-merge.git] / chrome / renderer / resources / offline.js
blob27f9d80847abfe283f635e5c5c60e2ada204da94
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
13 function Runner(outerContainerId, opt_config) {
14 // Singleton
15 if (Runner.instance_) {
16 return Runner.instance_;
18 Runner.instance_ = this;
20 this.outerContainerEl = document.querySelector(outerContainerId);
21 this.containerEl = null;
22 this.detailsButton = this.outerContainerEl.querySelector('#details-button');
24 this.config = opt_config || Runner.config;
26 this.dimensions = Runner.defaultDimensions;
28 this.canvas = null;
29 this.canvasCtx = null;
31 this.tRex = null;
33 this.distanceMeter = null;
34 this.distanceRan = 0;
36 this.highestScore = 0;
38 this.time = 0;
39 this.runningTime = 0;
40 this.msPerFrame = 1000 / FPS;
41 this.currentSpeed = this.config.SPEED;
43 this.obstacles = [];
45 this.started = false;
46 this.activated = false;
47 this.crashed = false;
48 this.paused = false;
50 this.resizeTimerId_ = null;
52 this.playCount = 0;
54 // Sound FX.
55 this.audioBuffer = null;
56 this.soundFx = {};
58 // Global web audio context for playing sounds.
59 this.audioContext = null;
61 // Images.
62 this.images = {};
63 this.imagesLoaded = 0;
64 this.loadImages();
66 window['Runner'] = Runner;
69 /**
70 * Default game width.
71 * @const
73 var DEFAULT_WIDTH = 600;
75 /**
76 * Frames per second.
77 * @const
79 var FPS = 60;
81 /** @const */
82 var IS_HIDPI = window.devicePixelRatio > 1;
84 /** @const */
85 var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1;
87 /** @const */
88 var IS_TOUCH_ENABLED = 'ontouchstart' in window;
90 /** @const */
91 var IS_IOS = window.navigator.userAgent.indexOf('CriOS') > -1;
93 /**
94 * Default game configuration.
95 * @enum {number}
97 Runner.config = {
98 ACCELERATION: 0.001,
99 BG_CLOUD_SPEED: 0.2,
100 BOTTOM_PAD: 10,
101 CLEAR_TIME: 3000,
102 CLOUD_FREQUENCY: 0.5,
103 GAMEOVER_CLEAR_TIME: 750,
104 GAP_COEFFICIENT: 0.6,
105 GRAVITY: 0.6,
106 INITIAL_JUMP_VELOCITY: 12,
107 MAX_CLOUDS: 6,
108 MAX_OBSTACLE_LENGTH: 3,
109 MAX_SPEED: 12,
110 MIN_JUMP_HEIGHT: 35,
111 MOBILE_SPEED_COEFFICIENT: 1.2,
112 RESOURCE_TEMPLATE_ID: 'audio-resources',
113 SPEED: 6,
114 SPEED_DROP_COEFFICIENT: 3
119 * Default dimensions.
120 * @enum {string}
122 Runner.defaultDimensions = {
123 WIDTH: DEFAULT_WIDTH,
124 HEIGHT: 150
129 * CSS class names.
130 * @enum {string}
132 Runner.classes = {
133 CANVAS: 'runner-canvas',
134 CONTAINER: 'runner-container',
135 CRASHED: 'crashed',
136 ICON: 'icon-offline',
137 TOUCH_CONTROLLER: 'controller'
142 * Image source urls.
143 * @enum {array.<object>}
145 Runner.imageSources = {
146 LDPI: [
147 {name: 'CACTUS_LARGE', id: '1x-obstacle-large'},
148 {name: 'CACTUS_SMALL', id: '1x-obstacle-small'},
149 {name: 'CLOUD', id: '1x-cloud'},
150 {name: 'HORIZON', id: '1x-horizon'},
151 {name: 'RESTART', id: '1x-restart'},
152 {name: 'TEXT_SPRITE', id: '1x-text'},
153 {name: 'TREX', id: '1x-trex'}
155 HDPI: [
156 {name: 'CACTUS_LARGE', id: '2x-obstacle-large'},
157 {name: 'CACTUS_SMALL', id: '2x-obstacle-small'},
158 {name: 'CLOUD', id: '2x-cloud'},
159 {name: 'HORIZON', id: '2x-horizon'},
160 {name: 'RESTART', id: '2x-restart'},
161 {name: 'TEXT_SPRITE', id: '2x-text'},
162 {name: 'TREX', id: '2x-trex'}
168 * Sound FX. Reference to the ID of the audio tag on interstitial page.
169 * @enum {string}
171 Runner.sounds = {
172 BUTTON_PRESS: 'offline-sound-press',
173 HIT: 'offline-sound-hit',
174 SCORE: 'offline-sound-reached'
179 * Key code mapping.
180 * @enum {object}
182 Runner.keycodes = {
183 JUMP: {'38': 1, '32': 1}, // Up, spacebar
184 DUCK: {'40': 1}, // Down
185 RESTART: {'13': 1} // Enter
190 * Runner event names.
191 * @enum {string}
193 Runner.events = {
194 ANIM_END: 'webkitAnimationEnd',
195 CLICK: 'click',
196 KEYDOWN: 'keydown',
197 KEYUP: 'keyup',
198 MOUSEDOWN: 'mousedown',
199 MOUSEUP: 'mouseup',
200 RESIZE: 'resize',
201 TOUCHEND: 'touchend',
202 TOUCHSTART: 'touchstart',
203 VISIBILITY: 'visibilitychange',
204 BLUR: 'blur',
205 FOCUS: 'focus',
206 LOAD: 'load'
210 Runner.prototype = {
212 * Setting individual settings for debugging.
213 * @param {string} setting
214 * @param {*} value
216 updateConfigSetting: function(setting, value) {
217 if (setting in this.config && value != undefined) {
218 this.config[setting] = value;
220 switch (setting) {
221 case 'GRAVITY':
222 case 'MIN_JUMP_HEIGHT':
223 case 'SPEED_DROP_COEFFICIENT':
224 this.tRex.config[setting] = value;
225 break;
226 case 'INITIAL_JUMP_VELOCITY':
227 this.tRex.setJumpVelocity(value);
228 break;
229 case 'SPEED':
230 this.setSpeed(value);
231 break;
237 * Load and cache the image assets from the page.
239 loadImages: function() {
240 var imageSources = IS_HIDPI ? Runner.imageSources.HDPI :
241 Runner.imageSources.LDPI;
243 var numImages = imageSources.length;
245 for (var i = numImages - 1; i >= 0; i--) {
246 var imgSource = imageSources[i];
247 this.images[imgSource.name] = document.getElementById(imgSource.id);
249 this.init();
253 * Load and decode base 64 encoded sounds.
255 loadSounds: function() {
256 if (!IS_IOS) {
257 this.audioContext = new AudioContext();
258 var resourceTemplate =
259 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
261 for (var sound in Runner.sounds) {
262 var soundSrc =
263 resourceTemplate.getElementById(Runner.sounds[sound]).src;
264 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
265 var buffer = decodeBase64ToArrayBuffer(soundSrc);
267 // Async, so no guarantee of order in array.
268 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
269 this.soundFx[index] = audioData;
270 }.bind(this, sound));
276 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
277 * @param {number} opt_speed
279 setSpeed: function(opt_speed) {
280 var speed = opt_speed || this.currentSpeed;
282 // Reduce the speed on smaller mobile screens.
283 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
284 var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
285 this.config.MOBILE_SPEED_COEFFICIENT;
286 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
287 } else if (opt_speed) {
288 this.currentSpeed = opt_speed;
293 * Game initialiser.
295 init: function() {
296 // Hide the static icon.
297 document.querySelector('.' + Runner.classes.ICON).style.visibility =
298 'hidden';
300 this.adjustDimensions();
301 this.setSpeed();
303 this.containerEl = document.createElement('div');
304 this.containerEl.className = Runner.classes.CONTAINER;
306 // Player canvas container.
307 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
308 this.dimensions.HEIGHT, Runner.classes.PLAYER);
310 this.canvasCtx = this.canvas.getContext('2d');
311 this.canvasCtx.fillStyle = '#f7f7f7';
312 this.canvasCtx.fill();
313 Runner.updateCanvasScaling(this.canvas);
315 // Horizon contains clouds, obstacles and the ground.
316 this.horizon = new Horizon(this.canvas, this.images, this.dimensions,
317 this.config.GAP_COEFFICIENT);
319 // Distance meter
320 this.distanceMeter = new DistanceMeter(this.canvas,
321 this.images.TEXT_SPRITE, this.dimensions.WIDTH);
323 // Draw t-rex
324 this.tRex = new Trex(this.canvas, this.images.TREX);
326 this.outerContainerEl.appendChild(this.containerEl);
328 if (IS_MOBILE) {
329 this.createTouchController();
332 this.startListening();
333 this.update();
335 window.addEventListener(Runner.events.RESIZE,
336 this.debounceResize.bind(this));
340 * Create the touch controller. A div that covers whole screen.
342 createTouchController: function() {
343 this.touchController = document.createElement('div');
344 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
348 * Debounce the resize event.
350 debounceResize: function() {
351 if (!this.resizeTimerId_) {
352 this.resizeTimerId_ =
353 setInterval(this.adjustDimensions.bind(this), 250);
358 * Adjust game space dimensions on resize.
360 adjustDimensions: function() {
361 clearInterval(this.resizeTimerId_);
362 this.resizeTimerId_ = null;
364 var boxStyles = window.getComputedStyle(this.outerContainerEl);
365 var padding = Number(boxStyles.paddingLeft.substr(0,
366 boxStyles.paddingLeft.length - 2));
368 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
370 // Redraw the elements back onto the canvas.
371 if (this.canvas) {
372 this.canvas.width = this.dimensions.WIDTH;
373 this.canvas.height = this.dimensions.HEIGHT;
375 Runner.updateCanvasScaling(this.canvas);
377 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
378 this.clearCanvas();
379 this.horizon.update(0, 0, true);
380 this.tRex.update(0);
382 // Outer container and distance meter.
383 if (this.activated || this.crashed) {
384 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
385 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
386 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
387 this.stop();
388 } else {
389 this.tRex.draw(0, 0);
392 // Game over panel.
393 if (this.crashed && this.gameOverPanel) {
394 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
395 this.gameOverPanel.draw();
401 * Play the game intro.
402 * Canvas container width expands out to the full width.
404 playIntro: function() {
405 if (!this.started && !this.crashed) {
406 this.playingIntro = true;
407 this.tRex.playingIntro = true;
409 // CSS animation definition.
410 var keyframes = '@-webkit-keyframes intro { ' +
411 'from { width:' + Trex.config.WIDTH + 'px }' +
412 'to { width: ' + this.dimensions.WIDTH + 'px }' +
413 '}';
414 document.styleSheets[0].insertRule(keyframes, 0);
416 this.containerEl.addEventListener(Runner.events.ANIM_END,
417 this.startGame.bind(this));
419 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
420 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
422 if (this.touchController) {
423 this.outerContainerEl.appendChild(this.touchController);
425 this.activated = true;
426 this.started = true;
427 } else if (this.crashed) {
428 this.restart();
434 * Update the game status to started.
436 startGame: function() {
437 this.runningTime = 0;
438 this.playingIntro = false;
439 this.tRex.playingIntro = false;
440 this.containerEl.style.webkitAnimation = '';
441 this.playCount++;
443 // Handle tabbing off the page. Pause the current game.
444 window.addEventListener(Runner.events.VISIBILITY,
445 this.onVisibilityChange.bind(this));
447 window.addEventListener(Runner.events.BLUR,
448 this.onVisibilityChange.bind(this));
450 window.addEventListener(Runner.events.FOCUS,
451 this.onVisibilityChange.bind(this));
454 clearCanvas: function() {
455 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
456 this.dimensions.HEIGHT);
460 * Update the game frame.
462 update: function() {
463 this.drawPending = false;
465 var now = getTimeStamp();
466 var deltaTime = now - (this.time || now);
467 this.time = now;
469 if (this.activated) {
470 this.clearCanvas();
472 if (this.tRex.jumping) {
473 this.tRex.updateJump(deltaTime, this.config);
476 this.runningTime += deltaTime;
477 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
479 // First jump triggers the intro.
480 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
481 this.playIntro();
484 // The horizon doesn't move until the intro is over.
485 if (this.playingIntro) {
486 this.horizon.update(0, this.currentSpeed, hasObstacles);
487 } else {
488 deltaTime = !this.started ? 0 : deltaTime;
489 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
492 // Check for collisions.
493 var collision = hasObstacles &&
494 checkForCollision(this.horizon.obstacles[0], this.tRex);
496 if (!collision) {
497 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
499 if (this.currentSpeed < this.config.MAX_SPEED) {
500 this.currentSpeed += this.config.ACCELERATION;
502 } else {
503 this.gameOver();
506 if (this.distanceMeter.getActualDistance(this.distanceRan) >
507 this.distanceMeter.maxScore) {
508 this.distanceRan = 0;
511 var playAcheivementSound = this.distanceMeter.update(deltaTime,
512 Math.ceil(this.distanceRan));
514 if (playAcheivementSound) {
515 this.playSound(this.soundFx.SCORE);
519 if (!this.crashed) {
520 this.tRex.update(deltaTime);
521 this.raq();
526 * Event handler.
528 handleEvent: function(e) {
529 return (function(evtType, events) {
530 switch (evtType) {
531 case events.KEYDOWN:
532 case events.TOUCHSTART:
533 case events.MOUSEDOWN:
534 this.onKeyDown(e);
535 break;
536 case events.KEYUP:
537 case events.TOUCHEND:
538 case events.MOUSEUP:
539 this.onKeyUp(e);
540 break;
542 }.bind(this))(e.type, Runner.events);
546 * Bind relevant key / mouse / touch listeners.
548 startListening: function() {
549 // Keys.
550 document.addEventListener(Runner.events.KEYDOWN, this);
551 document.addEventListener(Runner.events.KEYUP, this);
553 if (IS_MOBILE) {
554 // Mobile only touch devices.
555 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
556 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
557 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
558 } else {
559 // Mouse.
560 document.addEventListener(Runner.events.MOUSEDOWN, this);
561 document.addEventListener(Runner.events.MOUSEUP, this);
566 * Remove all listeners.
568 stopListening: function() {
569 document.removeEventListener(Runner.events.KEYDOWN, this);
570 document.removeEventListener(Runner.events.KEYUP, this);
572 if (IS_MOBILE) {
573 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
574 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
575 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
576 } else {
577 document.removeEventListener(Runner.events.MOUSEDOWN, this);
578 document.removeEventListener(Runner.events.MOUSEUP, this);
583 * Process keydown.
584 * @param {Event} e
586 onKeyDown: function(e) {
587 if (e.target != this.detailsButton) {
588 if (!this.crashed && (Runner.keycodes.JUMP[String(e.keyCode)] ||
589 e.type == Runner.events.TOUCHSTART)) {
590 if (!this.activated) {
591 this.loadSounds();
592 this.activated = true;
595 if (!this.tRex.jumping) {
596 this.playSound(this.soundFx.BUTTON_PRESS);
597 this.tRex.startJump();
601 if (this.crashed && e.type == Runner.events.TOUCHSTART &&
602 e.currentTarget == this.containerEl) {
603 this.restart();
607 // Speed drop, activated only when jump key is not pressed.
608 if (Runner.keycodes.DUCK[e.keyCode] && this.tRex.jumping) {
609 e.preventDefault();
610 this.tRex.setSpeedDrop();
616 * Process key up.
617 * @param {Event} e
619 onKeyUp: function(e) {
620 var keyCode = String(e.keyCode);
621 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
622 e.type == Runner.events.TOUCHEND ||
623 e.type == Runner.events.MOUSEDOWN;
625 if (this.isRunning() && isjumpKey) {
626 this.tRex.endJump();
627 } else if (Runner.keycodes.DUCK[keyCode]) {
628 this.tRex.speedDrop = false;
629 } else if (this.crashed) {
630 // Check that enough time has elapsed before allowing jump key to restart.
631 var deltaTime = getTimeStamp() - this.time;
633 if (Runner.keycodes.RESTART[keyCode] ||
634 (e.type == Runner.events.MOUSEUP && e.target == this.canvas) ||
635 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
636 Runner.keycodes.JUMP[keyCode])) {
637 this.restart();
639 } else if (this.paused && isjumpKey) {
640 this.play();
645 * RequestAnimationFrame wrapper.
647 raq: function() {
648 if (!this.drawPending) {
649 this.drawPending = true;
650 this.raqId = requestAnimationFrame(this.update.bind(this));
655 * Whether the game is running.
656 * @return {boolean}
658 isRunning: function() {
659 return !!this.raqId;
663 * Game over state.
665 gameOver: function() {
666 this.playSound(this.soundFx.HIT);
667 vibrate(200);
669 this.stop();
670 this.crashed = true;
671 this.distanceMeter.acheivement = false;
673 this.tRex.update(100, Trex.status.CRASHED);
675 // Game over panel.
676 if (!this.gameOverPanel) {
677 this.gameOverPanel = new GameOverPanel(this.canvas,
678 this.images.TEXT_SPRITE, this.images.RESTART,
679 this.dimensions);
680 } else {
681 this.gameOverPanel.draw();
684 // Update the high score.
685 if (this.distanceRan > this.highestScore) {
686 this.highestScore = Math.ceil(this.distanceRan);
687 this.distanceMeter.setHighScore(this.highestScore);
690 // Reset the time clock.
691 this.time = getTimeStamp();
694 stop: function() {
695 this.activated = false;
696 this.paused = true;
697 cancelAnimationFrame(this.raqId);
698 this.raqId = 0;
701 play: function() {
702 if (!this.crashed) {
703 this.activated = true;
704 this.paused = false;
705 this.tRex.update(0, Trex.status.RUNNING);
706 this.time = getTimeStamp();
707 this.update();
711 restart: function() {
712 if (!this.raqId) {
713 this.playCount++;
714 this.runningTime = 0;
715 this.activated = true;
716 this.crashed = false;
717 this.distanceRan = 0;
718 this.setSpeed(this.config.SPEED);
720 this.time = getTimeStamp();
721 this.containerEl.classList.remove(Runner.classes.CRASHED);
722 this.clearCanvas();
723 this.distanceMeter.reset(this.highestScore);
724 this.horizon.reset();
725 this.tRex.reset();
726 this.playSound(this.soundFx.BUTTON_PRESS);
728 this.update();
733 * Pause the game if the tab is not in focus.
735 onVisibilityChange: function(e) {
736 if (document.hidden || document.webkitHidden || e.type == 'blur') {
737 this.stop();
738 } else {
739 this.play();
744 * Play a sound.
745 * @param {SoundBuffer} soundBuffer
747 playSound: function(soundBuffer) {
748 if (soundBuffer) {
749 var sourceNode = this.audioContext.createBufferSource();
750 sourceNode.buffer = soundBuffer;
751 sourceNode.connect(this.audioContext.destination);
752 sourceNode.start(0);
759 * Updates the canvas size taking into
760 * account the backing store pixel ratio and
761 * the device pixel ratio.
763 * See article by Paul Lewis:
764 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
766 * @param {HTMLCanvasElement} canvas
767 * @param {number} opt_width
768 * @param {number} opt_height
769 * @return {boolean} Whether the canvas was scaled.
771 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
772 var context = canvas.getContext('2d');
774 // Query the various pixel ratios
775 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
776 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
777 var ratio = devicePixelRatio / backingStoreRatio;
779 // Upscale the canvas if the two ratios don't match
780 if (devicePixelRatio !== backingStoreRatio) {
782 var oldWidth = opt_width || canvas.width;
783 var oldHeight = opt_height || canvas.height;
785 canvas.width = oldWidth * ratio;
786 canvas.height = oldHeight * ratio;
788 canvas.style.width = oldWidth + 'px';
789 canvas.style.height = oldHeight + 'px';
791 // Scale the context to counter the fact that we've manually scaled
792 // our canvas element.
793 context.scale(ratio, ratio);
794 return true;
796 return false;
801 * Get random number.
802 * @param {number} min
803 * @param {number} max
804 * @param {number}
806 function getRandomNum(min, max) {
807 return Math.floor(Math.random() * (max - min + 1)) + min;
812 * Vibrate on mobile devices.
813 * @param {number} duration Duration of the vibration in milliseconds.
815 function vibrate(duration) {
816 if (IS_MOBILE && window.navigator.vibrate) {
817 window.navigator.vibrate(duration);
823 * Create canvas element.
824 * @param {HTMLElement} container Element to append canvas to.
825 * @param {number} width
826 * @param {number} height
827 * @param {string} opt_classname
828 * @return {HTMLCanvasElement}
830 function createCanvas(container, width, height, opt_classname) {
831 var canvas = document.createElement('canvas');
832 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
833 opt_classname : Runner.classes.CANVAS;
834 canvas.width = width;
835 canvas.height = height;
836 container.appendChild(canvas);
838 return canvas;
843 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
844 * @param {string} base64String
846 function decodeBase64ToArrayBuffer(base64String) {
847 var len = (base64String.length / 4) * 3;
848 var str = atob(base64String);
849 var arrayBuffer = new ArrayBuffer(len);
850 var bytes = new Uint8Array(arrayBuffer);
852 for (var i = 0; i < len; i++) {
853 bytes[i] = str.charCodeAt(i);
855 return bytes.buffer;
860 * Return the current timestamp.
861 * @return {number}
863 function getTimeStamp() {
864 return IS_IOS ? new Date().getTime() : performance.now();
868 //******************************************************************************
872 * Game over panel.
873 * @param {!HTMLCanvasElement} canvas
874 * @param {!HTMLImage} textSprite
875 * @param {!HTMLImage} restartImg
876 * @param {!Object} dimensions Canvas dimensions.
877 * @constructor
879 function GameOverPanel(canvas, textSprite, restartImg, dimensions) {
880 this.canvas = canvas;
881 this.canvasCtx = canvas.getContext('2d');
882 this.canvasDimensions = dimensions;
883 this.textSprite = textSprite;
884 this.restartImg = restartImg;
885 this.draw();
890 * Dimensions used in the panel.
891 * @enum {number}
893 GameOverPanel.dimensions = {
894 TEXT_X: 0,
895 TEXT_Y: 13,
896 TEXT_WIDTH: 191,
897 TEXT_HEIGHT: 11,
898 RESTART_WIDTH: 36,
899 RESTART_HEIGHT: 32
903 GameOverPanel.prototype = {
905 * Update the panel dimensions.
906 * @param {number} width New canvas width.
907 * @param {number} opt_height Optional new canvas height.
909 updateDimensions: function(width, opt_height) {
910 this.canvasDimensions.WIDTH = width;
911 if (opt_height) {
912 this.canvasDimensions.HEIGHT = opt_height;
917 * Draw the panel.
919 draw: function() {
920 var dimensions = GameOverPanel.dimensions;
922 var centerX = this.canvasDimensions.WIDTH / 2;
924 // Game over text.
925 var textSourceX = dimensions.TEXT_X;
926 var textSourceY = dimensions.TEXT_Y;
927 var textSourceWidth = dimensions.TEXT_WIDTH;
928 var textSourceHeight = dimensions.TEXT_HEIGHT;
930 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
931 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
932 var textTargetWidth = dimensions.TEXT_WIDTH;
933 var textTargetHeight = dimensions.TEXT_HEIGHT;
935 var restartSourceWidth = dimensions.RESTART_WIDTH;
936 var restartSourceHeight = dimensions.RESTART_HEIGHT;
937 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
938 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
940 if (IS_HIDPI) {
941 textSourceY *= 2;
942 textSourceX *= 2;
943 textSourceWidth *= 2;
944 textSourceHeight *= 2;
945 restartSourceWidth *= 2;
946 restartSourceHeight *= 2;
949 // Game over text from sprite.
950 this.canvasCtx.drawImage(this.textSprite,
951 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
952 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
954 // Restart button.
955 this.canvasCtx.drawImage(this.restartImg, 0, 0,
956 restartSourceWidth, restartSourceHeight,
957 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
958 dimensions.RESTART_HEIGHT);
963 //******************************************************************************
966 * Check for a collision.
967 * @param {!Obstacle} obstacle
968 * @param {!Trex} tRex T-rex object.
969 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
970 * collision boxes.
971 * @return {Array.<CollisionBox>}
973 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
974 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
976 // Adjustments are made to the bounding box as there is a 1 pixel white
977 // border around the t-rex and obstacles.
978 var tRexBox = new CollisionBox(
979 tRex.xPos + 1,
980 tRex.yPos + 1,
981 tRex.config.WIDTH - 2,
982 tRex.config.HEIGHT - 2);
984 var obstacleBox = new CollisionBox(
985 obstacle.xPos + 1,
986 obstacle.yPos + 1,
987 obstacle.typeConfig.width * obstacle.size - 2,
988 obstacle.typeConfig.height - 2);
990 // Debug outer box
991 if (opt_canvasCtx) {
992 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
995 // Simple outer bounds check.
996 if (boxCompare(tRexBox, obstacleBox)) {
997 var collisionBoxes = obstacle.collisionBoxes;
998 var tRexCollisionBoxes = Trex.collisionBoxes;
1000 // Detailed axis aligned box check.
1001 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
1002 for (var i = 0; i < collisionBoxes.length; i++) {
1003 // Adjust the box to actual positions.
1004 var adjTrexBox =
1005 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
1006 var adjObstacleBox =
1007 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
1008 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
1010 // Draw boxes for debug.
1011 if (opt_canvasCtx) {
1012 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1015 if (crashed) {
1016 return [adjTrexBox, adjObstacleBox];
1021 return false;
1026 * Adjust the collision box.
1027 * @param {!CollisionBox} box The original box.
1028 * @param {!CollisionBox} adjustment Adjustment box.
1029 * @return {CollisionBox} The adjusted collision box object.
1031 function createAdjustedCollisionBox(box, adjustment) {
1032 return new CollisionBox(
1033 box.x + adjustment.x,
1034 box.y + adjustment.y,
1035 box.width,
1036 box.height);
1041 * Draw the collision boxes for debug.
1043 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1044 canvasCtx.save();
1045 canvasCtx.strokeStyle = '#f00';
1046 canvasCtx.strokeRect(tRexBox.x, tRexBox.y,
1047 tRexBox.width, tRexBox.height);
1049 canvasCtx.strokeStyle = '#0f0';
1050 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1051 obstacleBox.width, obstacleBox.height);
1052 canvasCtx.restore();
1057 * Compare two collision boxes for a collision.
1058 * @param {CollisionBox} tRexBox
1059 * @param {CollisionBox} obstacleBox
1060 * @return {boolean} Whether the boxes intersected.
1062 function boxCompare(tRexBox, obstacleBox) {
1063 var crashed = false;
1064 var tRexBoxX = tRexBox.x;
1065 var tRexBoxY = tRexBox.y;
1067 var obstacleBoxX = obstacleBox.x;
1068 var obstacleBoxY = obstacleBox.y;
1070 // Axis-Aligned Bounding Box method.
1071 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1072 tRexBox.x + tRexBox.width > obstacleBoxX &&
1073 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1074 tRexBox.height + tRexBox.y > obstacleBox.y) {
1075 crashed = true;
1078 return crashed;
1082 //******************************************************************************
1085 * Collision box object.
1086 * @param {number} x X position.
1087 * @param {number} y Y Position.
1088 * @param {number} w Width.
1089 * @param {number} h Height.
1091 function CollisionBox(x, y, w, h) {
1092 this.x = x;
1093 this.y = y;
1094 this.width = w;
1095 this.height = h;
1099 //******************************************************************************
1102 * Obstacle.
1103 * @param {HTMLCanvasCtx} canvasCtx
1104 * @param {Obstacle.type} type
1105 * @param {image} obstacleImg Image sprite.
1106 * @param {Object} dimensions
1107 * @param {number} gapCoefficient Mutipler in determining the gap.
1108 * @param {number} speed
1110 function Obstacle(canvasCtx, type, obstacleImg, dimensions,
1111 gapCoefficient, speed) {
1113 this.canvasCtx = canvasCtx;
1114 this.image = obstacleImg;
1115 this.typeConfig = type;
1116 this.gapCoefficient = gapCoefficient;
1117 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1118 this.dimensions = dimensions;
1119 this.remove = false;
1120 this.xPos = 0;
1121 this.yPos = this.typeConfig.yPos;
1122 this.width = 0;
1123 this.collisionBoxes = [];
1124 this.gap = 0;
1126 this.init(speed);
1130 * Coefficient for calculating the maximum gap.
1131 * @const
1133 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1136 * Maximum obstacle grouping count.
1137 * @const
1139 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1142 Obstacle.prototype = {
1144 * Initialise the DOM for the obstacle.
1145 * @param {number} speed
1147 init: function(speed) {
1148 this.cloneCollisionBoxes();
1150 // Only allow sizing if we're at the right speed.
1151 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1152 this.size = 1;
1155 this.width = this.typeConfig.width * this.size;
1156 this.xPos = this.dimensions.WIDTH - this.width;
1158 this.draw();
1160 // Make collision box adjustments,
1161 // Central box is adjusted to the size as one box.
1162 // ____ ______ ________
1163 // _| |-| _| |-| _| |-|
1164 // | |<->| | | |<--->| | | |<----->| |
1165 // | | 1 | | | | 2 | | | | 3 | |
1166 // |_|___|_| |_|_____|_| |_|_______|_|
1168 if (this.size > 1) {
1169 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1170 this.collisionBoxes[2].width;
1171 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1174 this.gap = this.getGap(this.gapCoefficient, speed);
1178 * Draw and crop based on size.
1180 draw: function() {
1181 var sourceWidth = this.typeConfig.width;
1182 var sourceHeight = this.typeConfig.height;
1184 if (IS_HIDPI) {
1185 sourceWidth = sourceWidth * 2;
1186 sourceHeight = sourceHeight * 2;
1189 // Sprite
1190 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1));
1191 this.canvasCtx.drawImage(this.image,
1192 sourceX, 0,
1193 sourceWidth * this.size, sourceHeight,
1194 this.xPos, this.yPos,
1195 this.typeConfig.width * this.size, this.typeConfig.height);
1199 * Obstacle frame update.
1200 * @param {number} deltaTime
1201 * @param {number} speed
1203 update: function(deltaTime, speed) {
1204 if (!this.remove) {
1205 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1206 this.draw();
1208 if (!this.isVisible()) {
1209 this.remove = true;
1215 * Calculate a random gap size.
1216 * - Minimum gap gets wider as speed increses
1217 * @param {number} gapCoefficient
1218 * @param {number} speed
1219 * @return {number} The gap size.
1221 getGap: function(gapCoefficient, speed) {
1222 var minGap = Math.round(this.width * speed +
1223 this.typeConfig.minGap * gapCoefficient);
1224 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1225 return getRandomNum(minGap, maxGap);
1229 * Check if obstacle is visible.
1230 * @return {boolean} Whether the obstacle is in the game area.
1232 isVisible: function() {
1233 return this.xPos + this.width > 0;
1237 * Make a copy of the collision boxes, since these will change based on
1238 * obstacle type and size.
1240 cloneCollisionBoxes: function() {
1241 var collisionBoxes = this.typeConfig.collisionBoxes;
1243 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1244 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1245 collisionBoxes[i].y, collisionBoxes[i].width,
1246 collisionBoxes[i].height);
1253 * Obstacle definitions.
1254 * minGap: minimum pixel space betweeen obstacles.
1255 * multipleSpeed: Speed at which multiples are allowed.
1257 Obstacle.types = [
1259 type: 'CACTUS_SMALL',
1260 className: ' cactus cactus-small ',
1261 width: 17,
1262 height: 35,
1263 yPos: 105,
1264 multipleSpeed: 3,
1265 minGap: 120,
1266 collisionBoxes: [
1267 new CollisionBox(0, 7, 5, 27),
1268 new CollisionBox(4, 0, 6, 34),
1269 new CollisionBox(10, 4, 7, 14)
1273 type: 'CACTUS_LARGE',
1274 className: ' cactus cactus-large ',
1275 width: 25,
1276 height: 50,
1277 yPos: 90,
1278 multipleSpeed: 6,
1279 minGap: 120,
1280 collisionBoxes: [
1281 new CollisionBox(0, 12, 7, 38),
1282 new CollisionBox(8, 0, 7, 49),
1283 new CollisionBox(13, 10, 10, 38)
1289 //******************************************************************************
1291 * T-rex game character.
1292 * @param {HTMLCanvas} canvas
1293 * @param {HTMLImage} image Character image.
1294 * @constructor
1296 function Trex(canvas, image) {
1297 this.canvas = canvas;
1298 this.canvasCtx = canvas.getContext('2d');
1299 this.image = image;
1300 this.xPos = 0;
1301 this.yPos = 0;
1302 // Position when on the ground.
1303 this.groundYPos = 0;
1304 this.currentFrame = 0;
1305 this.currentAnimFrames = [];
1306 this.blinkDelay = 0;
1307 this.animStartTime = 0;
1308 this.timer = 0;
1309 this.msPerFrame = 1000 / FPS;
1310 this.config = Trex.config;
1311 // Current status.
1312 this.status = Trex.status.WAITING;
1314 this.jumping = false;
1315 this.jumpVelocity = 0;
1316 this.reachedMinHeight = false;
1317 this.speedDrop = false;
1318 this.jumpCount = 0;
1319 this.jumpspotX = 0;
1321 this.init();
1326 * T-rex player config.
1327 * @enum {number}
1329 Trex.config = {
1330 DROP_VELOCITY: -5,
1331 GRAVITY: 0.6,
1332 HEIGHT: 47,
1333 INIITAL_JUMP_VELOCITY: -10,
1334 INTRO_DURATION: 1500,
1335 MAX_JUMP_HEIGHT: 30,
1336 MIN_JUMP_HEIGHT: 30,
1337 SPEED_DROP_COEFFICIENT: 3,
1338 SPRITE_WIDTH: 262,
1339 START_X_POS: 50,
1340 WIDTH: 44
1345 * Used in collision detection.
1346 * @type {Array.<CollisionBox>}
1348 Trex.collisionBoxes = [
1349 new CollisionBox(1, -1, 30, 26),
1350 new CollisionBox(32, 0, 8, 16),
1351 new CollisionBox(10, 35, 14, 8),
1352 new CollisionBox(1, 24, 29, 5),
1353 new CollisionBox(5, 30, 21, 4),
1354 new CollisionBox(9, 34, 15, 4)
1359 * Animation states.
1360 * @enum {string}
1362 Trex.status = {
1363 CRASHED: 'CRASHED',
1364 JUMPING: 'JUMPING',
1365 RUNNING: 'RUNNING',
1366 WAITING: 'WAITING'
1370 * Blinking coefficient.
1371 * @const
1373 Trex.BLINK_TIMING = 7000;
1377 * Animation config for different states.
1378 * @enum {object}
1380 Trex.animFrames = {
1381 WAITING: {
1382 frames: [44, 0],
1383 msPerFrame: 1000 / 3
1385 RUNNING: {
1386 frames: [88, 132],
1387 msPerFrame: 1000 / 12
1389 CRASHED: {
1390 frames: [220],
1391 msPerFrame: 1000 / 60
1393 JUMPING: {
1394 frames: [0],
1395 msPerFrame: 1000 / 60
1400 Trex.prototype = {
1402 * T-rex player initaliser.
1403 * Sets the t-rex to blink at random intervals.
1405 init: function() {
1406 this.blinkDelay = this.setBlinkDelay();
1407 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1408 Runner.config.BOTTOM_PAD;
1409 this.yPos = this.groundYPos;
1410 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1412 this.draw(0, 0);
1413 this.update(0, Trex.status.WAITING);
1417 * Setter for the jump velocity.
1418 * The approriate drop velocity is also set.
1420 setJumpVelocity: function(setting) {
1421 this.config.INIITAL_JUMP_VELOCITY = -setting;
1422 this.config.DROP_VELOCITY = -setting / 2;
1426 * Set the animation status.
1427 * @param {!number} deltaTime
1428 * @param {Trex.status} status Optional status to switch to.
1430 update: function(deltaTime, opt_status) {
1431 this.timer += deltaTime;
1433 // Update the status.
1434 if (opt_status) {
1435 this.status = opt_status;
1436 this.currentFrame = 0;
1437 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1438 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1440 if (opt_status == Trex.status.WAITING) {
1441 this.animStartTime = getTimeStamp();
1442 this.setBlinkDelay();
1446 // Game intro animation, T-rex moves in from the left.
1447 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1448 this.xPos += Math.round((this.config.START_X_POS /
1449 this.config.INTRO_DURATION) * deltaTime);
1452 if (this.status == Trex.status.WAITING) {
1453 this.blink(getTimeStamp());
1454 } else {
1455 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1458 // Update the frame position.
1459 if (this.timer >= this.msPerFrame) {
1460 this.currentFrame = this.currentFrame ==
1461 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1462 this.timer = 0;
1467 * Draw the t-rex to a particular position.
1468 * @param {number} x
1469 * @param {number} y
1471 draw: function(x, y) {
1472 var sourceX = x;
1473 var sourceY = y;
1474 var sourceWidth = this.config.WIDTH;
1475 var sourceHeight = this.config.HEIGHT;
1477 if (IS_HIDPI) {
1478 sourceX *= 2;
1479 sourceY *= 2;
1480 sourceWidth *= 2;
1481 sourceHeight *= 2;
1484 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1485 sourceWidth, sourceHeight,
1486 this.xPos, this.yPos,
1487 this.config.WIDTH, this.config.HEIGHT);
1491 * Sets a random time for the blink to happen.
1493 setBlinkDelay: function() {
1494 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1498 * Make t-rex blink at random intervals.
1499 * @param {number} time Current time in milliseconds.
1501 blink: function(time) {
1502 var deltaTime = time - this.animStartTime;
1504 if (deltaTime >= this.blinkDelay) {
1505 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1507 if (this.currentFrame == 1) {
1508 // Set new random delay to blink.
1509 this.setBlinkDelay();
1510 this.animStartTime = time;
1516 * Initialise a jump.
1518 startJump: function() {
1519 if (!this.jumping) {
1520 this.update(0, Trex.status.JUMPING);
1521 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY;
1522 this.jumping = true;
1523 this.reachedMinHeight = false;
1524 this.speedDrop = false;
1529 * Jump is complete, falling down.
1531 endJump: function() {
1532 if (this.reachedMinHeight &&
1533 this.jumpVelocity < this.config.DROP_VELOCITY) {
1534 this.jumpVelocity = this.config.DROP_VELOCITY;
1539 * Update frame for a jump.
1540 * @param {number} deltaTime
1542 updateJump: function(deltaTime) {
1543 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1544 var framesElapsed = deltaTime / msPerFrame;
1546 // Speed drop makes Trex fall faster.
1547 if (this.speedDrop) {
1548 this.yPos += Math.round(this.jumpVelocity *
1549 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1550 } else {
1551 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1554 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1556 // Minimum height has been reached.
1557 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1558 this.reachedMinHeight = true;
1561 // Reached max height
1562 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1563 this.endJump();
1566 // Back down at ground level. Jump completed.
1567 if (this.yPos > this.groundYPos) {
1568 this.reset();
1569 this.jumpCount++;
1572 this.update(deltaTime);
1576 * Set the speed drop. Immediately cancels the current jump.
1578 setSpeedDrop: function() {
1579 this.speedDrop = true;
1580 this.jumpVelocity = 1;
1584 * Reset the t-rex to running at start of game.
1586 reset: function() {
1587 this.yPos = this.groundYPos;
1588 this.jumpVelocity = 0;
1589 this.jumping = false;
1590 this.update(0, Trex.status.RUNNING);
1591 this.midair = false;
1592 this.speedDrop = false;
1593 this.jumpCount = 0;
1598 //******************************************************************************
1601 * Handles displaying the distance meter.
1602 * @param {!HTMLCanvasElement} canvas
1603 * @param {!HTMLImage} spriteSheet Image sprite.
1604 * @param {number} canvasWidth
1605 * @constructor
1607 function DistanceMeter(canvas, spriteSheet, canvasWidth) {
1608 this.canvas = canvas;
1609 this.canvasCtx = canvas.getContext('2d');
1610 this.image = spriteSheet;
1611 this.x = 0;
1612 this.y = 5;
1614 this.currentDistance = 0;
1615 this.maxScore = 0;
1616 this.highScore = 0;
1617 this.container = null;
1619 this.digits = [];
1620 this.acheivement = false;
1621 this.defaultString = '';
1622 this.flashTimer = 0;
1623 this.flashIterations = 0;
1625 this.config = DistanceMeter.config;
1626 this.init(canvasWidth);
1631 * @enum {number}
1633 DistanceMeter.dimensions = {
1634 WIDTH: 10,
1635 HEIGHT: 13,
1636 DEST_WIDTH: 11
1641 * Y positioning of the digits in the sprite sheet.
1642 * X position is always 0.
1643 * @type {array.<number>}
1645 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1649 * Distance meter config.
1650 * @enum {number}
1652 DistanceMeter.config = {
1653 // Number of digits.
1654 MAX_DISTANCE_UNITS: 5,
1656 // Distance that causes achievement animation.
1657 ACHIEVEMENT_DISTANCE: 100,
1659 // Used for conversion from pixel distance to a scaled unit.
1660 COEFFICIENT: 0.025,
1662 // Flash duration in milliseconds.
1663 FLASH_DURATION: 1000 / 4,
1665 // Flash iterations for achievement animation.
1666 FLASH_ITERATIONS: 3
1670 DistanceMeter.prototype = {
1672 * Initialise the distance meter to '00000'.
1673 * @param {number} width Canvas width in px.
1675 init: function(width) {
1676 var maxDistanceStr = '';
1678 this.calcXPos(width);
1679 this.maxScore = this.config.MAX_DISTANCE_UNITS;
1680 for (var i = 0; i < this.config.MAX_DISTANCE_UNITS; i++) {
1681 this.draw(i, 0);
1682 this.defaultString += '0';
1683 maxDistanceStr += '9';
1686 this.maxScore = parseInt(maxDistanceStr);
1690 * Calculate the xPos in the canvas.
1691 * @param {number} canvasWidth
1693 calcXPos: function(canvasWidth) {
1694 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1695 (this.config.MAX_DISTANCE_UNITS + 1));
1699 * Draw a digit to canvas.
1700 * @param {number} digitPos Position of the digit.
1701 * @param {number} value Digit value 0-9.
1702 * @param {boolean} opt_highScore Whether drawing the high score.
1704 draw: function(digitPos, value, opt_highScore) {
1705 var sourceWidth = DistanceMeter.dimensions.WIDTH;
1706 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1707 var sourceX = DistanceMeter.dimensions.WIDTH * value;
1709 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1710 var targetY = this.y;
1711 var targetWidth = DistanceMeter.dimensions.WIDTH;
1712 var targetHeight = DistanceMeter.dimensions.HEIGHT;
1714 // For high DPI we 2x source values.
1715 if (IS_HIDPI) {
1716 sourceWidth *= 2;
1717 sourceHeight *= 2;
1718 sourceX *= 2;
1721 this.canvasCtx.save();
1723 if (opt_highScore) {
1724 // Left of the current score.
1725 var highScoreX = this.x - (this.config.MAX_DISTANCE_UNITS * 2) *
1726 DistanceMeter.dimensions.WIDTH;
1727 this.canvasCtx.translate(highScoreX, this.y);
1728 } else {
1729 this.canvasCtx.translate(this.x, this.y);
1732 this.canvasCtx.drawImage(this.image, sourceX, 0,
1733 sourceWidth, sourceHeight,
1734 targetX, targetY,
1735 targetWidth, targetHeight
1738 this.canvasCtx.restore();
1742 * Covert pixel distance to a 'real' distance.
1743 * @param {number} distance Pixel distance ran.
1744 * @return {number} The 'real' distance ran.
1746 getActualDistance: function(distance) {
1747 return distance ?
1748 Math.round(distance * this.config.COEFFICIENT) : 0;
1752 * Update the distance meter.
1753 * @param {number} deltaTime
1754 * @param {number} distance
1755 * @return {boolean} Whether the acheivement sound fx should be played.
1757 update: function(deltaTime, distance) {
1758 var paint = true;
1759 var playSound = false;
1761 if (!this.acheivement) {
1762 distance = this.getActualDistance(distance);
1764 if (distance > 0) {
1765 // Acheivement unlocked
1766 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1767 // Flash score and play sound.
1768 this.acheivement = true;
1769 this.flashTimer = 0;
1770 playSound = true;
1773 // Create a string representation of the distance with leading 0.
1774 var distanceStr = (this.defaultString +
1775 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1776 this.digits = distanceStr.split('');
1777 } else {
1778 this.digits = this.defaultString.split('');
1780 } else {
1781 // Control flashing of the score on reaching acheivement.
1782 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1783 this.flashTimer += deltaTime;
1785 if (this.flashTimer < this.config.FLASH_DURATION) {
1786 paint = false;
1787 } else if (this.flashTimer >
1788 this.config.FLASH_DURATION * 2) {
1789 this.flashTimer = 0;
1790 this.flashIterations++;
1792 } else {
1793 this.acheivement = false;
1794 this.flashIterations = 0;
1795 this.flashTimer = 0;
1799 // Draw the digits if not flashing.
1800 if (paint) {
1801 for (var i = this.digits.length - 1; i >= 0; i--) {
1802 this.draw(i, parseInt(this.digits[i]));
1806 this.drawHighScore();
1808 return playSound;
1812 * Draw the high score.
1814 drawHighScore: function() {
1815 this.canvasCtx.save();
1816 this.canvasCtx.globalAlpha = .8;
1817 for (var i = this.highScore.length - 1; i >= 0; i--) {
1818 this.draw(i, parseInt(this.highScore[i], 10), true);
1820 this.canvasCtx.restore();
1824 * Set the highscore as a array string.
1825 * Position of char in the sprite: H - 10, I - 11.
1826 * @param {number} distance Distance ran in pixels.
1828 setHighScore: function(distance) {
1829 distance = this.getActualDistance(distance);
1830 var highScoreStr = (this.defaultString +
1831 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1833 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
1837 * Reset the distance meter back to '00000'.
1839 reset: function() {
1840 this.update(0);
1841 this.acheivement = false;
1846 //******************************************************************************
1849 * Cloud background item.
1850 * Similar to an obstacle object but without collision boxes.
1851 * @param {HTMLCanvasElement} canvas Canvas element.
1852 * @param {Image} cloudImg
1853 * @param {number} containerWidth
1855 function Cloud(canvas, cloudImg, containerWidth) {
1856 this.canvas = canvas;
1857 this.canvasCtx = this.canvas.getContext('2d');
1858 this.image = cloudImg;
1859 this.containerWidth = containerWidth;
1860 this.xPos = containerWidth;
1861 this.yPos = 0;
1862 this.remove = false;
1863 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
1864 Cloud.config.MAX_CLOUD_GAP);
1866 this.init();
1871 * Cloud object config.
1872 * @enum {number}
1874 Cloud.config = {
1875 HEIGHT: 14,
1876 MAX_CLOUD_GAP: 400,
1877 MAX_SKY_LEVEL: 30,
1878 MIN_CLOUD_GAP: 100,
1879 MIN_SKY_LEVEL: 71,
1880 WIDTH: 46
1884 Cloud.prototype = {
1886 * Initialise the cloud. Sets the Cloud height.
1888 init: function() {
1889 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
1890 Cloud.config.MIN_SKY_LEVEL);
1891 this.draw();
1895 * Draw the cloud.
1897 draw: function() {
1898 this.canvasCtx.save();
1899 var sourceWidth = Cloud.config.WIDTH;
1900 var sourceHeight = Cloud.config.HEIGHT;
1902 if (IS_HIDPI) {
1903 sourceWidth = sourceWidth * 2;
1904 sourceHeight = sourceHeight * 2;
1907 this.canvasCtx.drawImage(this.image, 0, 0,
1908 sourceWidth, sourceHeight,
1909 this.xPos, this.yPos,
1910 Cloud.config.WIDTH, Cloud.config.HEIGHT);
1912 this.canvasCtx.restore();
1916 * Update the cloud position.
1917 * @param {number} speed
1919 update: function(speed) {
1920 if (!this.remove) {
1921 this.xPos -= Math.ceil(speed);
1922 this.draw();
1924 // Mark as removeable if no longer in the canvas.
1925 if (!this.isVisible()) {
1926 this.remove = true;
1932 * Check if the cloud is visible on the stage.
1933 * @return {boolean}
1935 isVisible: function() {
1936 return this.xPos + Cloud.config.WIDTH > 0;
1941 //******************************************************************************
1944 * Horizon Line.
1945 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
1946 * @param {HTMLCanvasElement} canvas
1947 * @param {HTMLImage} bgImg Horizon line sprite.
1948 * @constructor
1950 function HorizonLine(canvas, bgImg) {
1951 this.image = bgImg;
1952 this.canvas = canvas;
1953 this.canvasCtx = canvas.getContext('2d');
1954 this.sourceDimensions = {};
1955 this.dimensions = HorizonLine.dimensions;
1956 this.sourceXPos = [0, this.dimensions.WIDTH];
1957 this.xPos = [];
1958 this.yPos = 0;
1959 this.bumpThreshold = 0.5;
1961 this.setSourceDimensions();
1962 this.draw();
1967 * Horizon line dimensions.
1968 * @enum {number}
1970 HorizonLine.dimensions = {
1971 WIDTH: 600,
1972 HEIGHT: 12,
1973 YPOS: 127
1977 HorizonLine.prototype = {
1979 * Set the source dimensions of the horizon line.
1981 setSourceDimensions: function() {
1983 for (var dimension in HorizonLine.dimensions) {
1984 if (IS_HIDPI) {
1985 if (dimension != 'YPOS') {
1986 this.sourceDimensions[dimension] =
1987 HorizonLine.dimensions[dimension] * 2;
1989 } else {
1990 this.sourceDimensions[dimension] =
1991 HorizonLine.dimensions[dimension];
1993 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
1996 this.xPos = [0, HorizonLine.dimensions.WIDTH];
1997 this.yPos = HorizonLine.dimensions.YPOS;
2001 * Return the crop x position of a type.
2003 getRandomType: function() {
2004 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
2008 * Draw the horizon line.
2010 draw: function() {
2011 this.canvasCtx.drawImage(this.image, this.sourceXPos[0], 0,
2012 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2013 this.xPos[0], this.yPos,
2014 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2016 this.canvasCtx.drawImage(this.image, this.sourceXPos[1], 0,
2017 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2018 this.xPos[1], this.yPos,
2019 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2023 * Update the x position of an indivdual piece of the line.
2024 * @param {number} pos Line position.
2025 * @param {number} increment
2027 updateXPos: function(pos, increment) {
2028 var line1 = pos;
2029 var line2 = pos == 0 ? 1 : 0;
2031 this.xPos[line1] -= increment;
2032 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2034 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2035 this.xPos[line1] += this.dimensions.WIDTH * 2;
2036 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2037 this.sourceXPos[line1] = this.getRandomType();
2042 * Update the horizon line.
2043 * @param {number} deltaTime
2044 * @param {number} speed
2046 update: function(deltaTime, speed) {
2047 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2049 if (this.xPos[0] <= 0) {
2050 this.updateXPos(0, increment);
2051 } else {
2052 this.updateXPos(1, increment);
2054 this.draw();
2058 * Reset horizon to the starting position.
2060 reset: function() {
2061 this.xPos[0] = 0;
2062 this.xPos[1] = HorizonLine.dimensions.WIDTH;
2067 //******************************************************************************
2070 * Horizon background class.
2071 * @param {HTMLCanvasElement} canvas
2072 * @param {Array.<HTMLImageElement>} images
2073 * @param {object} dimensions Canvas dimensions.
2074 * @param {number} gapCoefficient
2075 * @constructor
2077 function Horizon(canvas, images, dimensions, gapCoefficient) {
2078 this.canvas = canvas;
2079 this.canvasCtx = this.canvas.getContext('2d');
2080 this.config = Horizon.config;
2081 this.dimensions = dimensions;
2082 this.gapCoefficient = gapCoefficient;
2083 this.obstacles = [];
2084 this.horizonOffsets = [0, 0];
2085 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2087 // Cloud
2088 this.clouds = [];
2089 this.cloudImg = images.CLOUD;
2090 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2092 // Horizon
2093 this.horizonImg = images.HORIZON;
2094 this.horizonLine = null;
2096 // Obstacles
2097 this.obstacleImgs = {
2098 CACTUS_SMALL: images.CACTUS_SMALL,
2099 CACTUS_LARGE: images.CACTUS_LARGE
2102 this.init();
2107 * Horizon config.
2108 * @enum {number}
2110 Horizon.config = {
2111 BG_CLOUD_SPEED: 0.2,
2112 BUMPY_THRESHOLD: .3,
2113 CLOUD_FREQUENCY: .5,
2114 HORIZON_HEIGHT: 16,
2115 MAX_CLOUDS: 6
2119 Horizon.prototype = {
2121 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2123 init: function() {
2124 this.addCloud();
2125 this.horizonLine = new HorizonLine(this.canvas, this.horizonImg);
2129 * @param {number} deltaTime
2130 * @param {number} currentSpeed
2131 * @param {boolean} updateObstacles Used as an override to prevent
2132 * the obstacles from being updated / added. This happens in the
2133 * ease in section.
2135 update: function(deltaTime, currentSpeed, updateObstacles) {
2136 this.runningTime += deltaTime;
2137 this.horizonLine.update(deltaTime, currentSpeed);
2138 this.updateClouds(deltaTime, currentSpeed);
2140 if (updateObstacles) {
2141 this.updateObstacles(deltaTime, currentSpeed);
2146 * Update the cloud positions.
2147 * @param {number} deltaTime
2148 * @param {number} currentSpeed
2150 updateClouds: function(deltaTime, speed) {
2151 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2152 var numClouds = this.clouds.length;
2154 if (numClouds) {
2155 for (var i = numClouds - 1; i >= 0; i--) {
2156 this.clouds[i].update(cloudSpeed);
2159 var lastCloud = this.clouds[numClouds - 1];
2161 // Check for adding a new cloud.
2162 if (numClouds < this.config.MAX_CLOUDS &&
2163 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2164 this.cloudFrequency > Math.random()) {
2165 this.addCloud();
2168 // Remove expired clouds.
2169 this.clouds = this.clouds.filter(function(obj) {
2170 return !obj.remove;
2176 * Update the obstacle positions.
2177 * @param {number} deltaTime
2178 * @param {number} currentSpeed
2180 updateObstacles: function(deltaTime, currentSpeed) {
2181 // Obstacles, move to Horizon layer.
2182 var updatedObstacles = this.obstacles.slice(0);
2184 for (var i = 0; i < this.obstacles.length; i++) {
2185 var obstacle = this.obstacles[i];
2186 obstacle.update(deltaTime, currentSpeed);
2188 // Clean up existing obstacles.
2189 if (obstacle.remove) {
2190 updatedObstacles.shift();
2193 this.obstacles = updatedObstacles;
2195 if (this.obstacles.length > 0) {
2196 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2198 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2199 lastObstacle.isVisible() &&
2200 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2201 this.dimensions.WIDTH) {
2202 this.addNewObstacle(currentSpeed);
2203 lastObstacle.followingObstacleCreated = true;
2205 } else {
2206 // Create new obstacles.
2207 this.addNewObstacle(currentSpeed);
2212 * Add a new obstacle.
2213 * @param {number} currentSpeed
2215 addNewObstacle: function(currentSpeed) {
2216 var obstacleTypeIndex =
2217 getRandomNum(0, Obstacle.types.length - 1);
2218 var obstacleType = Obstacle.types[obstacleTypeIndex];
2219 var obstacleImg = this.obstacleImgs[obstacleType.type];
2221 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2222 obstacleImg, this.dimensions, this.gapCoefficient, currentSpeed));
2226 * Reset the horizon layer.
2227 * Remove existing obstacles and reposition the horizon line.
2229 reset: function() {
2230 this.obstacles = [];
2231 this.horizonLine.reset();
2235 * Update the canvas width and scaling.
2236 * @param {number} width Canvas width.
2237 * @param {number} height Canvas height.
2239 resize: function(width, height) {
2240 this.canvas.width = width;
2241 this.canvas.height = height;
2245 * Add a new cloud to the horizon.
2247 addCloud: function() {
2248 this.clouds.push(new Cloud(this.canvas, this.cloudImg,
2249 this.dimensions.WIDTH));
2252 })();