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