Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / renderer / resources / offline.js
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 })();