Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / renderer / resources / offline.js
blobe16dfa632e82fe28d2379e39d672617803cbb623
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;
20   this.outerContainerEl = document.querySelector(outerContainerId);
21   this.containerEl = null;
22   this.snackbarEl = null;
23   this.detailsButton = this.outerContainerEl.querySelector('#details-button');
25   this.config = opt_config || Runner.config;
27   this.dimensions = Runner.defaultDimensions;
29   this.canvas = null;
30   this.canvasCtx = null;
32   this.tRex = null;
34   this.distanceMeter = null;
35   this.distanceRan = 0;
37   this.highestScore = 0;
39   this.time = 0;
40   this.runningTime = 0;
41   this.msPerFrame = 1000 / FPS;
42   this.currentSpeed = this.config.SPEED;
44   this.obstacles = [];
46   this.started = false;
47   this.activated = false;
48   this.crashed = false;
49   this.paused = false;
51   this.resizeTimerId_ = null;
53   this.playCount = 0;
55   // Sound FX.
56   this.audioBuffer = null;
57   this.soundFx = {};
59   // Global web audio context for playing sounds.
60   this.audioContext = null;
62   // Images.
63   this.images = {};
64   this.imagesLoaded = 0;
66   if (this.isDisabled()) {
67     this.setupDisabledRunner();
68   } else {
69     this.loadImages();
70   }
72 window['Runner'] = Runner;
75 /**
76  * Default game width.
77  * @const
78  */
79 var DEFAULT_WIDTH = 600;
81 /**
82  * Frames per second.
83  * @const
84  */
85 var FPS = 60;
87 /** @const */
88 var IS_HIDPI = window.devicePixelRatio > 1;
90 /** @const */
91 var IS_IOS = window.navigator.userAgent.indexOf('CriOS') > -1 ||
92     window.navigator.userAgent == 'UIWebViewForStaticFileContent';
94 /** @const */
95 var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1 || IS_IOS;
97 /** @const */
98 var IS_TOUCH_ENABLED = 'ontouchstart' in window;
101  * Default game configuration.
102  * @enum {number}
103  */
104 Runner.config = {
105   ACCELERATION: 0.001,
106   BG_CLOUD_SPEED: 0.2,
107   BOTTOM_PAD: 10,
108   CLEAR_TIME: 3000,
109   CLOUD_FREQUENCY: 0.5,
110   GAMEOVER_CLEAR_TIME: 750,
111   GAP_COEFFICIENT: 0.6,
112   GRAVITY: 0.6,
113   INITIAL_JUMP_VELOCITY: 12,
114   MAX_CLOUDS: 6,
115   MAX_OBSTACLE_LENGTH: 3,
116   MAX_OBSTACLE_DUPLICATION: 2,
117   MAX_SPEED: 13,
118   MIN_JUMP_HEIGHT: 35,
119   MOBILE_SPEED_COEFFICIENT: 1.2,
120   RESOURCE_TEMPLATE_ID: 'audio-resources',
121   SPEED: 6,
122   SPEED_DROP_COEFFICIENT: 3
127  * Default dimensions.
128  * @enum {string}
129  */
130 Runner.defaultDimensions = {
131   WIDTH: DEFAULT_WIDTH,
132   HEIGHT: 150
137  * CSS class names.
138  * @enum {string}
139  */
140 Runner.classes = {
141   CANVAS: 'runner-canvas',
142   CONTAINER: 'runner-container',
143   CRASHED: 'crashed',
144   ICON: 'icon-offline',
145   SNACKBAR: 'snackbar',
146   SNACKBAR_SHOW: 'snackbar-show',
147   TOUCH_CONTROLLER: 'controller'
152  * Sprite definition layout of the spritesheet.
153  * @enum {Object}
154  */
155 Runner.spriteDefinition = {
156   LDPI: {
157     CACTUS_LARGE: {x: 332, y: 2},
158     CACTUS_SMALL: {x: 228, y: 2},
159     CLOUD: {x: 86, y: 2},
160     HORIZON: {x: 2, y: 54},
161     PTERODACTYL: {x: 134, y: 2},
162     RESTART: {x: 2, y: 2},
163     TEXT_SPRITE: {x: 484, y: 2},
164     TREX: {x: 677, y: 2}
165   },
166   HDPI: {
167     CACTUS_LARGE: {x: 652,y: 2},
168     CACTUS_SMALL: {x: 446,y: 2},
169     CLOUD: {x: 166,y: 2},
170     HORIZON: {x: 2,y: 104},
171     PTERODACTYL: {x: 260,y: 2},
172     RESTART: {x: 2,y: 2},
173     TEXT_SPRITE: {x: 954,y: 2},
174     TREX: {x: 1338,y: 2}
175   }
180  * Sound FX. Reference to the ID of the audio tag on interstitial page.
181  * @enum {string}
182  */
183 Runner.sounds = {
184   BUTTON_PRESS: 'offline-sound-press',
185   HIT: 'offline-sound-hit',
186   SCORE: 'offline-sound-reached'
191  * Key code mapping.
192  * @enum {Object}
193  */
194 Runner.keycodes = {
195   JUMP: {'38': 1, '32': 1},  // Up, spacebar
196   DUCK: {'40': 1},  // Down
197   RESTART: {'13': 1}  // Enter
202  * Runner event names.
203  * @enum {string}
204  */
205 Runner.events = {
206   ANIM_END: 'webkitAnimationEnd',
207   CLICK: 'click',
208   KEYDOWN: 'keydown',
209   KEYUP: 'keyup',
210   MOUSEDOWN: 'mousedown',
211   MOUSEUP: 'mouseup',
212   RESIZE: 'resize',
213   TOUCHEND: 'touchend',
214   TOUCHSTART: 'touchstart',
215   VISIBILITY: 'visibilitychange',
216   BLUR: 'blur',
217   FOCUS: 'focus',
218   LOAD: 'load'
222 Runner.prototype = {
223   /**
224    * Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
225    * @return {boolean}
226    */
227   isDisabled: function() {
228     return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
229   },
231   /**
232    * For disabled instances, set up a snackbar with the disabled message.
233    */
234   setupDisabledRunner: function() {
235     this.containerEl = document.createElement('div');
236     this.containerEl.className = Runner.classes.SNACKBAR;
237     this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
238     this.outerContainerEl.appendChild(this.containerEl);
240     // Show notification when the activation key is pressed.
241     document.addEventListener(Runner.events.KEYDOWN, function(e) {
242       if (Runner.keycodes.JUMP[e.keyCode]) {
243         this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
244         document.querySelector('.icon').classList.add('icon-disabled');
245       }
246     }.bind(this));
247   },
249   /**
250    * Setting individual settings for debugging.
251    * @param {string} setting
252    * @param {*} value
253    */
254   updateConfigSetting: function(setting, value) {
255     if (setting in this.config && value != undefined) {
256       this.config[setting] = value;
258       switch (setting) {
259         case 'GRAVITY':
260         case 'MIN_JUMP_HEIGHT':
261         case 'SPEED_DROP_COEFFICIENT':
262           this.tRex.config[setting] = value;
263           break;
264         case 'INITIAL_JUMP_VELOCITY':
265           this.tRex.setJumpVelocity(value);
266           break;
267         case 'SPEED':
268           this.setSpeed(value);
269           break;
270       }
271     }
272   },
274   /**
275    * Cache the appropriate image sprite from the page and get the sprite sheet
276    * definition.
277    */
278   loadImages: function() {
279     if (IS_HIDPI) {
280       Runner.imageSprite = document.getElementById('offline-resources-2x');
281       this.spriteDef = Runner.spriteDefinition.HDPI;
282     } else {
283       Runner.imageSprite = document.getElementById('offline-resources-1x');
284       this.spriteDef = Runner.spriteDefinition.LDPI;
285     }
287     this.init();
288   },
290   /**
291    * Load and decode base 64 encoded sounds.
292    */
293   loadSounds: function() {
294     if (!IS_IOS) {
295       this.audioContext = new AudioContext();
297       var resourceTemplate =
298           document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
300       for (var sound in Runner.sounds) {
301         var soundSrc =
302             resourceTemplate.getElementById(Runner.sounds[sound]).src;
303         soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
304         var buffer = decodeBase64ToArrayBuffer(soundSrc);
306         // Async, so no guarantee of order in array.
307         this.audioContext.decodeAudioData(buffer, function(index, audioData) {
308             this.soundFx[index] = audioData;
309           }.bind(this, sound));
310       }
311     }
312   },
314   /**
315    * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
316    * @param {number} opt_speed
317    */
318   setSpeed: function(opt_speed) {
319     var speed = opt_speed || this.currentSpeed;
321     // Reduce the speed on smaller mobile screens.
322     if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
323       var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
324           this.config.MOBILE_SPEED_COEFFICIENT;
325       this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
326     } else if (opt_speed) {
327       this.currentSpeed = opt_speed;
328     }
329   },
331   /**
332    * Game initialiser.
333    */
334   init: function() {
335     // Hide the static icon.
336     document.querySelector('.' + Runner.classes.ICON).style.visibility =
337         'hidden';
339     this.adjustDimensions();
340     this.setSpeed();
342     this.containerEl = document.createElement('div');
343     this.containerEl.className = Runner.classes.CONTAINER;
345     // Player canvas container.
346     this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
347         this.dimensions.HEIGHT, Runner.classes.PLAYER);
349     this.canvasCtx = this.canvas.getContext('2d');
350     this.canvasCtx.fillStyle = '#f7f7f7';
351     this.canvasCtx.fill();
352     Runner.updateCanvasScaling(this.canvas);
354     // Horizon contains clouds, obstacles and the ground.
355     this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
356         this.config.GAP_COEFFICIENT);
358     // Distance meter
359     this.distanceMeter = new DistanceMeter(this.canvas,
360           this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
362     // Draw t-rex
363     this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
365     this.outerContainerEl.appendChild(this.containerEl);
367     if (IS_MOBILE) {
368       this.createTouchController();
369     }
371     this.startListening();
372     this.update();
374     window.addEventListener(Runner.events.RESIZE,
375         this.debounceResize.bind(this));
376   },
378   /**
379    * Create the touch controller. A div that covers whole screen.
380    */
381   createTouchController: function() {
382     this.touchController = document.createElement('div');
383     this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
384   },
386   /**
387    * Debounce the resize event.
388    */
389   debounceResize: function() {
390     if (!this.resizeTimerId_) {
391       this.resizeTimerId_ =
392           setInterval(this.adjustDimensions.bind(this), 250);
393     }
394   },
396   /**
397    * Adjust game space dimensions on resize.
398    */
399   adjustDimensions: function() {
400     clearInterval(this.resizeTimerId_);
401     this.resizeTimerId_ = null;
403     var boxStyles = window.getComputedStyle(this.outerContainerEl);
404     var padding = Number(boxStyles.paddingLeft.substr(0,
405         boxStyles.paddingLeft.length - 2));
407     this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
409     // Redraw the elements back onto the canvas.
410     if (this.canvas) {
411       this.canvas.width = this.dimensions.WIDTH;
412       this.canvas.height = this.dimensions.HEIGHT;
414       Runner.updateCanvasScaling(this.canvas);
416       this.distanceMeter.calcXPos(this.dimensions.WIDTH);
417       this.clearCanvas();
418       this.horizon.update(0, 0, true);
419       this.tRex.update(0);
421       // Outer container and distance meter.
422       if (this.activated || this.crashed || this.paused) {
423         this.containerEl.style.width = this.dimensions.WIDTH + 'px';
424         this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
425         this.distanceMeter.update(0, Math.ceil(this.distanceRan));
426         this.stop();
427       } else {
428         this.tRex.draw(0, 0);
429       }
431       // Game over panel.
432       if (this.crashed && this.gameOverPanel) {
433         this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
434         this.gameOverPanel.draw();
435       }
436     }
437   },
439   /**
440    * Play the game intro.
441    * Canvas container width expands out to the full width.
442    */
443   playIntro: function() {
444     if (!this.started && !this.crashed) {
445       this.playingIntro = true;
446       this.tRex.playingIntro = true;
448       // CSS animation definition.
449       var keyframes = '@-webkit-keyframes intro { ' +
450             'from { width:' + Trex.config.WIDTH + 'px }' +
451             'to { width: ' + this.dimensions.WIDTH + 'px }' +
452           '}';
453       document.styleSheets[0].insertRule(keyframes, 0);
455       this.containerEl.addEventListener(Runner.events.ANIM_END,
456           this.startGame.bind(this));
458       this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
459       this.containerEl.style.width = this.dimensions.WIDTH + 'px';
461       if (this.touchController) {
462         this.outerContainerEl.appendChild(this.touchController);
463       }
464       this.activated = true;
465       this.started = true;
466     } else if (this.crashed) {
467       this.restart();
468     }
469   },
472   /**
473    * Update the game status to started.
474    */
475   startGame: function() {
476     this.runningTime = 0;
477     this.playingIntro = false;
478     this.tRex.playingIntro = false;
479     this.containerEl.style.webkitAnimation = '';
480     this.playCount++;
482     // Handle tabbing off the page. Pause the current game.
483     document.addEventListener(Runner.events.VISIBILITY,
484           this.onVisibilityChange.bind(this));
486     window.addEventListener(Runner.events.BLUR,
487           this.onVisibilityChange.bind(this));
489     window.addEventListener(Runner.events.FOCUS,
490           this.onVisibilityChange.bind(this));
491   },
493   clearCanvas: function() {
494     this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
495         this.dimensions.HEIGHT);
496   },
498   /**
499    * Update the game frame.
500    */
501   update: function() {
502     this.drawPending = false;
504     var now = getTimeStamp();
505     var deltaTime = now - (this.time || now);
506     this.time = now;
508     if (this.activated) {
509       this.clearCanvas();
511       if (this.tRex.jumping) {
512         this.tRex.updateJump(deltaTime);
513       }
515       this.runningTime += deltaTime;
516       var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
518       // First jump triggers the intro.
519       if (this.tRex.jumpCount == 1 && !this.playingIntro) {
520         this.playIntro();
521       }
523       // The horizon doesn't move until the intro is over.
524       if (this.playingIntro) {
525         this.horizon.update(0, this.currentSpeed, hasObstacles);
526       } else {
527         deltaTime = !this.started ? 0 : deltaTime;
528         this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
529       }
531       // Check for collisions.
532       var collision = hasObstacles &&
533           checkForCollision(this.horizon.obstacles[0], this.tRex);
535       if (!collision) {
536         this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
538         if (this.currentSpeed < this.config.MAX_SPEED) {
539           this.currentSpeed += this.config.ACCELERATION;
540         }
541       } else {
542         this.gameOver();
543       }
545       var playAcheivementSound = this.distanceMeter.update(deltaTime,
546           Math.ceil(this.distanceRan));
548       if (playAcheivementSound) {
549         this.playSound(this.soundFx.SCORE);
550       }
551     }
553     if (!this.crashed) {
554       this.tRex.update(deltaTime);
555       this.raq();
556     }
557   },
559   /**
560    * Event handler.
561    */
562   handleEvent: function(e) {
563     return (function(evtType, events) {
564       switch (evtType) {
565         case events.KEYDOWN:
566         case events.TOUCHSTART:
567         case events.MOUSEDOWN:
568           this.onKeyDown(e);
569           break;
570         case events.KEYUP:
571         case events.TOUCHEND:
572         case events.MOUSEUP:
573           this.onKeyUp(e);
574           break;
575       }
576     }.bind(this))(e.type, Runner.events);
577   },
579   /**
580    * Bind relevant key / mouse / touch listeners.
581    */
582   startListening: function() {
583     // Keys.
584     document.addEventListener(Runner.events.KEYDOWN, this);
585     document.addEventListener(Runner.events.KEYUP, this);
587     if (IS_MOBILE) {
588       // Mobile only touch devices.
589       this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
590       this.touchController.addEventListener(Runner.events.TOUCHEND, this);
591       this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
592     } else {
593       // Mouse.
594       document.addEventListener(Runner.events.MOUSEDOWN, this);
595       document.addEventListener(Runner.events.MOUSEUP, this);
596     }
597   },
599   /**
600    * Remove all listeners.
601    */
602   stopListening: function() {
603     document.removeEventListener(Runner.events.KEYDOWN, this);
604     document.removeEventListener(Runner.events.KEYUP, this);
606     if (IS_MOBILE) {
607       this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
608       this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
609       this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
610     } else {
611       document.removeEventListener(Runner.events.MOUSEDOWN, this);
612       document.removeEventListener(Runner.events.MOUSEUP, this);
613     }
614   },
616   /**
617    * Process keydown.
618    * @param {Event} e
619    */
620   onKeyDown: function(e) {
621     // Prevent native page scrolling whilst tapping on mobile.
622     if (IS_MOBILE) {
623       e.preventDefault();
624     }
626     if (e.target != this.detailsButton) {
627       if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] ||
628            e.type == Runner.events.TOUCHSTART)) {
629         if (!this.activated) {
630           this.loadSounds();
631           this.activated = true;
632           errorPageController.trackEasterEgg();
633         }
635         if (!this.tRex.jumping && !this.tRex.ducking) {
636           this.playSound(this.soundFx.BUTTON_PRESS);
637           this.tRex.startJump(this.currentSpeed);
638         }
639       }
641       if (this.crashed && e.type == Runner.events.TOUCHSTART &&
642           e.currentTarget == this.containerEl) {
643         this.restart();
644       }
645     }
647     if (this.activated && !this.crashed && Runner.keycodes.DUCK[e.keyCode]) {
648       e.preventDefault();
649       if (this.tRex.jumping) {
650         // Speed drop, activated only when jump key is not pressed.
651         this.tRex.setSpeedDrop();
652       } else if (!this.tRex.jumping && !this.tRex.ducking) {
653         // Duck.
654         this.tRex.setDuck(true);
655       }
656     }
657   },
660   /**
661    * Process key up.
662    * @param {Event} e
663    */
664   onKeyUp: function(e) {
665     var keyCode = String(e.keyCode);
666     var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
667        e.type == Runner.events.TOUCHEND ||
668        e.type == Runner.events.MOUSEDOWN;
670     if (this.isRunning() && isjumpKey) {
671       this.tRex.endJump();
672     } else if (Runner.keycodes.DUCK[keyCode]) {
673       this.tRex.speedDrop = false;
674       this.tRex.setDuck(false);
675     } else if (this.crashed) {
676       // Check that enough time has elapsed before allowing jump key to restart.
677       var deltaTime = getTimeStamp() - this.time;
679       if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
680           (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
681           Runner.keycodes.JUMP[keyCode])) {
682         this.restart();
683       }
684     } else if (this.paused && isjumpKey) {
685       // Reset the jump state
686       this.tRex.reset();
687       this.play();
688     }
689   },
691   /**
692    * Returns whether the event was a left click on canvas.
693    * On Windows right click is registered as a click.
694    * @param {Event} e
695    * @return {boolean}
696    */
697   isLeftClickOnCanvas: function(e) {
698     return e.button != null && e.button < 2 &&
699         e.type == Runner.events.MOUSEUP && e.target == this.canvas;
700   },
702   /**
703    * RequestAnimationFrame wrapper.
704    */
705   raq: function() {
706     if (!this.drawPending) {
707       this.drawPending = true;
708       this.raqId = requestAnimationFrame(this.update.bind(this));
709     }
710   },
712   /**
713    * Whether the game is running.
714    * @return {boolean}
715    */
716   isRunning: function() {
717     return !!this.raqId;
718   },
720   /**
721    * Game over state.
722    */
723   gameOver: function() {
724     this.playSound(this.soundFx.HIT);
725     vibrate(200);
727     this.stop();
728     this.crashed = true;
729     this.distanceMeter.acheivement = false;
731     this.tRex.update(100, Trex.status.CRASHED);
733     // Game over panel.
734     if (!this.gameOverPanel) {
735       this.gameOverPanel = new GameOverPanel(this.canvas,
736           this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
737           this.dimensions);
738     } else {
739       this.gameOverPanel.draw();
740     }
742     // Update the high score.
743     if (this.distanceRan > this.highestScore) {
744       this.highestScore = Math.ceil(this.distanceRan);
745       this.distanceMeter.setHighScore(this.highestScore);
746     }
748     // Reset the time clock.
749     this.time = getTimeStamp();
750   },
752   stop: function() {
753     this.activated = false;
754     this.paused = true;
755     cancelAnimationFrame(this.raqId);
756     this.raqId = 0;
757   },
759   play: function() {
760     if (!this.crashed) {
761       this.activated = true;
762       this.paused = false;
763       this.tRex.update(0, Trex.status.RUNNING);
764       this.time = getTimeStamp();
765       this.update();
766     }
767   },
769   restart: function() {
770     if (!this.raqId) {
771       this.playCount++;
772       this.runningTime = 0;
773       this.activated = true;
774       this.crashed = false;
775       this.distanceRan = 0;
776       this.setSpeed(this.config.SPEED);
778       this.time = getTimeStamp();
779       this.containerEl.classList.remove(Runner.classes.CRASHED);
780       this.clearCanvas();
781       this.distanceMeter.reset(this.highestScore);
782       this.horizon.reset();
783       this.tRex.reset();
784       this.playSound(this.soundFx.BUTTON_PRESS);
786       this.update();
787     }
788   },
790   /**
791    * Pause the game if the tab is not in focus.
792    */
793   onVisibilityChange: function(e) {
794     if (document.hidden || document.webkitHidden || e.type == 'blur') {
795       this.stop();
796     } else if (!this.crashed) {
797       this.tRex.reset();
798       this.play();
799     }
800   },
802   /**
803    * Play a sound.
804    * @param {SoundBuffer} soundBuffer
805    */
806   playSound: function(soundBuffer) {
807     if (soundBuffer) {
808       var sourceNode = this.audioContext.createBufferSource();
809       sourceNode.buffer = soundBuffer;
810       sourceNode.connect(this.audioContext.destination);
811       sourceNode.start(0);
812     }
813   }
818  * Updates the canvas size taking into
819  * account the backing store pixel ratio and
820  * the device pixel ratio.
822  * See article by Paul Lewis:
823  * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
825  * @param {HTMLCanvasElement} canvas
826  * @param {number} opt_width
827  * @param {number} opt_height
828  * @return {boolean} Whether the canvas was scaled.
829  */
830 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
831   var context = canvas.getContext('2d');
833   // Query the various pixel ratios
834   var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
835   var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
836   var ratio = devicePixelRatio / backingStoreRatio;
838   // Upscale the canvas if the two ratios don't match
839   if (devicePixelRatio !== backingStoreRatio) {
840     var oldWidth = opt_width || canvas.width;
841     var oldHeight = opt_height || canvas.height;
843     canvas.width = oldWidth * ratio;
844     canvas.height = oldHeight * ratio;
846     canvas.style.width = oldWidth + 'px';
847     canvas.style.height = oldHeight + 'px';
849     // Scale the context to counter the fact that we've manually scaled
850     // our canvas element.
851     context.scale(ratio, ratio);
852     return true;
853   } else if (devicePixelRatio == 1) {
854     // Reset the canvas width / height. Fixes scaling bug when the page is
855     // zoomed and the devicePixelRatio changes accordingly.
856     canvas.style.width = canvas.width + 'px';
857     canvas.style.height = canvas.height + 'px';
858   }
859   return false;
864  * Get random number.
865  * @param {number} min
866  * @param {number} max
867  * @param {number}
868  */
869 function getRandomNum(min, max) {
870   return Math.floor(Math.random() * (max - min + 1)) + min;
875  * Vibrate on mobile devices.
876  * @param {number} duration Duration of the vibration in milliseconds.
877  */
878 function vibrate(duration) {
879   if (IS_MOBILE && window.navigator.vibrate) {
880     window.navigator.vibrate(duration);
881   }
886  * Create canvas element.
887  * @param {HTMLElement} container Element to append canvas to.
888  * @param {number} width
889  * @param {number} height
890  * @param {string} opt_classname
891  * @return {HTMLCanvasElement}
892  */
893 function createCanvas(container, width, height, opt_classname) {
894   var canvas = document.createElement('canvas');
895   canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
896       opt_classname : Runner.classes.CANVAS;
897   canvas.width = width;
898   canvas.height = height;
899   container.appendChild(canvas);
901   return canvas;
906  * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
907  * @param {string} base64String
908  */
909 function decodeBase64ToArrayBuffer(base64String) {
910   var len = (base64String.length / 4) * 3;
911   var str = atob(base64String);
912   var arrayBuffer = new ArrayBuffer(len);
913   var bytes = new Uint8Array(arrayBuffer);
915   for (var i = 0; i < len; i++) {
916     bytes[i] = str.charCodeAt(i);
917   }
918   return bytes.buffer;
923  * Return the current timestamp.
924  * @return {number}
925  */
926 function getTimeStamp() {
927   return IS_IOS ? new Date().getTime() : performance.now();
931 //******************************************************************************
935  * Game over panel.
936  * @param {!HTMLCanvasElement} canvas
937  * @param {Object} textImgPos
938  * @param {Object} restartImgPos
939  * @param {!Object} dimensions Canvas dimensions.
940  * @constructor
941  */
942 function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
943   this.canvas = canvas;
944   this.canvasCtx = canvas.getContext('2d');
945   this.canvasDimensions = dimensions;
946   this.textImgPos = textImgPos;
947   this.restartImgPos = restartImgPos;
948   this.draw();
953  * Dimensions used in the panel.
954  * @enum {number}
955  */
956 GameOverPanel.dimensions = {
957   TEXT_X: 0,
958   TEXT_Y: 13,
959   TEXT_WIDTH: 191,
960   TEXT_HEIGHT: 11,
961   RESTART_WIDTH: 36,
962   RESTART_HEIGHT: 32
966 GameOverPanel.prototype = {
967   /**
968    * Update the panel dimensions.
969    * @param {number} width New canvas width.
970    * @param {number} opt_height Optional new canvas height.
971    */
972   updateDimensions: function(width, opt_height) {
973     this.canvasDimensions.WIDTH = width;
974     if (opt_height) {
975       this.canvasDimensions.HEIGHT = opt_height;
976     }
977   },
979   /**
980    * Draw the panel.
981    */
982   draw: function() {
983     var dimensions = GameOverPanel.dimensions;
985     var centerX = this.canvasDimensions.WIDTH / 2;
987     // Game over text.
988     var textSourceX = dimensions.TEXT_X;
989     var textSourceY = dimensions.TEXT_Y;
990     var textSourceWidth = dimensions.TEXT_WIDTH;
991     var textSourceHeight = dimensions.TEXT_HEIGHT;
993     var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
994     var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
995     var textTargetWidth = dimensions.TEXT_WIDTH;
996     var textTargetHeight = dimensions.TEXT_HEIGHT;
998     var restartSourceWidth = dimensions.RESTART_WIDTH;
999     var restartSourceHeight = dimensions.RESTART_HEIGHT;
1000     var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
1001     var restartTargetY = this.canvasDimensions.HEIGHT / 2;
1003     if (IS_HIDPI) {
1004       textSourceY *= 2;
1005       textSourceX *= 2;
1006       textSourceWidth *= 2;
1007       textSourceHeight *= 2;
1008       restartSourceWidth *= 2;
1009       restartSourceHeight *= 2;
1010     }
1012     textSourceX += this.textImgPos.x;
1013     textSourceY += this.textImgPos.y;
1015     // Game over text from sprite.
1016     this.canvasCtx.drawImage(Runner.imageSprite,
1017         textSourceX, textSourceY, textSourceWidth, textSourceHeight,
1018         textTargetX, textTargetY, textTargetWidth, textTargetHeight);
1020     // Restart button.
1021     this.canvasCtx.drawImage(Runner.imageSprite,
1022         this.restartImgPos.x, this.restartImgPos.y,
1023         restartSourceWidth, restartSourceHeight,
1024         restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
1025         dimensions.RESTART_HEIGHT);
1026   }
1030 //******************************************************************************
1033  * Check for a collision.
1034  * @param {!Obstacle} obstacle
1035  * @param {!Trex} tRex T-rex object.
1036  * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
1037  *    collision boxes.
1038  * @return {Array<CollisionBox>}
1039  */
1040 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
1041   var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
1043   // Adjustments are made to the bounding box as there is a 1 pixel white
1044   // border around the t-rex and obstacles.
1045   var tRexBox = new CollisionBox(
1046       tRex.xPos + 1,
1047       tRex.yPos + 1,
1048       tRex.config.WIDTH - 2,
1049       tRex.config.HEIGHT - 2);
1051   var obstacleBox = new CollisionBox(
1052       obstacle.xPos + 1,
1053       obstacle.yPos + 1,
1054       obstacle.typeConfig.width * obstacle.size - 2,
1055       obstacle.typeConfig.height - 2);
1057   // Debug outer box
1058   if (opt_canvasCtx) {
1059     drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
1060   }
1062   // Simple outer bounds check.
1063   if (boxCompare(tRexBox, obstacleBox)) {
1064     var collisionBoxes = obstacle.collisionBoxes;
1065     var tRexCollisionBoxes = tRex.ducking ?
1066         Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
1068     // Detailed axis aligned box check.
1069     for (var t = 0; t < tRexCollisionBoxes.length; t++) {
1070       for (var i = 0; i < collisionBoxes.length; i++) {
1071         // Adjust the box to actual positions.
1072         var adjTrexBox =
1073             createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
1074         var adjObstacleBox =
1075             createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
1076         var crashed = boxCompare(adjTrexBox, adjObstacleBox);
1078         // Draw boxes for debug.
1079         if (opt_canvasCtx) {
1080           drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1081         }
1083         if (crashed) {
1084           return [adjTrexBox, adjObstacleBox];
1085         }
1086       }
1087     }
1088   }
1089   return false;
1094  * Adjust the collision box.
1095  * @param {!CollisionBox} box The original box.
1096  * @param {!CollisionBox} adjustment Adjustment box.
1097  * @return {CollisionBox} The adjusted collision box object.
1098  */
1099 function createAdjustedCollisionBox(box, adjustment) {
1100   return new CollisionBox(
1101       box.x + adjustment.x,
1102       box.y + adjustment.y,
1103       box.width,
1104       box.height);
1109  * Draw the collision boxes for debug.
1110  */
1111 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1112   canvasCtx.save();
1113   canvasCtx.strokeStyle = '#f00';
1114   canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
1116   canvasCtx.strokeStyle = '#0f0';
1117   canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1118       obstacleBox.width, obstacleBox.height);
1119   canvasCtx.restore();
1124  * Compare two collision boxes for a collision.
1125  * @param {CollisionBox} tRexBox
1126  * @param {CollisionBox} obstacleBox
1127  * @return {boolean} Whether the boxes intersected.
1128  */
1129 function boxCompare(tRexBox, obstacleBox) {
1130   var crashed = false;
1131   var tRexBoxX = tRexBox.x;
1132   var tRexBoxY = tRexBox.y;
1134   var obstacleBoxX = obstacleBox.x;
1135   var obstacleBoxY = obstacleBox.y;
1137   // Axis-Aligned Bounding Box method.
1138   if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1139       tRexBox.x + tRexBox.width > obstacleBoxX &&
1140       tRexBox.y < obstacleBox.y + obstacleBox.height &&
1141       tRexBox.height + tRexBox.y > obstacleBox.y) {
1142     crashed = true;
1143   }
1145   return crashed;
1149 //******************************************************************************
1152  * Collision box object.
1153  * @param {number} x X position.
1154  * @param {number} y Y Position.
1155  * @param {number} w Width.
1156  * @param {number} h Height.
1157  */
1158 function CollisionBox(x, y, w, h) {
1159   this.x = x;
1160   this.y = y;
1161   this.width = w;
1162   this.height = h;
1166 //******************************************************************************
1169  * Obstacle.
1170  * @param {HTMLCanvasCtx} canvasCtx
1171  * @param {Obstacle.type} type
1172  * @param {Object} spritePos Obstacle position in sprite.
1173  * @param {Object} dimensions
1174  * @param {number} gapCoefficient Mutipler in determining the gap.
1175  * @param {number} speed
1176  */
1177 function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
1178     gapCoefficient, speed) {
1180   this.canvasCtx = canvasCtx;
1181   this.spritePos = spriteImgPos;
1182   this.typeConfig = type;
1183   this.gapCoefficient = gapCoefficient;
1184   this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1185   this.dimensions = dimensions;
1186   this.remove = false;
1187   this.xPos = 0;
1188   this.yPos = 0;
1189   this.width = 0;
1190   this.collisionBoxes = [];
1191   this.gap = 0;
1192   this.speedOffset = 0;
1194   // For animated obstacles.
1195   this.currentFrame = 0;
1196   this.timer = 0;
1198   this.init(speed);
1202  * Coefficient for calculating the maximum gap.
1203  * @const
1204  */
1205 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1208  * Maximum obstacle grouping count.
1209  * @const
1210  */
1211 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1214 Obstacle.prototype = {
1215   /**
1216    * Initialise the DOM for the obstacle.
1217    * @param {number} speed
1218    */
1219   init: function(speed) {
1220     this.cloneCollisionBoxes();
1222     // Only allow sizing if we're at the right speed.
1223     if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1224       this.size = 1;
1225     }
1227     this.width = this.typeConfig.width * this.size;
1228     this.xPos = this.dimensions.WIDTH - this.width;
1230     // Check if obstacle can be positioned at various heights.
1231     if (Array.isArray(this.typeConfig.yPos))  {
1232       var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
1233           this.typeConfig.yPos;
1234       this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
1235     } else {
1236       this.yPos = this.typeConfig.yPos;
1237     }
1239     this.draw();
1241     // Make collision box adjustments,
1242     // Central box is adjusted to the size as one box.
1243     //      ____        ______        ________
1244     //    _|   |-|    _|     |-|    _|       |-|
1245     //   | |<->| |   | |<--->| |   | |<----->| |
1246     //   | | 1 | |   | |  2  | |   | |   3   | |
1247     //   |_|___|_|   |_|_____|_|   |_|_______|_|
1248     //
1249     if (this.size > 1) {
1250       this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1251           this.collisionBoxes[2].width;
1252       this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1253     }
1255     // For obstacles that go at a different speed from the horizon.
1256     if (this.typeConfig.speedOffset) {
1257       this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
1258           -this.typeConfig.speedOffset;
1259     }
1261     this.gap = this.getGap(this.gapCoefficient, speed);
1262   },
1264   /**
1265    * Draw and crop based on size.
1266    */
1267   draw: function() {
1268     var sourceWidth = this.typeConfig.width;
1269     var sourceHeight = this.typeConfig.height;
1271     if (IS_HIDPI) {
1272       sourceWidth = sourceWidth * 2;
1273       sourceHeight = sourceHeight * 2;
1274     }
1276     // X position in sprite.
1277     var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
1278         this.spritePos.x;
1280     // Animation frames.
1281     if (this.currentFrame > 0) {
1282       sourceX += sourceWidth * this.currentFrame;
1283     }
1285     this.canvasCtx.drawImage(Runner.imageSprite,
1286       sourceX, this.spritePos.y,
1287       sourceWidth * this.size, sourceHeight,
1288       this.xPos, this.yPos,
1289       this.typeConfig.width * this.size, this.typeConfig.height);
1290   },
1292   /**
1293    * Obstacle frame update.
1294    * @param {number} deltaTime
1295    * @param {number} speed
1296    */
1297   update: function(deltaTime, speed) {
1298     if (!this.remove) {
1299       if (this.typeConfig.speedOffset) {
1300         speed += this.speedOffset;
1301       }
1302       this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1304       // Update frame
1305       if (this.typeConfig.numFrames) {
1306         this.timer += deltaTime;
1307         if (this.timer >= this.typeConfig.frameRate) {
1308           this.currentFrame =
1309               this.currentFrame == this.typeConfig.numFrames - 1 ?
1310               0 : this.currentFrame + 1;
1311           this.timer = 0;
1312         }
1313       }
1314       this.draw();
1316       if (!this.isVisible()) {
1317         this.remove = true;
1318       }
1319     }
1320   },
1322   /**
1323    * Calculate a random gap size.
1324    * - Minimum gap gets wider as speed increses
1325    * @param {number} gapCoefficient
1326    * @param {number} speed
1327    * @return {number} The gap size.
1328    */
1329   getGap: function(gapCoefficient, speed) {
1330     var minGap = Math.round(this.width * speed +
1331           this.typeConfig.minGap * gapCoefficient);
1332     var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1333     return getRandomNum(minGap, maxGap);
1334   },
1336   /**
1337    * Check if obstacle is visible.
1338    * @return {boolean} Whether the obstacle is in the game area.
1339    */
1340   isVisible: function() {
1341     return this.xPos + this.width > 0;
1342   },
1344   /**
1345    * Make a copy of the collision boxes, since these will change based on
1346    * obstacle type and size.
1347    */
1348   cloneCollisionBoxes: function() {
1349     var collisionBoxes = this.typeConfig.collisionBoxes;
1351     for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1352       this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1353           collisionBoxes[i].y, collisionBoxes[i].width,
1354           collisionBoxes[i].height);
1355     }
1356   }
1361  * Obstacle definitions.
1362  * minGap: minimum pixel space betweeen obstacles.
1363  * multipleSpeed: Speed at which multiples are allowed.
1364  * speedOffset: speed faster / slower than the horizon.
1365  * minSpeed: Minimum speed which the obstacle can make an appearance.
1366  */
1367 Obstacle.types = [
1368   {
1369     type: 'CACTUS_SMALL',
1370     width: 17,
1371     height: 35,
1372     yPos: 105,
1373     multipleSpeed: 4,
1374     minGap: 120,
1375     minSpeed: 0,
1376     collisionBoxes: [
1377       new CollisionBox(0, 7, 5, 27),
1378       new CollisionBox(4, 0, 6, 34),
1379       new CollisionBox(10, 4, 7, 14)
1380     ]
1381   },
1382   {
1383     type: 'CACTUS_LARGE',
1384     width: 25,
1385     height: 50,
1386     yPos: 90,
1387     multipleSpeed: 7,
1388     minGap: 120,
1389     minSpeed: 0,
1390     collisionBoxes: [
1391       new CollisionBox(0, 12, 7, 38),
1392       new CollisionBox(8, 0, 7, 49),
1393       new CollisionBox(13, 10, 10, 38)
1394     ]
1395   },
1396   {
1397     type: 'PTERODACTYL',
1398     width: 46,
1399     height: 40,
1400     yPos: [ 100, 75, 50 ], // Variable height.
1401     yPosMobile: [ 100, 50 ], // Variable height mobile.
1402     multipleSpeed: 999,
1403     minSpeed: 8.5,
1404     minGap: 150,
1405     collisionBoxes: [
1406       new CollisionBox(15, 15, 16, 5),
1407       new CollisionBox(18, 21, 24, 6),
1408       new CollisionBox(2, 14, 4, 3),
1409       new CollisionBox(6, 10, 4, 7),
1410       new CollisionBox(10, 8, 6, 9)
1411     ],
1412     numFrames: 2,
1413     frameRate: 1000/6,
1414     speedOffset: .8
1415   }
1419 //******************************************************************************
1421  * T-rex game character.
1422  * @param {HTMLCanvas} canvas
1423  * @param {Object} spritePos Positioning within image sprite.
1424  * @constructor
1425  */
1426 function Trex(canvas, spritePos) {
1427   this.canvas = canvas;
1428   this.canvasCtx = canvas.getContext('2d');
1429   this.spritePos = spritePos;
1430   this.xPos = 0;
1431   this.yPos = 0;
1432   // Position when on the ground.
1433   this.groundYPos = 0;
1434   this.currentFrame = 0;
1435   this.currentAnimFrames = [];
1436   this.blinkDelay = 0;
1437   this.animStartTime = 0;
1438   this.timer = 0;
1439   this.msPerFrame = 1000 / FPS;
1440   this.config = Trex.config;
1441   // Current status.
1442   this.status = Trex.status.WAITING;
1444   this.jumping = false;
1445   this.ducking = false;
1446   this.jumpVelocity = 0;
1447   this.reachedMinHeight = false;
1448   this.speedDrop = false;
1449   this.jumpCount = 0;
1450   this.jumpspotX = 0;
1452   this.init();
1457  * T-rex player config.
1458  * @enum {number}
1459  */
1460 Trex.config = {
1461   DROP_VELOCITY: -5,
1462   GRAVITY: 0.6,
1463   HEIGHT: 47,
1464   HEIGHT_DUCK: 25,
1465   INIITAL_JUMP_VELOCITY: -10,
1466   INTRO_DURATION: 1500,
1467   MAX_JUMP_HEIGHT: 30,
1468   MIN_JUMP_HEIGHT: 30,
1469   SPEED_DROP_COEFFICIENT: 3,
1470   SPRITE_WIDTH: 262,
1471   START_X_POS: 50,
1472   WIDTH: 44,
1473   WIDTH_DUCK: 59
1478  * Used in collision detection.
1479  * @type {Array<CollisionBox>}
1480  */
1481 Trex.collisionBoxes = {
1482   DUCKING: [
1483     new CollisionBox(1, 18, 55, 25)
1484   ],
1485   RUNNING: [
1486     new CollisionBox(22, 0, 17, 16),
1487     new CollisionBox(1, 18, 30, 9),
1488     new CollisionBox(10, 35, 14, 8),
1489     new CollisionBox(1, 24, 29, 5),
1490     new CollisionBox(5, 30, 21, 4),
1491     new CollisionBox(9, 34, 15, 4)
1492   ]
1497  * Animation states.
1498  * @enum {string}
1499  */
1500 Trex.status = {
1501   CRASHED: 'CRASHED',
1502   DUCKING: 'DUCKING',
1503   JUMPING: 'JUMPING',
1504   RUNNING: 'RUNNING',
1505   WAITING: 'WAITING'
1509  * Blinking coefficient.
1510  * @const
1511  */
1512 Trex.BLINK_TIMING = 7000;
1516  * Animation config for different states.
1517  * @enum {Object}
1518  */
1519 Trex.animFrames = {
1520   WAITING: {
1521     frames: [44, 0],
1522     msPerFrame: 1000 / 3
1523   },
1524   RUNNING: {
1525     frames: [88, 132],
1526     msPerFrame: 1000 / 12
1527   },
1528   CRASHED: {
1529     frames: [220],
1530     msPerFrame: 1000 / 60
1531   },
1532   JUMPING: {
1533     frames: [0],
1534     msPerFrame: 1000 / 60
1535   },
1536   DUCKING: {
1537     frames: [262, 321],
1538     msPerFrame: 1000 / 8
1539   }
1543 Trex.prototype = {
1544   /**
1545    * T-rex player initaliser.
1546    * Sets the t-rex to blink at random intervals.
1547    */
1548   init: function() {
1549     this.blinkDelay = this.setBlinkDelay();
1550     this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1551         Runner.config.BOTTOM_PAD;
1552     this.yPos = this.groundYPos;
1553     this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1555     this.draw(0, 0);
1556     this.update(0, Trex.status.WAITING);
1557   },
1559   /**
1560    * Setter for the jump velocity.
1561    * The approriate drop velocity is also set.
1562    */
1563   setJumpVelocity: function(setting) {
1564     this.config.INIITAL_JUMP_VELOCITY = -setting;
1565     this.config.DROP_VELOCITY = -setting / 2;
1566   },
1568   /**
1569    * Set the animation status.
1570    * @param {!number} deltaTime
1571    * @param {Trex.status} status Optional status to switch to.
1572    */
1573   update: function(deltaTime, opt_status) {
1574     this.timer += deltaTime;
1576     // Update the status.
1577     if (opt_status) {
1578       this.status = opt_status;
1579       this.currentFrame = 0;
1580       this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1581       this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1583       if (opt_status == Trex.status.WAITING) {
1584         this.animStartTime = getTimeStamp();
1585         this.setBlinkDelay();
1586       }
1587     }
1589     // Game intro animation, T-rex moves in from the left.
1590     if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1591       this.xPos += Math.round((this.config.START_X_POS /
1592           this.config.INTRO_DURATION) * deltaTime);
1593     }
1595     if (this.status == Trex.status.WAITING) {
1596       this.blink(getTimeStamp());
1597     } else {
1598       this.draw(this.currentAnimFrames[this.currentFrame], 0);
1599     }
1601     // Update the frame position.
1602     if (this.timer >= this.msPerFrame) {
1603       this.currentFrame = this.currentFrame ==
1604           this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1605       this.timer = 0;
1606     }
1608     // Speed drop becomes duck if the down key is still being pressed.
1609     if (this.speedDrop && this.yPos == this.groundYPos) {
1610       this.speedDrop = false;
1611       this.setDuck(true);
1612     }
1613   },
1615   /**
1616    * Draw the t-rex to a particular position.
1617    * @param {number} x
1618    * @param {number} y
1619    */
1620   draw: function(x, y) {
1621     var sourceX = x;
1622     var sourceY = y;
1623     var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
1624         this.config.WIDTH_DUCK : this.config.WIDTH;
1625     var sourceHeight = this.config.HEIGHT;
1627     if (IS_HIDPI) {
1628       sourceX *= 2;
1629       sourceY *= 2;
1630       sourceWidth *= 2;
1631       sourceHeight *= 2;
1632     }
1634     // Adjustments for sprite sheet position.
1635     sourceX += this.spritePos.x;
1636     sourceY += this.spritePos.y;
1638     // Ducking.
1639     if (this.ducking && this.status != Trex.status.CRASHED) {
1640       this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1641           sourceWidth, sourceHeight,
1642           this.xPos, this.yPos,
1643           this.config.WIDTH_DUCK, this.config.HEIGHT);
1644     } else {
1645       // Crashed whilst ducking. Trex is standing up so needs adjustment.
1646       if (this.ducking && this.status == Trex.status.CRASHED) {
1647         this.xPos++;
1648       }
1649       // Standing / running
1650       this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1651           sourceWidth, sourceHeight,
1652           this.xPos, this.yPos,
1653           this.config.WIDTH, this.config.HEIGHT);
1654     }
1655   },
1657   /**
1658    * Sets a random time for the blink to happen.
1659    */
1660   setBlinkDelay: function() {
1661     this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1662   },
1664   /**
1665    * Make t-rex blink at random intervals.
1666    * @param {number} time Current time in milliseconds.
1667    */
1668   blink: function(time) {
1669     var deltaTime = time - this.animStartTime;
1671     if (deltaTime >= this.blinkDelay) {
1672       this.draw(this.currentAnimFrames[this.currentFrame], 0);
1674       if (this.currentFrame == 1) {
1675         // Set new random delay to blink.
1676         this.setBlinkDelay();
1677         this.animStartTime = time;
1678       }
1679     }
1680   },
1682   /**
1683    * Initialise a jump.
1684    * @param {number} speed
1685    */
1686   startJump: function(speed) {
1687     if (!this.jumping) {
1688       this.update(0, Trex.status.JUMPING);
1689       // Tweak the jump velocity based on the speed.
1690       this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
1691       this.jumping = true;
1692       this.reachedMinHeight = false;
1693       this.speedDrop = false;
1694     }
1695   },
1697   /**
1698    * Jump is complete, falling down.
1699    */
1700   endJump: function() {
1701     if (this.reachedMinHeight &&
1702         this.jumpVelocity < this.config.DROP_VELOCITY) {
1703       this.jumpVelocity = this.config.DROP_VELOCITY;
1704     }
1705   },
1707   /**
1708    * Update frame for a jump.
1709    * @param {number} deltaTime
1710    * @param {number} speed
1711    */
1712   updateJump: function(deltaTime, speed) {
1713     var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1714     var framesElapsed = deltaTime / msPerFrame;
1716     // Speed drop makes Trex fall faster.
1717     if (this.speedDrop) {
1718       this.yPos += Math.round(this.jumpVelocity *
1719           this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1720     } else {
1721       this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1722     }
1724     this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1726     // Minimum height has been reached.
1727     if (this.yPos < this.minJumpHeight || this.speedDrop) {
1728       this.reachedMinHeight = true;
1729     }
1731     // Reached max height
1732     if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1733       this.endJump();
1734     }
1736     // Back down at ground level. Jump completed.
1737     if (this.yPos > this.groundYPos) {
1738       this.reset();
1739       this.jumpCount++;
1740     }
1742     this.update(deltaTime);
1743   },
1745   /**
1746    * Set the speed drop. Immediately cancels the current jump.
1747    */
1748   setSpeedDrop: function() {
1749     this.speedDrop = true;
1750     this.jumpVelocity = 1;
1751   },
1753   /**
1754    * @param {boolean} isDucking.
1755    */
1756   setDuck: function(isDucking) {
1757     if (isDucking && this.status != Trex.status.DUCKING) {
1758       this.update(0, Trex.status.DUCKING);
1759       this.ducking = true;
1760     } else if (this.status == Trex.status.DUCKING) {
1761       this.update(0, Trex.status.RUNNING);
1762       this.ducking = false;
1763     }
1764   },
1766   /**
1767    * Reset the t-rex to running at start of game.
1768    */
1769   reset: function() {
1770     this.yPos = this.groundYPos;
1771     this.jumpVelocity = 0;
1772     this.jumping = false;
1773     this.ducking = false;
1774     this.update(0, Trex.status.RUNNING);
1775     this.midair = false;
1776     this.speedDrop = false;
1777     this.jumpCount = 0;
1778   }
1782 //******************************************************************************
1785  * Handles displaying the distance meter.
1786  * @param {!HTMLCanvasElement} canvas
1787  * @param {Object} spritePos Image position in sprite.
1788  * @param {number} canvasWidth
1789  * @constructor
1790  */
1791 function DistanceMeter(canvas, spritePos, canvasWidth) {
1792   this.canvas = canvas;
1793   this.canvasCtx = canvas.getContext('2d');
1794   this.image = Runner.imageSprite;
1795   this.spritePos = spritePos;
1796   this.x = 0;
1797   this.y = 5;
1799   this.currentDistance = 0;
1800   this.maxScore = 0;
1801   this.highScore = 0;
1802   this.container = null;
1804   this.digits = [];
1805   this.acheivement = false;
1806   this.defaultString = '';
1807   this.flashTimer = 0;
1808   this.flashIterations = 0;
1810   this.config = DistanceMeter.config;
1811   this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
1812   this.init(canvasWidth);
1817  * @enum {number}
1818  */
1819 DistanceMeter.dimensions = {
1820   WIDTH: 10,
1821   HEIGHT: 13,
1822   DEST_WIDTH: 11
1827  * Y positioning of the digits in the sprite sheet.
1828  * X position is always 0.
1829  * @type {Array<number>}
1830  */
1831 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1835  * Distance meter config.
1836  * @enum {number}
1837  */
1838 DistanceMeter.config = {
1839   // Number of digits.
1840   MAX_DISTANCE_UNITS: 5,
1842   // Distance that causes achievement animation.
1843   ACHIEVEMENT_DISTANCE: 100,
1845   // Used for conversion from pixel distance to a scaled unit.
1846   COEFFICIENT: 0.025,
1848   // Flash duration in milliseconds.
1849   FLASH_DURATION: 1000 / 4,
1851   // Flash iterations for achievement animation.
1852   FLASH_ITERATIONS: 3
1856 DistanceMeter.prototype = {
1857   /**
1858    * Initialise the distance meter to '00000'.
1859    * @param {number} width Canvas width in px.
1860    */
1861   init: function(width) {
1862     var maxDistanceStr = '';
1864     this.calcXPos(width);
1865     this.maxScore = this.maxScoreUnits;
1866     for (var i = 0; i < this.maxScoreUnits; i++) {
1867       this.draw(i, 0);
1868       this.defaultString += '0';
1869       maxDistanceStr += '9';
1870     }
1872     this.maxScore = parseInt(maxDistanceStr);
1873   },
1875   /**
1876    * Calculate the xPos in the canvas.
1877    * @param {number} canvasWidth
1878    */
1879   calcXPos: function(canvasWidth) {
1880     this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1881         (this.maxScoreUnits + 1));
1882   },
1884   /**
1885    * Draw a digit to canvas.
1886    * @param {number} digitPos Position of the digit.
1887    * @param {number} value Digit value 0-9.
1888    * @param {boolean} opt_highScore Whether drawing the high score.
1889    */
1890   draw: function(digitPos, value, opt_highScore) {
1891     var sourceWidth = DistanceMeter.dimensions.WIDTH;
1892     var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1893     var sourceX = DistanceMeter.dimensions.WIDTH * value;
1894     var sourceY = 0;
1896     var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1897     var targetY = this.y;
1898     var targetWidth = DistanceMeter.dimensions.WIDTH;
1899     var targetHeight = DistanceMeter.dimensions.HEIGHT;
1901     // For high DPI we 2x source values.
1902     if (IS_HIDPI) {
1903       sourceWidth *= 2;
1904       sourceHeight *= 2;
1905       sourceX *= 2;
1906     }
1908     sourceX += this.spritePos.x;
1909     sourceY += this.spritePos.y;
1911     this.canvasCtx.save();
1913     if (opt_highScore) {
1914       // Left of the current score.
1915       var highScoreX = this.x - (this.maxScoreUnits * 2) *
1916           DistanceMeter.dimensions.WIDTH;
1917       this.canvasCtx.translate(highScoreX, this.y);
1918     } else {
1919       this.canvasCtx.translate(this.x, this.y);
1920     }
1922     this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1923         sourceWidth, sourceHeight,
1924         targetX, targetY,
1925         targetWidth, targetHeight
1926       );
1928     this.canvasCtx.restore();
1929   },
1931   /**
1932    * Covert pixel distance to a 'real' distance.
1933    * @param {number} distance Pixel distance ran.
1934    * @return {number} The 'real' distance ran.
1935    */
1936   getActualDistance: function(distance) {
1937     return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
1938   },
1940   /**
1941    * Update the distance meter.
1942    * @param {number} distance
1943    * @param {number} deltaTime
1944    * @return {boolean} Whether the acheivement sound fx should be played.
1945    */
1946   update: function(deltaTime, distance) {
1947     var paint = true;
1948     var playSound = false;
1950     if (!this.acheivement) {
1951       distance = this.getActualDistance(distance);
1953       // Score has gone beyond the initial digit count.
1954       if (distance > this.maxScore && this.maxScoreUnits ==
1955         this.config.MAX_DISTANCE_UNITS) {
1956         this.maxScoreUnits++;
1957         this.maxScore = parseInt(this.maxScore + '9');
1958       } else {
1959         this.distance = 0;
1960       }
1962       if (distance > 0) {
1963         // Acheivement unlocked
1964         if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1965           // Flash score and play sound.
1966           this.acheivement = true;
1967           this.flashTimer = 0;
1968           playSound = true;
1969         }
1971         // Create a string representation of the distance with leading 0.
1972         var distanceStr = (this.defaultString +
1973             distance).substr(-this.maxScoreUnits);
1974         this.digits = distanceStr.split('');
1975       } else {
1976         this.digits = this.defaultString.split('');
1977       }
1978     } else {
1979       // Control flashing of the score on reaching acheivement.
1980       if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1981         this.flashTimer += deltaTime;
1983         if (this.flashTimer < this.config.FLASH_DURATION) {
1984           paint = false;
1985         } else if (this.flashTimer >
1986             this.config.FLASH_DURATION * 2) {
1987           this.flashTimer = 0;
1988           this.flashIterations++;
1989         }
1990       } else {
1991         this.acheivement = false;
1992         this.flashIterations = 0;
1993         this.flashTimer = 0;
1994       }
1995     }
1997     // Draw the digits if not flashing.
1998     if (paint) {
1999       for (var i = this.digits.length - 1; i >= 0; i--) {
2000         this.draw(i, parseInt(this.digits[i]));
2001       }
2002     }
2004     this.drawHighScore();
2006     return playSound;
2007   },
2009   /**
2010    * Draw the high score.
2011    */
2012   drawHighScore: function() {
2013     this.canvasCtx.save();
2014     this.canvasCtx.globalAlpha = .8;
2015     for (var i = this.highScore.length - 1; i >= 0; i--) {
2016       this.draw(i, parseInt(this.highScore[i], 10), true);
2017     }
2018     this.canvasCtx.restore();
2019   },
2021   /**
2022    * Set the highscore as a array string.
2023    * Position of char in the sprite: H - 10, I - 11.
2024    * @param {number} distance Distance ran in pixels.
2025    */
2026   setHighScore: function(distance) {
2027     distance = this.getActualDistance(distance);
2028     var highScoreStr = (this.defaultString +
2029         distance).substr(-this.maxScoreUnits);
2031     this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
2032   },
2034   /**
2035    * Reset the distance meter back to '00000'.
2036    */
2037   reset: function() {
2038     this.update(0);
2039     this.acheivement = false;
2040   }
2044 //******************************************************************************
2047  * Cloud background item.
2048  * Similar to an obstacle object but without collision boxes.
2049  * @param {HTMLCanvasElement} canvas Canvas element.
2050  * @param {Object} spritePos Position of image in sprite.
2051  * @param {number} containerWidth
2052  */
2053 function Cloud(canvas, spritePos, containerWidth) {
2054   this.canvas = canvas;
2055   this.canvasCtx = this.canvas.getContext('2d');
2056   this.spritePos = spritePos;
2057   this.containerWidth = containerWidth;
2058   this.xPos = containerWidth;
2059   this.yPos = 0;
2060   this.remove = false;
2061   this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
2062       Cloud.config.MAX_CLOUD_GAP);
2064   this.init();
2069  * Cloud object config.
2070  * @enum {number}
2071  */
2072 Cloud.config = {
2073   HEIGHT: 14,
2074   MAX_CLOUD_GAP: 400,
2075   MAX_SKY_LEVEL: 30,
2076   MIN_CLOUD_GAP: 100,
2077   MIN_SKY_LEVEL: 71,
2078   WIDTH: 46
2082 Cloud.prototype = {
2083   /**
2084    * Initialise the cloud. Sets the Cloud height.
2085    */
2086   init: function() {
2087     this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
2088         Cloud.config.MIN_SKY_LEVEL);
2089     this.draw();
2090   },
2092   /**
2093    * Draw the cloud.
2094    */
2095   draw: function() {
2096     this.canvasCtx.save();
2097     var sourceWidth = Cloud.config.WIDTH;
2098     var sourceHeight = Cloud.config.HEIGHT;
2100     if (IS_HIDPI) {
2101       sourceWidth = sourceWidth * 2;
2102       sourceHeight = sourceHeight * 2;
2103     }
2105     this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
2106         this.spritePos.y,
2107         sourceWidth, sourceHeight,
2108         this.xPos, this.yPos,
2109         Cloud.config.WIDTH, Cloud.config.HEIGHT);
2111     this.canvasCtx.restore();
2112   },
2114   /**
2115    * Update the cloud position.
2116    * @param {number} speed
2117    */
2118   update: function(speed) {
2119     if (!this.remove) {
2120       this.xPos -= Math.ceil(speed);
2121       this.draw();
2123       // Mark as removeable if no longer in the canvas.
2124       if (!this.isVisible()) {
2125         this.remove = true;
2126       }
2127     }
2128   },
2130   /**
2131    * Check if the cloud is visible on the stage.
2132    * @return {boolean}
2133    */
2134   isVisible: function() {
2135     return this.xPos + Cloud.config.WIDTH > 0;
2136   }
2140 //******************************************************************************
2143  * Horizon Line.
2144  * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
2145  * @param {HTMLCanvasElement} canvas
2146  * @param {Object} spritePos Horizon position in sprite.
2147  * @constructor
2148  */
2149 function HorizonLine(canvas, spritePos) {
2150   this.spritePos = spritePos;
2151   this.canvas = canvas;
2152   this.canvasCtx = canvas.getContext('2d');
2153   this.sourceDimensions = {};
2154   this.dimensions = HorizonLine.dimensions;
2155   this.sourceXPos = [this.spritePos.x, this.spritePos.x +
2156       this.dimensions.WIDTH];
2157   this.xPos = [];
2158   this.yPos = 0;
2159   this.bumpThreshold = 0.5;
2161   this.setSourceDimensions();
2162   this.draw();
2167  * Horizon line dimensions.
2168  * @enum {number}
2169  */
2170 HorizonLine.dimensions = {
2171   WIDTH: 600,
2172   HEIGHT: 12,
2173   YPOS: 127
2177 HorizonLine.prototype = {
2178   /**
2179    * Set the source dimensions of the horizon line.
2180    */
2181   setSourceDimensions: function() {
2183     for (var dimension in HorizonLine.dimensions) {
2184       if (IS_HIDPI) {
2185         if (dimension != 'YPOS') {
2186           this.sourceDimensions[dimension] =
2187               HorizonLine.dimensions[dimension] * 2;
2188         }
2189       } else {
2190         this.sourceDimensions[dimension] =
2191             HorizonLine.dimensions[dimension];
2192       }
2193       this.dimensions[dimension] = HorizonLine.dimensions[dimension];
2194     }
2196     this.xPos = [0, HorizonLine.dimensions.WIDTH];
2197     this.yPos = HorizonLine.dimensions.YPOS;
2198   },
2200   /**
2201    * Return the crop x position of a type.
2202    */
2203   getRandomType: function() {
2204     return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
2205   },
2207   /**
2208    * Draw the horizon line.
2209    */
2210   draw: function() {
2211     this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
2212         this.spritePos.y,
2213         this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2214         this.xPos[0], this.yPos,
2215         this.dimensions.WIDTH, this.dimensions.HEIGHT);
2217     this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
2218         this.spritePos.y,
2219         this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2220         this.xPos[1], this.yPos,
2221         this.dimensions.WIDTH, this.dimensions.HEIGHT);
2222   },
2224   /**
2225    * Update the x position of an indivdual piece of the line.
2226    * @param {number} pos Line position.
2227    * @param {number} increment
2228    */
2229   updateXPos: function(pos, increment) {
2230     var line1 = pos;
2231     var line2 = pos == 0 ? 1 : 0;
2233     this.xPos[line1] -= increment;
2234     this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2236     if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2237       this.xPos[line1] += this.dimensions.WIDTH * 2;
2238       this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2239       this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
2240     }
2241   },
2243   /**
2244    * Update the horizon line.
2245    * @param {number} deltaTime
2246    * @param {number} speed
2247    */
2248   update: function(deltaTime, speed) {
2249     var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2251     if (this.xPos[0] <= 0) {
2252       this.updateXPos(0, increment);
2253     } else {
2254       this.updateXPos(1, increment);
2255     }
2256     this.draw();
2257   },
2259   /**
2260    * Reset horizon to the starting position.
2261    */
2262   reset: function() {
2263     this.xPos[0] = 0;
2264     this.xPos[1] = HorizonLine.dimensions.WIDTH;
2265   }
2269 //******************************************************************************
2272  * Horizon background class.
2273  * @param {HTMLCanvasElement} canvas
2274  * @param {Object} spritePos Sprite positioning.
2275  * @param {Object} dimensions Canvas dimensions.
2276  * @param {number} gapCoefficient
2277  * @constructor
2278  */
2279 function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
2280   this.canvas = canvas;
2281   this.canvasCtx = this.canvas.getContext('2d');
2282   this.config = Horizon.config;
2283   this.dimensions = dimensions;
2284   this.gapCoefficient = gapCoefficient;
2285   this.obstacles = [];
2286   this.obstacleHistory = [];
2287   this.horizonOffsets = [0, 0];
2288   this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2289   this.spritePos = spritePos;
2291   // Cloud
2292   this.clouds = [];
2293   this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2295   // Horizon
2296   this.horizonLine = null;
2298   this.init();
2303  * Horizon config.
2304  * @enum {number}
2305  */
2306 Horizon.config = {
2307   BG_CLOUD_SPEED: 0.2,
2308   BUMPY_THRESHOLD: .3,
2309   CLOUD_FREQUENCY: .5,
2310   HORIZON_HEIGHT: 16,
2311   MAX_CLOUDS: 6
2315 Horizon.prototype = {
2316   /**
2317    * Initialise the horizon. Just add the line and a cloud. No obstacles.
2318    */
2319   init: function() {
2320     this.addCloud();
2321     this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
2322   },
2324   /**
2325    * @param {number} deltaTime
2326    * @param {number} currentSpeed
2327    * @param {boolean} updateObstacles Used as an override to prevent
2328    *     the obstacles from being updated / added. This happens in the
2329    *     ease in section.
2330    */
2331   update: function(deltaTime, currentSpeed, updateObstacles) {
2332     this.runningTime += deltaTime;
2333     this.horizonLine.update(deltaTime, currentSpeed);
2334     this.updateClouds(deltaTime, currentSpeed);
2336     if (updateObstacles) {
2337       this.updateObstacles(deltaTime, currentSpeed);
2338     }
2339   },
2341   /**
2342    * Update the cloud positions.
2343    * @param {number} deltaTime
2344    * @param {number} currentSpeed
2345    */
2346   updateClouds: function(deltaTime, speed) {
2347     var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2348     var numClouds = this.clouds.length;
2350     if (numClouds) {
2351       for (var i = numClouds - 1; i >= 0; i--) {
2352         this.clouds[i].update(cloudSpeed);
2353       }
2355       var lastCloud = this.clouds[numClouds - 1];
2357       // Check for adding a new cloud.
2358       if (numClouds < this.config.MAX_CLOUDS &&
2359           (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2360           this.cloudFrequency > Math.random()) {
2361         this.addCloud();
2362       }
2364       // Remove expired clouds.
2365       this.clouds = this.clouds.filter(function(obj) {
2366         return !obj.remove;
2367       });
2368     }
2369   },
2371   /**
2372    * Update the obstacle positions.
2373    * @param {number} deltaTime
2374    * @param {number} currentSpeed
2375    */
2376   updateObstacles: function(deltaTime, currentSpeed) {
2377     // Obstacles, move to Horizon layer.
2378     var updatedObstacles = this.obstacles.slice(0);
2380     for (var i = 0; i < this.obstacles.length; i++) {
2381       var obstacle = this.obstacles[i];
2382       obstacle.update(deltaTime, currentSpeed);
2384       // Clean up existing obstacles.
2385       if (obstacle.remove) {
2386         updatedObstacles.shift();
2387       }
2388     }
2389     this.obstacles = updatedObstacles;
2391     if (this.obstacles.length > 0) {
2392       var lastObstacle = this.obstacles[this.obstacles.length - 1];
2394       if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2395           lastObstacle.isVisible() &&
2396           (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2397           this.dimensions.WIDTH) {
2398         this.addNewObstacle(currentSpeed);
2399         lastObstacle.followingObstacleCreated = true;
2400       }
2401     } else {
2402       // Create new obstacles.
2403       this.addNewObstacle(currentSpeed);
2404     }
2405   },
2407   /**
2408    * Add a new obstacle.
2409    * @param {number} currentSpeed
2410    */
2411   addNewObstacle: function(currentSpeed) {
2412     var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
2413     var obstacleType = Obstacle.types[obstacleTypeIndex];
2415     // Check for multiples of the same type of obstacle.
2416     // Also check obstacle is available at current speed.
2417     if (this.duplicateObstacleCheck(obstacleType.type) ||
2418         currentSpeed < obstacleType.minSpeed) {
2419       this.addNewObstacle(currentSpeed);
2420     } else {
2421       var obstacleSpritePos = this.spritePos[obstacleType.type];
2423       this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2424           obstacleSpritePos, this.dimensions,
2425           this.gapCoefficient, currentSpeed));
2427       this.obstacleHistory.unshift(obstacleType.type);
2429       if (this.obstacleHistory.length > 1) {
2430         this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
2431       }
2432     }
2433   },
2435   /**
2436    * Returns whether the previous two obstacles are the same as the next one.
2437    * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
2438    * @return {boolean}
2439    */
2440   duplicateObstacleCheck: function(nextObstacleType) {
2441     var duplicateCount = 0;
2443     for (var i = 0; i < this.obstacleHistory.length; i++) {
2444       duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
2445           duplicateCount + 1 : 0;
2446     }
2447     return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
2448   },
2450   /**
2451    * Reset the horizon layer.
2452    * Remove existing obstacles and reposition the horizon line.
2453    */
2454   reset: function() {
2455     this.obstacles = [];
2456     this.horizonLine.reset();
2457   },
2459   /**
2460    * Update the canvas width and scaling.
2461    * @param {number} width Canvas width.
2462    * @param {number} height Canvas height.
2463    */
2464   resize: function(width, height) {
2465     this.canvas.width = width;
2466     this.canvas.height = height;
2467   },
2469   /**
2470    * Add a new cloud to the horizon.
2471    */
2472   addCloud: function() {
2473     this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
2474         this.dimensions.WIDTH));
2475   }
2477 })();