Use multiline attribute to check for IA2_STATE_MULTILINE.
[chromium-blink-merge.git] / chrome / renderer / resources / offline.js
blob5e6c6b348615f8a02f15c4ff5dc4807bf016a6b9
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) {
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     window.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 && e.button < 2 && e.type == Runner.events.MOUSEUP &&
699         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 {
797       this.play();
798     }
799   },
801   /**
802    * Play a sound.
803    * @param {SoundBuffer} soundBuffer
804    */
805   playSound: function(soundBuffer) {
806     if (soundBuffer) {
807       var sourceNode = this.audioContext.createBufferSource();
808       sourceNode.buffer = soundBuffer;
809       sourceNode.connect(this.audioContext.destination);
810       sourceNode.start(0);
811     }
812   }
817  * Updates the canvas size taking into
818  * account the backing store pixel ratio and
819  * the device pixel ratio.
821  * See article by Paul Lewis:
822  * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
824  * @param {HTMLCanvasElement} canvas
825  * @param {number} opt_width
826  * @param {number} opt_height
827  * @return {boolean} Whether the canvas was scaled.
828  */
829 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
830   var context = canvas.getContext('2d');
832   // Query the various pixel ratios
833   var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
834   var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
835   var ratio = devicePixelRatio / backingStoreRatio;
837   // Upscale the canvas if the two ratios don't match
838   if (devicePixelRatio !== backingStoreRatio) {
839     var oldWidth = opt_width || canvas.width;
840     var oldHeight = opt_height || canvas.height;
842     canvas.width = oldWidth * ratio;
843     canvas.height = oldHeight * ratio;
845     canvas.style.width = oldWidth + 'px';
846     canvas.style.height = oldHeight + 'px';
848     // Scale the context to counter the fact that we've manually scaled
849     // our canvas element.
850     context.scale(ratio, ratio);
851     return true;
852   } else if (devicePixelRatio == 1) {
853     // Reset the canvas width / height. Fixes scaling bug when the page is
854     // zoomed and the devicePixelRatio changes accordingly.
855     canvas.style.width = canvas.width + 'px';
856     canvas.style.height = canvas.height + 'px';
857   }
858   return false;
863  * Get random number.
864  * @param {number} min
865  * @param {number} max
866  * @param {number}
867  */
868 function getRandomNum(min, max) {
869   return Math.floor(Math.random() * (max - min + 1)) + min;
874  * Vibrate on mobile devices.
875  * @param {number} duration Duration of the vibration in milliseconds.
876  */
877 function vibrate(duration) {
878   if (IS_MOBILE && window.navigator.vibrate) {
879     window.navigator.vibrate(duration);
880   }
885  * Create canvas element.
886  * @param {HTMLElement} container Element to append canvas to.
887  * @param {number} width
888  * @param {number} height
889  * @param {string} opt_classname
890  * @return {HTMLCanvasElement}
891  */
892 function createCanvas(container, width, height, opt_classname) {
893   var canvas = document.createElement('canvas');
894   canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
895       opt_classname : Runner.classes.CANVAS;
896   canvas.width = width;
897   canvas.height = height;
898   container.appendChild(canvas);
900   return canvas;
905  * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
906  * @param {string} base64String
907  */
908 function decodeBase64ToArrayBuffer(base64String) {
909   var len = (base64String.length / 4) * 3;
910   var str = atob(base64String);
911   var arrayBuffer = new ArrayBuffer(len);
912   var bytes = new Uint8Array(arrayBuffer);
914   for (var i = 0; i < len; i++) {
915     bytes[i] = str.charCodeAt(i);
916   }
917   return bytes.buffer;
922  * Return the current timestamp.
923  * @return {number}
924  */
925 function getTimeStamp() {
926   return IS_IOS ? new Date().getTime() : performance.now();
930 //******************************************************************************
934  * Game over panel.
935  * @param {!HTMLCanvasElement} canvas
936  * @param {Object} textImgPos
937  * @param {Object} restartImgPos
938  * @param {!Object} dimensions Canvas dimensions.
939  * @constructor
940  */
941 function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
942   this.canvas = canvas;
943   this.canvasCtx = canvas.getContext('2d');
944   this.canvasDimensions = dimensions;
945   this.textImgPos = textImgPos;
946   this.restartImgPos = restartImgPos;
947   this.draw();
952  * Dimensions used in the panel.
953  * @enum {number}
954  */
955 GameOverPanel.dimensions = {
956   TEXT_X: 0,
957   TEXT_Y: 13,
958   TEXT_WIDTH: 191,
959   TEXT_HEIGHT: 11,
960   RESTART_WIDTH: 36,
961   RESTART_HEIGHT: 32
965 GameOverPanel.prototype = {
966   /**
967    * Update the panel dimensions.
968    * @param {number} width New canvas width.
969    * @param {number} opt_height Optional new canvas height.
970    */
971   updateDimensions: function(width, opt_height) {
972     this.canvasDimensions.WIDTH = width;
973     if (opt_height) {
974       this.canvasDimensions.HEIGHT = opt_height;
975     }
976   },
978   /**
979    * Draw the panel.
980    */
981   draw: function() {
982     var dimensions = GameOverPanel.dimensions;
984     var centerX = this.canvasDimensions.WIDTH / 2;
986     // Game over text.
987     var textSourceX = dimensions.TEXT_X;
988     var textSourceY = dimensions.TEXT_Y;
989     var textSourceWidth = dimensions.TEXT_WIDTH;
990     var textSourceHeight = dimensions.TEXT_HEIGHT;
992     var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
993     var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
994     var textTargetWidth = dimensions.TEXT_WIDTH;
995     var textTargetHeight = dimensions.TEXT_HEIGHT;
997     var restartSourceWidth = dimensions.RESTART_WIDTH;
998     var restartSourceHeight = dimensions.RESTART_HEIGHT;
999     var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
1000     var restartTargetY = this.canvasDimensions.HEIGHT / 2;
1002     if (IS_HIDPI) {
1003       textSourceY *= 2;
1004       textSourceX *= 2;
1005       textSourceWidth *= 2;
1006       textSourceHeight *= 2;
1007       restartSourceWidth *= 2;
1008       restartSourceHeight *= 2;
1009     }
1011     textSourceX += this.textImgPos.x;
1012     textSourceY += this.textImgPos.y;
1014     // Game over text from sprite.
1015     this.canvasCtx.drawImage(Runner.imageSprite,
1016         textSourceX, textSourceY, textSourceWidth, textSourceHeight,
1017         textTargetX, textTargetY, textTargetWidth, textTargetHeight);
1019     // Restart button.
1020     this.canvasCtx.drawImage(Runner.imageSprite,
1021         this.restartImgPos.x, this.restartImgPos.y,
1022         restartSourceWidth, restartSourceHeight,
1023         restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
1024         dimensions.RESTART_HEIGHT);
1025   }
1029 //******************************************************************************
1032  * Check for a collision.
1033  * @param {!Obstacle} obstacle
1034  * @param {!Trex} tRex T-rex object.
1035  * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
1036  *    collision boxes.
1037  * @return {Array<CollisionBox>}
1038  */
1039 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
1040   var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
1042   // Adjustments are made to the bounding box as there is a 1 pixel white
1043   // border around the t-rex and obstacles.
1044   var tRexBox = new CollisionBox(
1045       tRex.xPos + 1,
1046       tRex.yPos + 1,
1047       tRex.config.WIDTH - 2,
1048       tRex.config.HEIGHT - 2);
1050   var obstacleBox = new CollisionBox(
1051       obstacle.xPos + 1,
1052       obstacle.yPos + 1,
1053       obstacle.typeConfig.width * obstacle.size - 2,
1054       obstacle.typeConfig.height - 2);
1056   // Debug outer box
1057   if (opt_canvasCtx) {
1058     drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
1059   }
1061   // Simple outer bounds check.
1062   if (boxCompare(tRexBox, obstacleBox)) {
1063     var collisionBoxes = obstacle.collisionBoxes;
1064     var tRexCollisionBoxes = tRex.ducking ?
1065         Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
1067     // Detailed axis aligned box check.
1068     for (var t = 0; t < tRexCollisionBoxes.length; t++) {
1069       for (var i = 0; i < collisionBoxes.length; i++) {
1070         // Adjust the box to actual positions.
1071         var adjTrexBox =
1072             createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
1073         var adjObstacleBox =
1074             createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
1075         var crashed = boxCompare(adjTrexBox, adjObstacleBox);
1077         // Draw boxes for debug.
1078         if (opt_canvasCtx) {
1079           drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1080         }
1082         if (crashed) {
1083           return [adjTrexBox, adjObstacleBox];
1084         }
1085       }
1086     }
1087   }
1088   return false;
1093  * Adjust the collision box.
1094  * @param {!CollisionBox} box The original box.
1095  * @param {!CollisionBox} adjustment Adjustment box.
1096  * @return {CollisionBox} The adjusted collision box object.
1097  */
1098 function createAdjustedCollisionBox(box, adjustment) {
1099   return new CollisionBox(
1100       box.x + adjustment.x,
1101       box.y + adjustment.y,
1102       box.width,
1103       box.height);
1108  * Draw the collision boxes for debug.
1109  */
1110 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1111   canvasCtx.save();
1112   canvasCtx.strokeStyle = '#f00';
1113   canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
1115   canvasCtx.strokeStyle = '#0f0';
1116   canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1117       obstacleBox.width, obstacleBox.height);
1118   canvasCtx.restore();
1123  * Compare two collision boxes for a collision.
1124  * @param {CollisionBox} tRexBox
1125  * @param {CollisionBox} obstacleBox
1126  * @return {boolean} Whether the boxes intersected.
1127  */
1128 function boxCompare(tRexBox, obstacleBox) {
1129   var crashed = false;
1130   var tRexBoxX = tRexBox.x;
1131   var tRexBoxY = tRexBox.y;
1133   var obstacleBoxX = obstacleBox.x;
1134   var obstacleBoxY = obstacleBox.y;
1136   // Axis-Aligned Bounding Box method.
1137   if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1138       tRexBox.x + tRexBox.width > obstacleBoxX &&
1139       tRexBox.y < obstacleBox.y + obstacleBox.height &&
1140       tRexBox.height + tRexBox.y > obstacleBox.y) {
1141     crashed = true;
1142   }
1144   return crashed;
1148 //******************************************************************************
1151  * Collision box object.
1152  * @param {number} x X position.
1153  * @param {number} y Y Position.
1154  * @param {number} w Width.
1155  * @param {number} h Height.
1156  */
1157 function CollisionBox(x, y, w, h) {
1158   this.x = x;
1159   this.y = y;
1160   this.width = w;
1161   this.height = h;
1165 //******************************************************************************
1168  * Obstacle.
1169  * @param {HTMLCanvasCtx} canvasCtx
1170  * @param {Obstacle.type} type
1171  * @param {Object} spritePos Obstacle position in sprite.
1172  * @param {Object} dimensions
1173  * @param {number} gapCoefficient Mutipler in determining the gap.
1174  * @param {number} speed
1175  */
1176 function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
1177     gapCoefficient, speed) {
1179   this.canvasCtx = canvasCtx;
1180   this.spritePos = spriteImgPos;
1181   this.typeConfig = type;
1182   this.gapCoefficient = gapCoefficient;
1183   this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1184   this.dimensions = dimensions;
1185   this.remove = false;
1186   this.xPos = 0;
1187   this.yPos = 0;
1188   this.width = 0;
1189   this.collisionBoxes = [];
1190   this.gap = 0;
1191   this.speedOffset = 0;
1193   // For animated obstacles.
1194   this.currentFrame = 0;
1195   this.timer = 0;
1197   this.init(speed);
1201  * Coefficient for calculating the maximum gap.
1202  * @const
1203  */
1204 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1207  * Maximum obstacle grouping count.
1208  * @const
1209  */
1210 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1213 Obstacle.prototype = {
1214   /**
1215    * Initialise the DOM for the obstacle.
1216    * @param {number} speed
1217    */
1218   init: function(speed) {
1219     this.cloneCollisionBoxes();
1221     // Only allow sizing if we're at the right speed.
1222     if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1223       this.size = 1;
1224     }
1226     this.width = this.typeConfig.width * this.size;
1227     this.xPos = this.dimensions.WIDTH - this.width;
1229     // Check if obstacle can be positioned at various heights.
1230     if (Array.isArray(this.typeConfig.yPos))  {
1231       var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
1232           this.typeConfig.yPos;
1233       this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
1234     } else {
1235       this.yPos = this.typeConfig.yPos;
1236     }
1238     this.draw();
1240     // Make collision box adjustments,
1241     // Central box is adjusted to the size as one box.
1242     //      ____        ______        ________
1243     //    _|   |-|    _|     |-|    _|       |-|
1244     //   | |<->| |   | |<--->| |   | |<----->| |
1245     //   | | 1 | |   | |  2  | |   | |   3   | |
1246     //   |_|___|_|   |_|_____|_|   |_|_______|_|
1247     //
1248     if (this.size > 1) {
1249       this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1250           this.collisionBoxes[2].width;
1251       this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1252     }
1254     // For obstacles that go at a different speed from the horizon.
1255     if (this.typeConfig.speedOffset) {
1256       this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
1257           -this.typeConfig.speedOffset;
1258     }
1260     this.gap = this.getGap(this.gapCoefficient, speed);
1261   },
1263   /**
1264    * Draw and crop based on size.
1265    */
1266   draw: function() {
1267     var sourceWidth = this.typeConfig.width;
1268     var sourceHeight = this.typeConfig.height;
1270     if (IS_HIDPI) {
1271       sourceWidth = sourceWidth * 2;
1272       sourceHeight = sourceHeight * 2;
1273     }
1275     // X position in sprite.
1276     var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
1277         this.spritePos.x;
1279     // Animation frames.
1280     if (this.currentFrame > 0) {
1281       sourceX += sourceWidth * this.currentFrame;
1282     }
1284     this.canvasCtx.drawImage(Runner.imageSprite,
1285       sourceX, this.spritePos.y,
1286       sourceWidth * this.size, sourceHeight,
1287       this.xPos, this.yPos,
1288       this.typeConfig.width * this.size, this.typeConfig.height);
1289   },
1291   /**
1292    * Obstacle frame update.
1293    * @param {number} deltaTime
1294    * @param {number} speed
1295    */
1296   update: function(deltaTime, speed) {
1297     if (!this.remove) {
1298       if (this.typeConfig.speedOffset) {
1299         speed += this.speedOffset;
1300       }
1301       this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1303       // Update frame
1304       if (this.typeConfig.numFrames) {
1305         this.timer += deltaTime;
1306         if (this.timer >= this.typeConfig.frameRate) {
1307           this.currentFrame =
1308               this.currentFrame == this.typeConfig.numFrames - 1 ?
1309               0 : this.currentFrame + 1;
1310           this.timer = 0;
1311         }
1312       }
1313       this.draw();
1315       if (!this.isVisible()) {
1316         this.remove = true;
1317       }
1318     }
1319   },
1321   /**
1322    * Calculate a random gap size.
1323    * - Minimum gap gets wider as speed increses
1324    * @param {number} gapCoefficient
1325    * @param {number} speed
1326    * @return {number} The gap size.
1327    */
1328   getGap: function(gapCoefficient, speed) {
1329     var minGap = Math.round(this.width * speed +
1330           this.typeConfig.minGap * gapCoefficient);
1331     var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1332     return getRandomNum(minGap, maxGap);
1333   },
1335   /**
1336    * Check if obstacle is visible.
1337    * @return {boolean} Whether the obstacle is in the game area.
1338    */
1339   isVisible: function() {
1340     return this.xPos + this.width > 0;
1341   },
1343   /**
1344    * Make a copy of the collision boxes, since these will change based on
1345    * obstacle type and size.
1346    */
1347   cloneCollisionBoxes: function() {
1348     var collisionBoxes = this.typeConfig.collisionBoxes;
1350     for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1351       this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1352           collisionBoxes[i].y, collisionBoxes[i].width,
1353           collisionBoxes[i].height);
1354     }
1355   }
1360  * Obstacle definitions.
1361  * minGap: minimum pixel space betweeen obstacles.
1362  * multipleSpeed: Speed at which multiples are allowed.
1363  * speedOffset: speed faster / slower than the horizon.
1364  * minSpeed: Minimum speed which the obstacle can make an appearance.
1365  */
1366 Obstacle.types = [
1367   {
1368     type: 'CACTUS_SMALL',
1369     width: 17,
1370     height: 35,
1371     yPos: 105,
1372     multipleSpeed: 4,
1373     minGap: 120,
1374     minSpeed: 0,
1375     collisionBoxes: [
1376       new CollisionBox(0, 7, 5, 27),
1377       new CollisionBox(4, 0, 6, 34),
1378       new CollisionBox(10, 4, 7, 14)
1379     ]
1380   },
1381   {
1382     type: 'CACTUS_LARGE',
1383     width: 25,
1384     height: 50,
1385     yPos: 90,
1386     multipleSpeed: 7,
1387     minGap: 120,
1388     minSpeed: 0,
1389     collisionBoxes: [
1390       new CollisionBox(0, 12, 7, 38),
1391       new CollisionBox(8, 0, 7, 49),
1392       new CollisionBox(13, 10, 10, 38)
1393     ]
1394   },
1395   {
1396     type: 'PTERODACTYL',
1397     width: 46,
1398     height: 40,
1399     yPos: [ 100, 75, 50 ], // Variable height.
1400     yPosMobile: [ 100, 50 ], // Variable height mobile.
1401     multipleSpeed: 999,
1402     minSpeed: 8.5,
1403     minGap: 150,
1404     collisionBoxes: [
1405       new CollisionBox(15, 15, 16, 5),
1406       new CollisionBox(18, 21, 24, 6),
1407       new CollisionBox(2, 14, 4, 3),
1408       new CollisionBox(6, 10, 4, 7),
1409       new CollisionBox(10, 8, 6, 9)
1410     ],
1411     numFrames: 2,
1412     frameRate: 1000/6,
1413     speedOffset: .8
1414   }
1418 //******************************************************************************
1420  * T-rex game character.
1421  * @param {HTMLCanvas} canvas
1422  * @param {Object} spritePos Positioning within image sprite.
1423  * @constructor
1424  */
1425 function Trex(canvas, spritePos) {
1426   this.canvas = canvas;
1427   this.canvasCtx = canvas.getContext('2d');
1428   this.spritePos = spritePos;
1429   this.xPos = 0;
1430   this.yPos = 0;
1431   // Position when on the ground.
1432   this.groundYPos = 0;
1433   this.currentFrame = 0;
1434   this.currentAnimFrames = [];
1435   this.blinkDelay = 0;
1436   this.animStartTime = 0;
1437   this.timer = 0;
1438   this.msPerFrame = 1000 / FPS;
1439   this.config = Trex.config;
1440   // Current status.
1441   this.status = Trex.status.WAITING;
1443   this.jumping = false;
1444   this.ducking = false;
1445   this.jumpVelocity = 0;
1446   this.reachedMinHeight = false;
1447   this.speedDrop = false;
1448   this.jumpCount = 0;
1449   this.jumpspotX = 0;
1451   this.init();
1456  * T-rex player config.
1457  * @enum {number}
1458  */
1459 Trex.config = {
1460   DROP_VELOCITY: -5,
1461   GRAVITY: 0.6,
1462   HEIGHT: 47,
1463   HEIGHT_DUCK: 25,
1464   INIITAL_JUMP_VELOCITY: -10,
1465   INTRO_DURATION: 1500,
1466   MAX_JUMP_HEIGHT: 30,
1467   MIN_JUMP_HEIGHT: 30,
1468   SPEED_DROP_COEFFICIENT: 3,
1469   SPRITE_WIDTH: 262,
1470   START_X_POS: 50,
1471   WIDTH: 44,
1472   WIDTH_DUCK: 59
1477  * Used in collision detection.
1478  * @type {Array<CollisionBox>}
1479  */
1480 Trex.collisionBoxes = {
1481   DUCKING: [
1482     new CollisionBox(1, 18, 55, 25)
1483   ],
1484   RUNNING: [
1485     new CollisionBox(22, 0, 17, 16),
1486     new CollisionBox(1, 18, 30, 9),
1487     new CollisionBox(10, 35, 14, 8),
1488     new CollisionBox(1, 24, 29, 5),
1489     new CollisionBox(5, 30, 21, 4),
1490     new CollisionBox(9, 34, 15, 4)
1491   ]
1496  * Animation states.
1497  * @enum {string}
1498  */
1499 Trex.status = {
1500   CRASHED: 'CRASHED',
1501   DUCKING: 'DUCKING',
1502   JUMPING: 'JUMPING',
1503   RUNNING: 'RUNNING',
1504   WAITING: 'WAITING'
1508  * Blinking coefficient.
1509  * @const
1510  */
1511 Trex.BLINK_TIMING = 7000;
1515  * Animation config for different states.
1516  * @enum {Object}
1517  */
1518 Trex.animFrames = {
1519   WAITING: {
1520     frames: [44, 0],
1521     msPerFrame: 1000 / 3
1522   },
1523   RUNNING: {
1524     frames: [88, 132],
1525     msPerFrame: 1000 / 12
1526   },
1527   CRASHED: {
1528     frames: [220],
1529     msPerFrame: 1000 / 60
1530   },
1531   JUMPING: {
1532     frames: [0],
1533     msPerFrame: 1000 / 60
1534   },
1535   DUCKING: {
1536     frames: [262, 321],
1537     msPerFrame: 1000 / 8
1538   }
1542 Trex.prototype = {
1543   /**
1544    * T-rex player initaliser.
1545    * Sets the t-rex to blink at random intervals.
1546    */
1547   init: function() {
1548     this.blinkDelay = this.setBlinkDelay();
1549     this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1550         Runner.config.BOTTOM_PAD;
1551     this.yPos = this.groundYPos;
1552     this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1554     this.draw(0, 0);
1555     this.update(0, Trex.status.WAITING);
1556   },
1558   /**
1559    * Setter for the jump velocity.
1560    * The approriate drop velocity is also set.
1561    */
1562   setJumpVelocity: function(setting) {
1563     this.config.INIITAL_JUMP_VELOCITY = -setting;
1564     this.config.DROP_VELOCITY = -setting / 2;
1565   },
1567   /**
1568    * Set the animation status.
1569    * @param {!number} deltaTime
1570    * @param {Trex.status} status Optional status to switch to.
1571    */
1572   update: function(deltaTime, opt_status) {
1573     this.timer += deltaTime;
1575     // Update the status.
1576     if (opt_status) {
1577       this.status = opt_status;
1578       this.currentFrame = 0;
1579       this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1580       this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1582       if (opt_status == Trex.status.WAITING) {
1583         this.animStartTime = getTimeStamp();
1584         this.setBlinkDelay();
1585       }
1586     }
1588     // Game intro animation, T-rex moves in from the left.
1589     if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1590       this.xPos += Math.round((this.config.START_X_POS /
1591           this.config.INTRO_DURATION) * deltaTime);
1592     }
1594     if (this.status == Trex.status.WAITING) {
1595       this.blink(getTimeStamp());
1596     } else {
1597       this.draw(this.currentAnimFrames[this.currentFrame], 0);
1598     }
1600     // Update the frame position.
1601     if (this.timer >= this.msPerFrame) {
1602       this.currentFrame = this.currentFrame ==
1603           this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1604       this.timer = 0;
1605     }
1607     // Speed drop becomes duck if the down key is still being pressed.
1608     if (this.speedDrop && this.yPos == this.groundYPos) {
1609       this.speedDrop = false;
1610       this.setDuck(true);
1611     }
1612   },
1614   /**
1615    * Draw the t-rex to a particular position.
1616    * @param {number} x
1617    * @param {number} y
1618    */
1619   draw: function(x, y) {
1620     var sourceX = x;
1621     var sourceY = y;
1622     var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
1623         this.config.WIDTH_DUCK : this.config.WIDTH;
1624     var sourceHeight = this.config.HEIGHT;
1626     if (IS_HIDPI) {
1627       sourceX *= 2;
1628       sourceY *= 2;
1629       sourceWidth *= 2;
1630       sourceHeight *= 2;
1631     }
1633     // Adjustments for sprite sheet position.
1634     sourceX += this.spritePos.x;
1635     sourceY += this.spritePos.y;
1637     // Ducking.
1638     if (this.ducking && this.status != Trex.status.CRASHED) {
1639       this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1640           sourceWidth, sourceHeight,
1641           this.xPos, this.yPos,
1642           this.config.WIDTH_DUCK, this.config.HEIGHT);
1643     } else {
1644       // Crashed whilst ducking. Trex is standing up so needs adjustment.
1645       if (this.ducking && this.status == Trex.status.CRASHED) {
1646         this.xPos++;
1647       }
1648       // Standing / running
1649       this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1650           sourceWidth, sourceHeight,
1651           this.xPos, this.yPos,
1652           this.config.WIDTH, this.config.HEIGHT);
1653     }
1654   },
1656   /**
1657    * Sets a random time for the blink to happen.
1658    */
1659   setBlinkDelay: function() {
1660     this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1661   },
1663   /**
1664    * Make t-rex blink at random intervals.
1665    * @param {number} time Current time in milliseconds.
1666    */
1667   blink: function(time) {
1668     var deltaTime = time - this.animStartTime;
1670     if (deltaTime >= this.blinkDelay) {
1671       this.draw(this.currentAnimFrames[this.currentFrame], 0);
1673       if (this.currentFrame == 1) {
1674         // Set new random delay to blink.
1675         this.setBlinkDelay();
1676         this.animStartTime = time;
1677       }
1678     }
1679   },
1681   /**
1682    * Initialise a jump.
1683    * @param {number} speed
1684    */
1685   startJump: function(speed) {
1686     if (!this.jumping) {
1687       this.update(0, Trex.status.JUMPING);
1688       // Tweak the jump velocity based on the speed.
1689       this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
1690       this.jumping = true;
1691       this.reachedMinHeight = false;
1692       this.speedDrop = false;
1693     }
1694   },
1696   /**
1697    * Jump is complete, falling down.
1698    */
1699   endJump: function() {
1700     if (this.reachedMinHeight &&
1701         this.jumpVelocity < this.config.DROP_VELOCITY) {
1702       this.jumpVelocity = this.config.DROP_VELOCITY;
1703     }
1704   },
1706   /**
1707    * Update frame for a jump.
1708    * @param {number} deltaTime
1709    * @param {number} speed
1710    */
1711   updateJump: function(deltaTime, speed) {
1712     var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1713     var framesElapsed = deltaTime / msPerFrame;
1715     // Speed drop makes Trex fall faster.
1716     if (this.speedDrop) {
1717       this.yPos += Math.round(this.jumpVelocity *
1718           this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1719     } else {
1720       this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1721     }
1723     this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1725     // Minimum height has been reached.
1726     if (this.yPos < this.minJumpHeight || this.speedDrop) {
1727       this.reachedMinHeight = true;
1728     }
1730     // Reached max height
1731     if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1732       this.endJump();
1733     }
1735     // Back down at ground level. Jump completed.
1736     if (this.yPos > this.groundYPos) {
1737       this.reset();
1738       this.jumpCount++;
1739     }
1741     this.update(deltaTime);
1742   },
1744   /**
1745    * Set the speed drop. Immediately cancels the current jump.
1746    */
1747   setSpeedDrop: function() {
1748     this.speedDrop = true;
1749     this.jumpVelocity = 1;
1750   },
1752   /**
1753    * @param {boolean} isDucking.
1754    */
1755   setDuck: function(isDucking) {
1756     if (isDucking && this.status != Trex.status.DUCKING) {
1757       this.update(0, Trex.status.DUCKING);
1758       this.ducking = true;
1759     } else if (this.status == Trex.status.DUCKING) {
1760       this.update(0, Trex.status.RUNNING);
1761       this.ducking = false;
1762     }
1763   },
1765   /**
1766    * Reset the t-rex to running at start of game.
1767    */
1768   reset: function() {
1769     this.yPos = this.groundYPos;
1770     this.jumpVelocity = 0;
1771     this.jumping = false;
1772     this.ducking = false;
1773     this.update(0, Trex.status.RUNNING);
1774     this.midair = false;
1775     this.speedDrop = false;
1776     this.jumpCount = 0;
1777   }
1781 //******************************************************************************
1784  * Handles displaying the distance meter.
1785  * @param {!HTMLCanvasElement} canvas
1786  * @param {Object} spritePos Image position in sprite.
1787  * @param {number} canvasWidth
1788  * @constructor
1789  */
1790 function DistanceMeter(canvas, spritePos, canvasWidth) {
1791   this.canvas = canvas;
1792   this.canvasCtx = canvas.getContext('2d');
1793   this.image = Runner.imageSprite;
1794   this.spritePos = spritePos;
1795   this.x = 0;
1796   this.y = 5;
1798   this.currentDistance = 0;
1799   this.maxScore = 0;
1800   this.highScore = 0;
1801   this.container = null;
1803   this.digits = [];
1804   this.acheivement = false;
1805   this.defaultString = '';
1806   this.flashTimer = 0;
1807   this.flashIterations = 0;
1809   this.config = DistanceMeter.config;
1810   this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
1811   this.init(canvasWidth);
1816  * @enum {number}
1817  */
1818 DistanceMeter.dimensions = {
1819   WIDTH: 10,
1820   HEIGHT: 13,
1821   DEST_WIDTH: 11
1826  * Y positioning of the digits in the sprite sheet.
1827  * X position is always 0.
1828  * @type {Array<number>}
1829  */
1830 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1834  * Distance meter config.
1835  * @enum {number}
1836  */
1837 DistanceMeter.config = {
1838   // Number of digits.
1839   MAX_DISTANCE_UNITS: 5,
1841   // Distance that causes achievement animation.
1842   ACHIEVEMENT_DISTANCE: 100,
1844   // Used for conversion from pixel distance to a scaled unit.
1845   COEFFICIENT: 0.025,
1847   // Flash duration in milliseconds.
1848   FLASH_DURATION: 1000 / 4,
1850   // Flash iterations for achievement animation.
1851   FLASH_ITERATIONS: 3
1855 DistanceMeter.prototype = {
1856   /**
1857    * Initialise the distance meter to '00000'.
1858    * @param {number} width Canvas width in px.
1859    */
1860   init: function(width) {
1861     var maxDistanceStr = '';
1863     this.calcXPos(width);
1864     this.maxScore = this.maxScoreUnits;
1865     for (var i = 0; i < this.maxScoreUnits; i++) {
1866       this.draw(i, 0);
1867       this.defaultString += '0';
1868       maxDistanceStr += '9';
1869     }
1871     this.maxScore = parseInt(maxDistanceStr);
1872   },
1874   /**
1875    * Calculate the xPos in the canvas.
1876    * @param {number} canvasWidth
1877    */
1878   calcXPos: function(canvasWidth) {
1879     this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1880         (this.maxScoreUnits + 1));
1881   },
1883   /**
1884    * Draw a digit to canvas.
1885    * @param {number} digitPos Position of the digit.
1886    * @param {number} value Digit value 0-9.
1887    * @param {boolean} opt_highScore Whether drawing the high score.
1888    */
1889   draw: function(digitPos, value, opt_highScore) {
1890     var sourceWidth = DistanceMeter.dimensions.WIDTH;
1891     var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1892     var sourceX = DistanceMeter.dimensions.WIDTH * value;
1893     var sourceY = 0;
1895     var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1896     var targetY = this.y;
1897     var targetWidth = DistanceMeter.dimensions.WIDTH;
1898     var targetHeight = DistanceMeter.dimensions.HEIGHT;
1900     // For high DPI we 2x source values.
1901     if (IS_HIDPI) {
1902       sourceWidth *= 2;
1903       sourceHeight *= 2;
1904       sourceX *= 2;
1905     }
1907     sourceX += this.spritePos.x;
1908     sourceY += this.spritePos.y;
1910     this.canvasCtx.save();
1912     if (opt_highScore) {
1913       // Left of the current score.
1914       var highScoreX = this.x - (this.maxScoreUnits * 2) *
1915           DistanceMeter.dimensions.WIDTH;
1916       this.canvasCtx.translate(highScoreX, this.y);
1917     } else {
1918       this.canvasCtx.translate(this.x, this.y);
1919     }
1921     this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1922         sourceWidth, sourceHeight,
1923         targetX, targetY,
1924         targetWidth, targetHeight
1925       );
1927     this.canvasCtx.restore();
1928   },
1930   /**
1931    * Covert pixel distance to a 'real' distance.
1932    * @param {number} distance Pixel distance ran.
1933    * @return {number} The 'real' distance ran.
1934    */
1935   getActualDistance: function(distance) {
1936     return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
1937   },
1939   /**
1940    * Update the distance meter.
1941    * @param {number} distance
1942    * @param {number} deltaTime
1943    * @return {boolean} Whether the acheivement sound fx should be played.
1944    */
1945   update: function(deltaTime, distance) {
1946     var paint = true;
1947     var playSound = false;
1949     if (!this.acheivement) {
1950       distance = this.getActualDistance(distance);
1952       // Score has gone beyond the initial digit count.
1953       if (distance > this.maxScore && this.maxScoreUnits ==
1954         this.config.MAX_DISTANCE_UNITS) {
1955         this.maxScoreUnits++;
1956         this.maxScore = parseInt(this.maxScore + '9');
1957       } else {
1958         this.distance = 0;
1959       }
1961       if (distance > 0) {
1962         // Acheivement unlocked
1963         if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1964           // Flash score and play sound.
1965           this.acheivement = true;
1966           this.flashTimer = 0;
1967           playSound = true;
1968         }
1970         // Create a string representation of the distance with leading 0.
1971         var distanceStr = (this.defaultString +
1972             distance).substr(-this.maxScoreUnits);
1973         this.digits = distanceStr.split('');
1974       } else {
1975         this.digits = this.defaultString.split('');
1976       }
1977     } else {
1978       // Control flashing of the score on reaching acheivement.
1979       if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1980         this.flashTimer += deltaTime;
1982         if (this.flashTimer < this.config.FLASH_DURATION) {
1983           paint = false;
1984         } else if (this.flashTimer >
1985             this.config.FLASH_DURATION * 2) {
1986           this.flashTimer = 0;
1987           this.flashIterations++;
1988         }
1989       } else {
1990         this.acheivement = false;
1991         this.flashIterations = 0;
1992         this.flashTimer = 0;
1993       }
1994     }
1996     // Draw the digits if not flashing.
1997     if (paint) {
1998       for (var i = this.digits.length - 1; i >= 0; i--) {
1999         this.draw(i, parseInt(this.digits[i]));
2000       }
2001     }
2003     this.drawHighScore();
2005     return playSound;
2006   },
2008   /**
2009    * Draw the high score.
2010    */
2011   drawHighScore: function() {
2012     this.canvasCtx.save();
2013     this.canvasCtx.globalAlpha = .8;
2014     for (var i = this.highScore.length - 1; i >= 0; i--) {
2015       this.draw(i, parseInt(this.highScore[i], 10), true);
2016     }
2017     this.canvasCtx.restore();
2018   },
2020   /**
2021    * Set the highscore as a array string.
2022    * Position of char in the sprite: H - 10, I - 11.
2023    * @param {number} distance Distance ran in pixels.
2024    */
2025   setHighScore: function(distance) {
2026     distance = this.getActualDistance(distance);
2027     var highScoreStr = (this.defaultString +
2028         distance).substr(-this.maxScoreUnits);
2030     this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
2031   },
2033   /**
2034    * Reset the distance meter back to '00000'.
2035    */
2036   reset: function() {
2037     this.update(0);
2038     this.acheivement = false;
2039   }
2043 //******************************************************************************
2046  * Cloud background item.
2047  * Similar to an obstacle object but without collision boxes.
2048  * @param {HTMLCanvasElement} canvas Canvas element.
2049  * @param {Object} spritePos Position of image in sprite.
2050  * @param {number} containerWidth
2051  */
2052 function Cloud(canvas, spritePos, containerWidth) {
2053   this.canvas = canvas;
2054   this.canvasCtx = this.canvas.getContext('2d');
2055   this.spritePos = spritePos;
2056   this.containerWidth = containerWidth;
2057   this.xPos = containerWidth;
2058   this.yPos = 0;
2059   this.remove = false;
2060   this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
2061       Cloud.config.MAX_CLOUD_GAP);
2063   this.init();
2068  * Cloud object config.
2069  * @enum {number}
2070  */
2071 Cloud.config = {
2072   HEIGHT: 14,
2073   MAX_CLOUD_GAP: 400,
2074   MAX_SKY_LEVEL: 30,
2075   MIN_CLOUD_GAP: 100,
2076   MIN_SKY_LEVEL: 71,
2077   WIDTH: 46
2081 Cloud.prototype = {
2082   /**
2083    * Initialise the cloud. Sets the Cloud height.
2084    */
2085   init: function() {
2086     this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
2087         Cloud.config.MIN_SKY_LEVEL);
2088     this.draw();
2089   },
2091   /**
2092    * Draw the cloud.
2093    */
2094   draw: function() {
2095     this.canvasCtx.save();
2096     var sourceWidth = Cloud.config.WIDTH;
2097     var sourceHeight = Cloud.config.HEIGHT;
2099     if (IS_HIDPI) {
2100       sourceWidth = sourceWidth * 2;
2101       sourceHeight = sourceHeight * 2;
2102     }
2104     this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
2105         this.spritePos.y,
2106         sourceWidth, sourceHeight,
2107         this.xPos, this.yPos,
2108         Cloud.config.WIDTH, Cloud.config.HEIGHT);
2110     this.canvasCtx.restore();
2111   },
2113   /**
2114    * Update the cloud position.
2115    * @param {number} speed
2116    */
2117   update: function(speed) {
2118     if (!this.remove) {
2119       this.xPos -= Math.ceil(speed);
2120       this.draw();
2122       // Mark as removeable if no longer in the canvas.
2123       if (!this.isVisible()) {
2124         this.remove = true;
2125       }
2126     }
2127   },
2129   /**
2130    * Check if the cloud is visible on the stage.
2131    * @return {boolean}
2132    */
2133   isVisible: function() {
2134     return this.xPos + Cloud.config.WIDTH > 0;
2135   }
2139 //******************************************************************************
2142  * Horizon Line.
2143  * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
2144  * @param {HTMLCanvasElement} canvas
2145  * @param {Object} spritePos Horizon position in sprite.
2146  * @constructor
2147  */
2148 function HorizonLine(canvas, spritePos) {
2149   this.spritePos = spritePos;
2150   this.canvas = canvas;
2151   this.canvasCtx = canvas.getContext('2d');
2152   this.sourceDimensions = {};
2153   this.dimensions = HorizonLine.dimensions;
2154   this.sourceXPos = [this.spritePos.x, this.spritePos.x +
2155       this.dimensions.WIDTH];
2156   this.xPos = [];
2157   this.yPos = 0;
2158   this.bumpThreshold = 0.5;
2160   this.setSourceDimensions();
2161   this.draw();
2166  * Horizon line dimensions.
2167  * @enum {number}
2168  */
2169 HorizonLine.dimensions = {
2170   WIDTH: 600,
2171   HEIGHT: 12,
2172   YPOS: 127
2176 HorizonLine.prototype = {
2177   /**
2178    * Set the source dimensions of the horizon line.
2179    */
2180   setSourceDimensions: function() {
2182     for (var dimension in HorizonLine.dimensions) {
2183       if (IS_HIDPI) {
2184         if (dimension != 'YPOS') {
2185           this.sourceDimensions[dimension] =
2186               HorizonLine.dimensions[dimension] * 2;
2187         }
2188       } else {
2189         this.sourceDimensions[dimension] =
2190             HorizonLine.dimensions[dimension];
2191       }
2192       this.dimensions[dimension] = HorizonLine.dimensions[dimension];
2193     }
2195     this.xPos = [0, HorizonLine.dimensions.WIDTH];
2196     this.yPos = HorizonLine.dimensions.YPOS;
2197   },
2199   /**
2200    * Return the crop x position of a type.
2201    */
2202   getRandomType: function() {
2203     return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
2204   },
2206   /**
2207    * Draw the horizon line.
2208    */
2209   draw: function() {
2210     this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
2211         this.spritePos.y,
2212         this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2213         this.xPos[0], this.yPos,
2214         this.dimensions.WIDTH, this.dimensions.HEIGHT);
2216     this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
2217         this.spritePos.y,
2218         this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2219         this.xPos[1], this.yPos,
2220         this.dimensions.WIDTH, this.dimensions.HEIGHT);
2221   },
2223   /**
2224    * Update the x position of an indivdual piece of the line.
2225    * @param {number} pos Line position.
2226    * @param {number} increment
2227    */
2228   updateXPos: function(pos, increment) {
2229     var line1 = pos;
2230     var line2 = pos == 0 ? 1 : 0;
2232     this.xPos[line1] -= increment;
2233     this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2235     if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2236       this.xPos[line1] += this.dimensions.WIDTH * 2;
2237       this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2238       this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
2239     }
2240   },
2242   /**
2243    * Update the horizon line.
2244    * @param {number} deltaTime
2245    * @param {number} speed
2246    */
2247   update: function(deltaTime, speed) {
2248     var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2250     if (this.xPos[0] <= 0) {
2251       this.updateXPos(0, increment);
2252     } else {
2253       this.updateXPos(1, increment);
2254     }
2255     this.draw();
2256   },
2258   /**
2259    * Reset horizon to the starting position.
2260    */
2261   reset: function() {
2262     this.xPos[0] = 0;
2263     this.xPos[1] = HorizonLine.dimensions.WIDTH;
2264   }
2268 //******************************************************************************
2271  * Horizon background class.
2272  * @param {HTMLCanvasElement} canvas
2273  * @param {Object} spritePos Sprite positioning.
2274  * @param {Object} dimensions Canvas dimensions.
2275  * @param {number} gapCoefficient
2276  * @constructor
2277  */
2278 function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
2279   this.canvas = canvas;
2280   this.canvasCtx = this.canvas.getContext('2d');
2281   this.config = Horizon.config;
2282   this.dimensions = dimensions;
2283   this.gapCoefficient = gapCoefficient;
2284   this.obstacles = [];
2285   this.obstacleHistory = [];
2286   this.horizonOffsets = [0, 0];
2287   this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2288   this.spritePos = spritePos;
2290   // Cloud
2291   this.clouds = [];
2292   this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2294   // Horizon
2295   this.horizonLine = null;
2297   this.init();
2302  * Horizon config.
2303  * @enum {number}
2304  */
2305 Horizon.config = {
2306   BG_CLOUD_SPEED: 0.2,
2307   BUMPY_THRESHOLD: .3,
2308   CLOUD_FREQUENCY: .5,
2309   HORIZON_HEIGHT: 16,
2310   MAX_CLOUDS: 6
2314 Horizon.prototype = {
2315   /**
2316    * Initialise the horizon. Just add the line and a cloud. No obstacles.
2317    */
2318   init: function() {
2319     this.addCloud();
2320     this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
2321   },
2323   /**
2324    * @param {number} deltaTime
2325    * @param {number} currentSpeed
2326    * @param {boolean} updateObstacles Used as an override to prevent
2327    *     the obstacles from being updated / added. This happens in the
2328    *     ease in section.
2329    */
2330   update: function(deltaTime, currentSpeed, updateObstacles) {
2331     this.runningTime += deltaTime;
2332     this.horizonLine.update(deltaTime, currentSpeed);
2333     this.updateClouds(deltaTime, currentSpeed);
2335     if (updateObstacles) {
2336       this.updateObstacles(deltaTime, currentSpeed);
2337     }
2338   },
2340   /**
2341    * Update the cloud positions.
2342    * @param {number} deltaTime
2343    * @param {number} currentSpeed
2344    */
2345   updateClouds: function(deltaTime, speed) {
2346     var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2347     var numClouds = this.clouds.length;
2349     if (numClouds) {
2350       for (var i = numClouds - 1; i >= 0; i--) {
2351         this.clouds[i].update(cloudSpeed);
2352       }
2354       var lastCloud = this.clouds[numClouds - 1];
2356       // Check for adding a new cloud.
2357       if (numClouds < this.config.MAX_CLOUDS &&
2358           (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2359           this.cloudFrequency > Math.random()) {
2360         this.addCloud();
2361       }
2363       // Remove expired clouds.
2364       this.clouds = this.clouds.filter(function(obj) {
2365         return !obj.remove;
2366       });
2367     }
2368   },
2370   /**
2371    * Update the obstacle positions.
2372    * @param {number} deltaTime
2373    * @param {number} currentSpeed
2374    */
2375   updateObstacles: function(deltaTime, currentSpeed) {
2376     // Obstacles, move to Horizon layer.
2377     var updatedObstacles = this.obstacles.slice(0);
2379     for (var i = 0; i < this.obstacles.length; i++) {
2380       var obstacle = this.obstacles[i];
2381       obstacle.update(deltaTime, currentSpeed);
2383       // Clean up existing obstacles.
2384       if (obstacle.remove) {
2385         updatedObstacles.shift();
2386       }
2387     }
2388     this.obstacles = updatedObstacles;
2390     if (this.obstacles.length > 0) {
2391       var lastObstacle = this.obstacles[this.obstacles.length - 1];
2393       if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2394           lastObstacle.isVisible() &&
2395           (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2396           this.dimensions.WIDTH) {
2397         this.addNewObstacle(currentSpeed);
2398         lastObstacle.followingObstacleCreated = true;
2399       }
2400     } else {
2401       // Create new obstacles.
2402       this.addNewObstacle(currentSpeed);
2403     }
2404   },
2406   /**
2407    * Add a new obstacle.
2408    * @param {number} currentSpeed
2409    */
2410   addNewObstacle: function(currentSpeed) {
2411     var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
2412     var obstacleType = Obstacle.types[obstacleTypeIndex];
2414     // Check for multiples of the same type of obstacle.
2415     // Also check obstacle is available at current speed.
2416     if (this.duplicateObstacleCheck(obstacleType.type) ||
2417         currentSpeed < obstacleType.minSpeed) {
2418       this.addNewObstacle(currentSpeed);
2419     } else {
2420       var obstacleSpritePos = this.spritePos[obstacleType.type];
2422       this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2423           obstacleSpritePos, this.dimensions,
2424           this.gapCoefficient, currentSpeed));
2426       this.obstacleHistory.unshift(obstacleType.type);
2428       if (this.obstacleHistory.length > 1) {
2429         this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
2430       }
2431     }
2432   },
2434   /**
2435    * Returns whether the previous two obstacles are the same as the next one.
2436    * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
2437    * @return {boolean}
2438    */
2439   duplicateObstacleCheck: function(nextObstacleType) {
2440     var duplicateCount = 0;
2442     for (var i = 0; i < this.obstacleHistory.length; i++) {
2443       duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
2444           duplicateCount + 1 : 0;
2445     }
2446     return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
2447   },
2449   /**
2450    * Reset the horizon layer.
2451    * Remove existing obstacles and reposition the horizon line.
2452    */
2453   reset: function() {
2454     this.obstacles = [];
2455     this.horizonLine.reset();
2456   },
2458   /**
2459    * Update the canvas width and scaling.
2460    * @param {number} width Canvas width.
2461    * @param {number} height Canvas height.
2462    */
2463   resize: function(width, height) {
2464     this.canvas.width = width;
2465     this.canvas.height = height;
2466   },
2468   /**
2469    * Add a new cloud to the horizon.
2470    */
2471   addCloud: function() {
2472     this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
2473         this.dimensions.WIDTH));
2474   }
2476 })();