chrome离线小恐龙游戏

来源:互联网 发布:上瘾网络剧第十六集 编辑:程序博客网 时间:2024/06/09 16:10

chrome浏览器在离线的时候会有个小恐龙跑酷的游戏,翻了下源码,2700行不到。
先挖个坑,把所有的源码放进来,有空的话看看具体怎么实现的。

// Copyright (c) 2014 The Chromium Authors. All rights reserved.// Use of this source code is governed by a BSD-style license that can be// found in the LICENSE file.(function() {'use strict';/** * T-Rex runner. * @param {string} outerContainerId Outer containing element id. * @param {Object} opt_config * @constructor * @export */function Runner(outerContainerId, opt_config) {  // Singleton  if (Runner.instance_) {    return Runner.instance_;  }  Runner.instance_ = this;  this.outerContainerEl = document.querySelector(outerContainerId);  this.containerEl = null;  this.snackbarEl = null;  this.config = opt_config || Runner.config;  this.dimensions = Runner.defaultDimensions;  this.canvas = null;  this.canvasCtx = null;  this.tRex = null;  this.distanceMeter = null;  this.distanceRan = 0;  this.highestScore = 0;  this.time = 0;  this.runningTime = 0;  this.msPerFrame = 1000 / FPS;  this.currentSpeed = this.config.SPEED;  this.obstacles = [];  this.activated = false; // Whether the easter egg has been activated.  this.playing = false; // Whether the game is currently in play state.  this.crashed = false;  this.paused = false;  this.inverted = false;  this.invertTimer = 0;  this.resizeTimerId_ = null;  this.playCount = 0;  // Sound FX.  this.audioBuffer = null;  this.soundFx = {};  // Global web audio context for playing sounds.  this.audioContext = null;  // Images.  this.images = {};  this.imagesLoaded = 0;  if (this.isDisabled()) {    this.setupDisabledRunner();  } else {    this.loadImages();  }}window['Runner'] = Runner;/** * Default game width. * @const */var DEFAULT_WIDTH = 600;/** * Frames per second. * @const */var FPS = 60;/** @const */var IS_HIDPI = window.devicePixelRatio > 1;/** @const */var IS_IOS = /iPad|iPhone|iPod/.test(window.navigator.platform);/** @const */var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS;/** @const */var IS_TOUCH_ENABLED = 'ontouchstart' in window;/** * Default game configuration. * @enum {number} */Runner.config = {  ACCELERATION: 0.001,  BG_CLOUD_SPEED: 0.2,  BOTTOM_PAD: 10,  CLEAR_TIME: 3000,  CLOUD_FREQUENCY: 0.5,  GAMEOVER_CLEAR_TIME: 750,  GAP_COEFFICIENT: 0.6,  GRAVITY: 0.6,  INITIAL_JUMP_VELOCITY: 12,  INVERT_FADE_DURATION: 12000,  INVERT_DISTANCE: 700,  MAX_BLINK_COUNT: 3,  MAX_CLOUDS: 6,  MAX_OBSTACLE_LENGTH: 3,  MAX_OBSTACLE_DUPLICATION: 2,  MAX_SPEED: 13,  MIN_JUMP_HEIGHT: 35,  MOBILE_SPEED_COEFFICIENT: 1.2,  RESOURCE_TEMPLATE_ID: 'audio-resources',  SPEED: 6,  SPEED_DROP_COEFFICIENT: 3};/** * Default dimensions. * @enum {string} */Runner.defaultDimensions = {  WIDTH: DEFAULT_WIDTH,  HEIGHT: 150};/** * CSS class names. * @enum {string} */Runner.classes = {  CANVAS: 'runner-canvas',  CONTAINER: 'runner-container',  CRASHED: 'crashed',  ICON: 'icon-offline',  INVERTED: 'inverted',  SNACKBAR: 'snackbar',  SNACKBAR_SHOW: 'snackbar-show',  TOUCH_CONTROLLER: 'controller'};/** * Sprite definition layout of the spritesheet. * @enum {Object} */Runner.spriteDefinition = {  LDPI: {    CACTUS_LARGE: {x: 332, y: 2},    CACTUS_SMALL: {x: 228, y: 2},    CLOUD: {x: 86, y: 2},    HORIZON: {x: 2, y: 54},    MOON: {x: 484, y: 2},    PTERODACTYL: {x: 134, y: 2},    RESTART: {x: 2, y: 2},    TEXT_SPRITE: {x: 655, y: 2},    TREX: {x: 848, y: 2},    STAR: {x: 645, y: 2}  },  HDPI: {    CACTUS_LARGE: {x: 652, y: 2},    CACTUS_SMALL: {x: 446, y: 2},    CLOUD: {x: 166, y: 2},    HORIZON: {x: 2, y: 104},    MOON: {x: 954, y: 2},    PTERODACTYL: {x: 260, y: 2},    RESTART: {x: 2, y: 2},    TEXT_SPRITE: {x: 1294, y: 2},    TREX: {x: 1678, y: 2},    STAR: {x: 1276, y: 2}  }};/** * Sound FX. Reference to the ID of the audio tag on interstitial page. * @enum {string} */Runner.sounds = {  BUTTON_PRESS: 'offline-sound-press',  HIT: 'offline-sound-hit',  SCORE: 'offline-sound-reached'};/** * Key code mapping. * @enum {Object} */Runner.keycodes = {  JUMP: {'38': 1, '32': 1},  // Up, spacebar  DUCK: {'40': 1},  // Down  RESTART: {'13': 1}  // Enter};/** * Runner event names. * @enum {string} */Runner.events = {  ANIM_END: 'webkitAnimationEnd',  CLICK: 'click',  KEYDOWN: 'keydown',  KEYUP: 'keyup',  MOUSEDOWN: 'mousedown',  MOUSEUP: 'mouseup',  RESIZE: 'resize',  TOUCHEND: 'touchend',  TOUCHSTART: 'touchstart',  VISIBILITY: 'visibilitychange',  BLUR: 'blur',  FOCUS: 'focus',  LOAD: 'load'};Runner.prototype = {  /**   * Whether the easter egg has been disabled. CrOS enterprise enrolled devices.   * @return {boolean}   */  isDisabled: function() {    return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');  },  /**   * For disabled instances, set up a snackbar with the disabled message.   */  setupDisabledRunner: function() {    this.containerEl = document.createElement('div');    this.containerEl.className = Runner.classes.SNACKBAR;    this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');    this.outerContainerEl.appendChild(this.containerEl);    // Show notification when the activation key is pressed.    document.addEventListener(Runner.events.KEYDOWN, function(e) {      if (Runner.keycodes.JUMP[e.keyCode]) {        this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);        document.querySelector('.icon').classList.add('icon-disabled');      }    }.bind(this));  },  /**   * Setting individual settings for debugging.   * @param {string} setting   * @param {*} value   */  updateConfigSetting: function(setting, value) {    if (setting in this.config && value != undefined) {      this.config[setting] = value;      switch (setting) {        case 'GRAVITY':        case 'MIN_JUMP_HEIGHT':        case 'SPEED_DROP_COEFFICIENT':          this.tRex.config[setting] = value;          break;        case 'INITIAL_JUMP_VELOCITY':          this.tRex.setJumpVelocity(value);          break;        case 'SPEED':          this.setSpeed(value);          break;      }    }  },  /**   * Cache the appropriate image sprite from the page and get the sprite sheet   * definition.   */  loadImages: function() {    if (IS_HIDPI) {      Runner.imageSprite = document.getElementById('offline-resources-2x');      this.spriteDef = Runner.spriteDefinition.HDPI;    } else {      Runner.imageSprite = document.getElementById('offline-resources-1x');      this.spriteDef = Runner.spriteDefinition.LDPI;    }    if (Runner.imageSprite.complete) {      this.init();    } else {      // If the images are not yet loaded, add a listener.      Runner.imageSprite.addEventListener(Runner.events.LOAD,          this.init.bind(this));    }  },  /**   * Load and decode base 64 encoded sounds.   */  loadSounds: function() {    if (!IS_IOS) {      this.audioContext = new AudioContext();      var resourceTemplate =          document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;      for (var sound in Runner.sounds) {        var soundSrc =            resourceTemplate.getElementById(Runner.sounds[sound]).src;        soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);        var buffer = decodeBase64ToArrayBuffer(soundSrc);        // Async, so no guarantee of order in array.        this.audioContext.decodeAudioData(buffer, function(index, audioData) {            this.soundFx[index] = audioData;          }.bind(this, sound));      }    }  },  /**   * Sets the game speed. Adjust the speed accordingly if on a smaller screen.   * @param {number} opt_speed   */  setSpeed: function(opt_speed) {    var speed = opt_speed || this.currentSpeed;    // Reduce the speed on smaller mobile screens.    if (this.dimensions.WIDTH < DEFAULT_WIDTH) {      var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *          this.config.MOBILE_SPEED_COEFFICIENT;      this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;    } else if (opt_speed) {      this.currentSpeed = opt_speed;    }  },  /**   * Game initialiser.   */  init: function() {    // Hide the static icon.    document.querySelector('.' + Runner.classes.ICON).style.visibility =        'hidden';    this.adjustDimensions();    this.setSpeed();    this.containerEl = document.createElement('div');    this.containerEl.className = Runner.classes.CONTAINER;    // Player canvas container.    this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,        this.dimensions.HEIGHT, Runner.classes.PLAYER);    this.canvasCtx = this.canvas.getContext('2d');    this.canvasCtx.fillStyle = '#f7f7f7';    this.canvasCtx.fill();    Runner.updateCanvasScaling(this.canvas);    // Horizon contains clouds, obstacles and the ground.    this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,        this.config.GAP_COEFFICIENT);    // Distance meter    this.distanceMeter = new DistanceMeter(this.canvas,          this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);    // Draw t-rex    this.tRex = new Trex(this.canvas, this.spriteDef.TREX);    this.outerContainerEl.appendChild(this.containerEl);    if (IS_MOBILE) {      this.createTouchController();    }    this.startListening();    this.update();    window.addEventListener(Runner.events.RESIZE,        this.debounceResize.bind(this));  },  /**   * Create the touch controller. A div that covers whole screen.   */  createTouchController: function() {    this.touchController = document.createElement('div');    this.touchController.className = Runner.classes.TOUCH_CONTROLLER;  },  /**   * Debounce the resize event.   */  debounceResize: function() {    if (!this.resizeTimerId_) {      this.resizeTimerId_ =          setInterval(this.adjustDimensions.bind(this), 250);    }  },  /**   * Adjust game space dimensions on resize.   */  adjustDimensions: function() {    clearInterval(this.resizeTimerId_);    this.resizeTimerId_ = null;    var boxStyles = window.getComputedStyle(this.outerContainerEl);    var padding = Number(boxStyles.paddingLeft.substr(0,        boxStyles.paddingLeft.length - 2));    this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;    // Redraw the elements back onto the canvas.    if (this.canvas) {      this.canvas.width = this.dimensions.WIDTH;      this.canvas.height = this.dimensions.HEIGHT;      Runner.updateCanvasScaling(this.canvas);      this.distanceMeter.calcXPos(this.dimensions.WIDTH);      this.clearCanvas();      this.horizon.update(0, 0, true);      this.tRex.update(0);      // Outer container and distance meter.      if (this.playing || this.crashed || this.paused) {        this.containerEl.style.width = this.dimensions.WIDTH + 'px';        this.containerEl.style.height = this.dimensions.HEIGHT + 'px';        this.distanceMeter.update(0, Math.ceil(this.distanceRan));        this.stop();      } else {        this.tRex.draw(0, 0);      }      // Game over panel.      if (this.crashed && this.gameOverPanel) {        this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);        this.gameOverPanel.draw();      }    }  },  /**   * Play the game intro.   * Canvas container width expands out to the full width.   */  playIntro: function() {    if (!this.activated && !this.crashed) {      this.playingIntro = true;      this.tRex.playingIntro = true;      // CSS animation definition.      var keyframes = '@-webkit-keyframes intro { ' +            'from { width:' + Trex.config.WIDTH + 'px }' +            'to { width: ' + this.dimensions.WIDTH + 'px }' +          '}';      document.styleSheets[0].insertRule(keyframes, 0);      this.containerEl.addEventListener(Runner.events.ANIM_END,          this.startGame.bind(this));      this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';      this.containerEl.style.width = this.dimensions.WIDTH + 'px';      if (this.touchController) {        this.outerContainerEl.appendChild(this.touchController);      }      this.playing = true;      this.activated = true;    } else if (this.crashed) {      this.restart();    }  },  /**   * Update the game status to started.   */  startGame: function() {    this.runningTime = 0;    this.playingIntro = false;    this.tRex.playingIntro = false;    this.containerEl.style.webkitAnimation = '';    this.playCount++;    // Handle tabbing off the page. Pause the current game.    document.addEventListener(Runner.events.VISIBILITY,          this.onVisibilityChange.bind(this));    window.addEventListener(Runner.events.BLUR,          this.onVisibilityChange.bind(this));    window.addEventListener(Runner.events.FOCUS,          this.onVisibilityChange.bind(this));  },  clearCanvas: function() {    this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,        this.dimensions.HEIGHT);  },  /**   * Update the game frame and schedules the next one.   */  update: function() {    this.updatePending = false;    var now = getTimeStamp();    var deltaTime = now - (this.time || now);    this.time = now;    if (this.playing) {      this.clearCanvas();      if (this.tRex.jumping) {        this.tRex.updateJump(deltaTime);      }      this.runningTime += deltaTime;      var hasObstacles = this.runningTime > this.config.CLEAR_TIME;      // First jump triggers the intro.      if (this.tRex.jumpCount == 1 && !this.playingIntro) {        this.playIntro();      }      // The horizon doesn't move until the intro is over.      if (this.playingIntro) {        this.horizon.update(0, this.currentSpeed, hasObstacles);      } else {        deltaTime = !this.activated ? 0 : deltaTime;        this.horizon.update(deltaTime, this.currentSpeed, hasObstacles,            this.inverted);      }      // Check for collisions.      var collision = hasObstacles &&          checkForCollision(this.horizon.obstacles[0], this.tRex);      if (!collision) {        this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;        if (this.currentSpeed < this.config.MAX_SPEED) {          this.currentSpeed += this.config.ACCELERATION;        }      } else {        this.gameOver();      }      var playAchievementSound = this.distanceMeter.update(deltaTime,          Math.ceil(this.distanceRan));      if (playAchievementSound) {        this.playSound(this.soundFx.SCORE);      }      // Night mode.      if (this.invertTimer > this.config.INVERT_FADE_DURATION) {        this.invertTimer = 0;        this.invertTrigger = false;        this.invert();      } else if (this.invertTimer) {        this.invertTimer += deltaTime;      } else {        var actualDistance =            this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));        if (actualDistance > 0) {          this.invertTrigger = !(actualDistance %              this.config.INVERT_DISTANCE);          if (this.invertTrigger && this.invertTimer === 0) {            this.invertTimer += deltaTime;            this.invert();          }        }      }    }    if (this.playing || (!this.activated &&        this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {      this.tRex.update(deltaTime);      this.scheduleNextUpdate();    }  },  /**   * Event handler.   */  handleEvent: function(e) {    return (function(evtType, events) {      switch (evtType) {        case events.KEYDOWN:        case events.TOUCHSTART:        case events.MOUSEDOWN:          this.onKeyDown(e);          break;        case events.KEYUP:        case events.TOUCHEND:        case events.MOUSEUP:          this.onKeyUp(e);          break;      }    }.bind(this))(e.type, Runner.events);  },  /**   * Bind relevant key / mouse / touch listeners.   */  startListening: function() {    // Keys.    document.addEventListener(Runner.events.KEYDOWN, this);    document.addEventListener(Runner.events.KEYUP, this);    if (IS_MOBILE) {      // Mobile only touch devices.      this.touchController.addEventListener(Runner.events.TOUCHSTART, this);      this.touchController.addEventListener(Runner.events.TOUCHEND, this);      this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);    } else {      // Mouse.      document.addEventListener(Runner.events.MOUSEDOWN, this);      document.addEventListener(Runner.events.MOUSEUP, this);    }  },  /**   * Remove all listeners.   */  stopListening: function() {    document.removeEventListener(Runner.events.KEYDOWN, this);    document.removeEventListener(Runner.events.KEYUP, this);    if (IS_MOBILE) {      this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);      this.touchController.removeEventListener(Runner.events.TOUCHEND, this);      this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);    } else {      document.removeEventListener(Runner.events.MOUSEDOWN, this);      document.removeEventListener(Runner.events.MOUSEUP, this);    }  },  /**   * Process keydown.   * @param {Event} e   */  onKeyDown: function(e) {    // Prevent native page scrolling whilst tapping on mobile.    if (IS_MOBILE && this.playing) {      e.preventDefault();    }    if (!this.crashed && !this.paused) {      if (Runner.keycodes.JUMP[e.keyCode] ||          e.type == Runner.events.TOUCHSTART) {        e.preventDefault();        // Starting the game for the first time.        if (!this.playing) {          this.loadSounds();          this.playing = true;          this.update();          if (window.errorPageController) {            errorPageController.trackEasterEgg();          }        }        // Start jump.        if (!this.tRex.jumping && !this.tRex.ducking) {          this.playSound(this.soundFx.BUTTON_PRESS);          this.tRex.startJump(this.currentSpeed);        }      } else if (this.playing && Runner.keycodes.DUCK[e.keyCode]) {        e.preventDefault();        if (this.tRex.jumping) {          // Speed drop, activated only when jump key is not pressed.          this.tRex.setSpeedDrop();        } else if (!this.tRex.jumping && !this.tRex.ducking) {          // Duck.          this.tRex.setDuck(true);        }      }    } else if (this.crashed && e.type == Runner.events.TOUCHSTART &&        e.currentTarget == this.containerEl) {      this.restart();    }  },  /**   * Process key up.   * @param {Event} e   */  onKeyUp: function(e) {    var keyCode = String(e.keyCode);    var isjumpKey = Runner.keycodes.JUMP[keyCode] ||       e.type == Runner.events.TOUCHEND ||       e.type == Runner.events.MOUSEDOWN;    if (this.isRunning() && isjumpKey) {      this.tRex.endJump();    } else if (Runner.keycodes.DUCK[keyCode]) {      this.tRex.speedDrop = false;      this.tRex.setDuck(false);    } else if (this.crashed) {      // Check that enough time has elapsed before allowing jump key to restart.      var deltaTime = getTimeStamp() - this.time;      if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||          (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&          Runner.keycodes.JUMP[keyCode])) {        this.restart();      }    } else if (this.paused && isjumpKey) {      // Reset the jump state      this.tRex.reset();      this.play();    }  },  /**   * Returns whether the event was a left click on canvas.   * On Windows right click is registered as a click.   * @param {Event} e   * @return {boolean}   */  isLeftClickOnCanvas: function(e) {    return e.button != null && e.button < 2 &&        e.type == Runner.events.MOUSEUP && e.target == this.canvas;  },  /**   * RequestAnimationFrame wrapper.   */  scheduleNextUpdate: function() {    if (!this.updatePending) {      this.updatePending = true;      this.raqId = requestAnimationFrame(this.update.bind(this));    }  },  /**   * Whether the game is running.   * @return {boolean}   */  isRunning: function() {    return !!this.raqId;  },  /**   * Game over state.   */  gameOver: function() {    this.playSound(this.soundFx.HIT);    vibrate(200);    this.stop();    this.crashed = true;    this.distanceMeter.acheivement = false;    this.tRex.update(100, Trex.status.CRASHED);    // Game over panel.    if (!this.gameOverPanel) {      this.gameOverPanel = new GameOverPanel(this.canvas,          this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,          this.dimensions);    } else {      this.gameOverPanel.draw();    }    // Update the high score.    if (this.distanceRan > this.highestScore) {      this.highestScore = Math.ceil(this.distanceRan);      this.distanceMeter.setHighScore(this.highestScore);    }    // Reset the time clock.    this.time = getTimeStamp();  },  stop: function() {    this.playing = false;    this.paused = true;    cancelAnimationFrame(this.raqId);    this.raqId = 0;  },  play: function() {    if (!this.crashed) {      this.playing = true;      this.paused = false;      this.tRex.update(0, Trex.status.RUNNING);      this.time = getTimeStamp();      this.update();    }  },  restart: function() {    if (!this.raqId) {      this.playCount++;      this.runningTime = 0;      this.playing = true;      this.paused = false;      this.crashed = false;      this.distanceRan = 0;      this.setSpeed(this.config.SPEED);      this.time = getTimeStamp();      this.containerEl.classList.remove(Runner.classes.CRASHED);      this.clearCanvas();      this.distanceMeter.reset(this.highestScore);      this.horizon.reset();      this.tRex.reset();      this.playSound(this.soundFx.BUTTON_PRESS);      this.invert(true);      this.update();    }  },  /**   * Pause the game if the tab is not in focus.   */  onVisibilityChange: function(e) {    if (document.hidden || document.webkitHidden || e.type == 'blur' ||      document.visibilityState != 'visible') {      this.stop();    } else if (!this.crashed) {      this.tRex.reset();      this.play();    }  },  /**   * Play a sound.   * @param {SoundBuffer} soundBuffer   */  playSound: function(soundBuffer) {    if (soundBuffer) {      var sourceNode = this.audioContext.createBufferSource();      sourceNode.buffer = soundBuffer;      sourceNode.connect(this.audioContext.destination);      sourceNode.start(0);    }  },  /**   * Inverts the current page / canvas colors.   * @param {boolean} Whether to reset colors.   */  invert: function(reset) {    if (reset) {      document.body.classList.toggle(Runner.classes.INVERTED, false);      this.invertTimer = 0;      this.inverted = false;    } else {      this.inverted = document.body.classList.toggle(Runner.classes.INVERTED,          this.invertTrigger);    }  }};/** * Updates the canvas size taking into * account the backing store pixel ratio and * the device pixel ratio. * * See article by Paul Lewis: * http://www.html5rocks.com/en/tutorials/canvas/hidpi/ * * @param {HTMLCanvasElement} canvas * @param {number} opt_width * @param {number} opt_height * @return {boolean} Whether the canvas was scaled. */Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {  var context = canvas.getContext('2d');  // Query the various pixel ratios  var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;  var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;  var ratio = devicePixelRatio / backingStoreRatio;  // Upscale the canvas if the two ratios don't match  if (devicePixelRatio !== backingStoreRatio) {    var oldWidth = opt_width || canvas.width;    var oldHeight = opt_height || canvas.height;    canvas.width = oldWidth * ratio;    canvas.height = oldHeight * ratio;    canvas.style.width = oldWidth + 'px';    canvas.style.height = oldHeight + 'px';    // Scale the context to counter the fact that we've manually scaled    // our canvas element.    context.scale(ratio, ratio);    return true;  } else if (devicePixelRatio == 1) {    // Reset the canvas width / height. Fixes scaling bug when the page is    // zoomed and the devicePixelRatio changes accordingly.    canvas.style.width = canvas.width + 'px';    canvas.style.height = canvas.height + 'px';  }  return false;};/** * Get random number. * @param {number} min * @param {number} max * @param {number} */function getRandomNum(min, max) {  return Math.floor(Math.random() * (max - min + 1)) + min;}/** * Vibrate on mobile devices. * @param {number} duration Duration of the vibration in milliseconds. */function vibrate(duration) {  if (IS_MOBILE && window.navigator.vibrate) {    window.navigator.vibrate(duration);  }}/** * Create canvas element. * @param {HTMLElement} container Element to append canvas to. * @param {number} width * @param {number} height * @param {string} opt_classname * @return {HTMLCanvasElement} */function createCanvas(container, width, height, opt_classname) {  var canvas = document.createElement('canvas');  canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +      opt_classname : Runner.classes.CANVAS;  canvas.width = width;  canvas.height = height;  container.appendChild(canvas);  return canvas;}/** * Decodes the base 64 audio to ArrayBuffer used by Web Audio. * @param {string} base64String */function decodeBase64ToArrayBuffer(base64String) {  var len = (base64String.length / 4) * 3;  var str = atob(base64String);  var arrayBuffer = new ArrayBuffer(len);  var bytes = new Uint8Array(arrayBuffer);  for (var i = 0; i < len; i++) {    bytes[i] = str.charCodeAt(i);  }  return bytes.buffer;}/** * Return the current timestamp. * @return {number} */function getTimeStamp() {  return IS_IOS ? new Date().getTime() : performance.now();}//******************************************************************************/** * Game over panel. * @param {!HTMLCanvasElement} canvas * @param {Object} textImgPos * @param {Object} restartImgPos * @param {!Object} dimensions Canvas dimensions. * @constructor */function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {  this.canvas = canvas;  this.canvasCtx = canvas.getContext('2d');  this.canvasDimensions = dimensions;  this.textImgPos = textImgPos;  this.restartImgPos = restartImgPos;  this.draw();};/** * Dimensions used in the panel. * @enum {number} */GameOverPanel.dimensions = {  TEXT_X: 0,  TEXT_Y: 13,  TEXT_WIDTH: 191,  TEXT_HEIGHT: 11,  RESTART_WIDTH: 36,  RESTART_HEIGHT: 32};GameOverPanel.prototype = {  /**   * Update the panel dimensions.   * @param {number} width New canvas width.   * @param {number} opt_height Optional new canvas height.   */  updateDimensions: function(width, opt_height) {    this.canvasDimensions.WIDTH = width;    if (opt_height) {      this.canvasDimensions.HEIGHT = opt_height;    }  },  /**   * Draw the panel.   */  draw: function() {    var dimensions = GameOverPanel.dimensions;    var centerX = this.canvasDimensions.WIDTH / 2;    // Game over text.    var textSourceX = dimensions.TEXT_X;    var textSourceY = dimensions.TEXT_Y;    var textSourceWidth = dimensions.TEXT_WIDTH;    var textSourceHeight = dimensions.TEXT_HEIGHT;    var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));    var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);    var textTargetWidth = dimensions.TEXT_WIDTH;    var textTargetHeight = dimensions.TEXT_HEIGHT;    var restartSourceWidth = dimensions.RESTART_WIDTH;    var restartSourceHeight = dimensions.RESTART_HEIGHT;    var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);    var restartTargetY = this.canvasDimensions.HEIGHT / 2;    if (IS_HIDPI) {      textSourceY *= 2;      textSourceX *= 2;      textSourceWidth *= 2;      textSourceHeight *= 2;      restartSourceWidth *= 2;      restartSourceHeight *= 2;    }    textSourceX += this.textImgPos.x;    textSourceY += this.textImgPos.y;    // Game over text from sprite.    this.canvasCtx.drawImage(Runner.imageSprite,        textSourceX, textSourceY, textSourceWidth, textSourceHeight,        textTargetX, textTargetY, textTargetWidth, textTargetHeight);    // Restart button.    this.canvasCtx.drawImage(Runner.imageSprite,        this.restartImgPos.x, this.restartImgPos.y,        restartSourceWidth, restartSourceHeight,        restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,        dimensions.RESTART_HEIGHT);  }};//******************************************************************************/** * Check for a collision. * @param {!Obstacle} obstacle * @param {!Trex} tRex T-rex object. * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing *    collision boxes. * @return {Array<CollisionBox>} */function checkForCollision(obstacle, tRex, opt_canvasCtx) {  var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;  // Adjustments are made to the bounding box as there is a 1 pixel white  // border around the t-rex and obstacles.  var tRexBox = new CollisionBox(      tRex.xPos + 1,      tRex.yPos + 1,      tRex.config.WIDTH - 2,      tRex.config.HEIGHT - 2);  var obstacleBox = new CollisionBox(      obstacle.xPos + 1,      obstacle.yPos + 1,      obstacle.typeConfig.width * obstacle.size - 2,      obstacle.typeConfig.height - 2);  // Debug outer box  if (opt_canvasCtx) {    drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);  }  // Simple outer bounds check.  if (boxCompare(tRexBox, obstacleBox)) {    var collisionBoxes = obstacle.collisionBoxes;    var tRexCollisionBoxes = tRex.ducking ?        Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;    // Detailed axis aligned box check.    for (var t = 0; t < tRexCollisionBoxes.length; t++) {      for (var i = 0; i < collisionBoxes.length; i++) {        // Adjust the box to actual positions.        var adjTrexBox =            createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);        var adjObstacleBox =            createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);        var crashed = boxCompare(adjTrexBox, adjObstacleBox);        // Draw boxes for debug.        if (opt_canvasCtx) {          drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);        }        if (crashed) {          return [adjTrexBox, adjObstacleBox];        }      }    }  }  return false;};/** * Adjust the collision box. * @param {!CollisionBox} box The original box. * @param {!CollisionBox} adjustment Adjustment box. * @return {CollisionBox} The adjusted collision box object. */function createAdjustedCollisionBox(box, adjustment) {  return new CollisionBox(      box.x + adjustment.x,      box.y + adjustment.y,      box.width,      box.height);};/** * Draw the collision boxes for debug. */function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {  canvasCtx.save();  canvasCtx.strokeStyle = '#f00';  canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);  canvasCtx.strokeStyle = '#0f0';  canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,      obstacleBox.width, obstacleBox.height);  canvasCtx.restore();};/** * Compare two collision boxes for a collision. * @param {CollisionBox} tRexBox * @param {CollisionBox} obstacleBox * @return {boolean} Whether the boxes intersected. */function boxCompare(tRexBox, obstacleBox) {  var crashed = false;  var tRexBoxX = tRexBox.x;  var tRexBoxY = tRexBox.y;  var obstacleBoxX = obstacleBox.x;  var obstacleBoxY = obstacleBox.y;  // Axis-Aligned Bounding Box method.  if (tRexBox.x < obstacleBoxX + obstacleBox.width &&      tRexBox.x + tRexBox.width > obstacleBoxX &&      tRexBox.y < obstacleBox.y + obstacleBox.height &&      tRexBox.height + tRexBox.y > obstacleBox.y) {    crashed = true;  }  return crashed;};//******************************************************************************/** * Collision box object. * @param {number} x X position. * @param {number} y Y Position. * @param {number} w Width. * @param {number} h Height. */function CollisionBox(x, y, w, h) {  this.x = x;  this.y = y;  this.width = w;  this.height = h;};//******************************************************************************/** * Obstacle. * @param {HTMLCanvasCtx} canvasCtx * @param {Obstacle.type} type * @param {Object} spritePos Obstacle position in sprite. * @param {Object} dimensions * @param {number} gapCoefficient Mutipler in determining the gap. * @param {number} speed * @param {number} opt_xOffset */function Obstacle(canvasCtx, type, spriteImgPos, dimensions,    gapCoefficient, speed, opt_xOffset) {  this.canvasCtx = canvasCtx;  this.spritePos = spriteImgPos;  this.typeConfig = type;  this.gapCoefficient = gapCoefficient;  this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);  this.dimensions = dimensions;  this.remove = false;  this.xPos = dimensions.WIDTH + (opt_xOffset || 0);  this.yPos = 0;  this.width = 0;  this.collisionBoxes = [];  this.gap = 0;  this.speedOffset = 0;  // For animated obstacles.  this.currentFrame = 0;  this.timer = 0;  this.init(speed);};/** * Coefficient for calculating the maximum gap. * @const */Obstacle.MAX_GAP_COEFFICIENT = 1.5;/** * Maximum obstacle grouping count. * @const */Obstacle.MAX_OBSTACLE_LENGTH = 3,Obstacle.prototype = {  /**   * Initialise the DOM for the obstacle.   * @param {number} speed   */  init: function(speed) {    this.cloneCollisionBoxes();    // Only allow sizing if we're at the right speed.    if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {      this.size = 1;    }    this.width = this.typeConfig.width * this.size;    // Check if obstacle can be positioned at various heights.    if (Array.isArray(this.typeConfig.yPos))  {      var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :          this.typeConfig.yPos;      this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];    } else {      this.yPos = this.typeConfig.yPos;    }    this.draw();    // Make collision box adjustments,    // Central box is adjusted to the size as one box.    //      ____        ______        ________    //    _|   |-|    _|     |-|    _|       |-|    //   | |<->| |   | |<--->| |   | |<----->| |    //   | | 1 | |   | |  2  | |   | |   3   | |    //   |_|___|_|   |_|_____|_|   |_|_______|_|    //    if (this.size > 1) {      this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -          this.collisionBoxes[2].width;      this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;    }    // For obstacles that go at a different speed from the horizon.    if (this.typeConfig.speedOffset) {      this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :          -this.typeConfig.speedOffset;    }    this.gap = this.getGap(this.gapCoefficient, speed);  },  /**   * Draw and crop based on size.   */  draw: function() {    var sourceWidth = this.typeConfig.width;    var sourceHeight = this.typeConfig.height;    if (IS_HIDPI) {      sourceWidth = sourceWidth * 2;      sourceHeight = sourceHeight * 2;    }    // X position in sprite.    var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +        this.spritePos.x;    // Animation frames.    if (this.currentFrame > 0) {      sourceX += sourceWidth * this.currentFrame;    }    this.canvasCtx.drawImage(Runner.imageSprite,      sourceX, this.spritePos.y,      sourceWidth * this.size, sourceHeight,      this.xPos, this.yPos,      this.typeConfig.width * this.size, this.typeConfig.height);  },  /**   * Obstacle frame update.   * @param {number} deltaTime   * @param {number} speed   */  update: function(deltaTime, speed) {    if (!this.remove) {      if (this.typeConfig.speedOffset) {        speed += this.speedOffset;      }      this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);      // Update frame      if (this.typeConfig.numFrames) {        this.timer += deltaTime;        if (this.timer >= this.typeConfig.frameRate) {          this.currentFrame =              this.currentFrame == this.typeConfig.numFrames - 1 ?              0 : this.currentFrame + 1;          this.timer = 0;        }      }      this.draw();      if (!this.isVisible()) {        this.remove = true;      }    }  },  /**   * Calculate a random gap size.   * - Minimum gap gets wider as speed increses   * @param {number} gapCoefficient   * @param {number} speed   * @return {number} The gap size.   */  getGap: function(gapCoefficient, speed) {    var minGap = Math.round(this.width * speed +          this.typeConfig.minGap * gapCoefficient);    var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);    return getRandomNum(minGap, maxGap);  },  /**   * Check if obstacle is visible.   * @return {boolean} Whether the obstacle is in the game area.   */  isVisible: function() {    return this.xPos + this.width > 0;  },  /**   * Make a copy of the collision boxes, since these will change based on   * obstacle type and size.   */  cloneCollisionBoxes: function() {    var collisionBoxes = this.typeConfig.collisionBoxes;    for (var i = collisionBoxes.length - 1; i >= 0; i--) {      this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,          collisionBoxes[i].y, collisionBoxes[i].width,          collisionBoxes[i].height);    }  }};/** * Obstacle definitions. * minGap: minimum pixel space betweeen obstacles. * multipleSpeed: Speed at which multiples are allowed. * speedOffset: speed faster / slower than the horizon. * minSpeed: Minimum speed which the obstacle can make an appearance. */Obstacle.types = [  {    type: 'CACTUS_SMALL',    width: 17,    height: 35,    yPos: 105,    multipleSpeed: 4,    minGap: 120,    minSpeed: 0,    collisionBoxes: [      new CollisionBox(0, 7, 5, 27),      new CollisionBox(4, 0, 6, 34),      new CollisionBox(10, 4, 7, 14)    ]  },  {    type: 'CACTUS_LARGE',    width: 25,    height: 50,    yPos: 90,    multipleSpeed: 7,    minGap: 120,    minSpeed: 0,    collisionBoxes: [      new CollisionBox(0, 12, 7, 38),      new CollisionBox(8, 0, 7, 49),      new CollisionBox(13, 10, 10, 38)    ]  },  {    type: 'PTERODACTYL',    width: 46,    height: 40,    yPos: [ 100, 75, 50 ], // Variable height.    yPosMobile: [ 100, 50 ], // Variable height mobile.    multipleSpeed: 999,    minSpeed: 8.5,    minGap: 150,    collisionBoxes: [      new CollisionBox(15, 15, 16, 5),      new CollisionBox(18, 21, 24, 6),      new CollisionBox(2, 14, 4, 3),      new CollisionBox(6, 10, 4, 7),      new CollisionBox(10, 8, 6, 9)    ],    numFrames: 2,    frameRate: 1000/6,    speedOffset: .8  }];//******************************************************************************/** * T-rex game character. * @param {HTMLCanvas} canvas * @param {Object} spritePos Positioning within image sprite. * @constructor */function Trex(canvas, spritePos) {  this.canvas = canvas;  this.canvasCtx = canvas.getContext('2d');  this.spritePos = spritePos;  this.xPos = 0;  this.yPos = 0;  // Position when on the ground.  this.groundYPos = 0;  this.currentFrame = 0;  this.currentAnimFrames = [];  this.blinkDelay = 0;  this.blinkCount = 0;  this.animStartTime = 0;  this.timer = 0;  this.msPerFrame = 1000 / FPS;  this.config = Trex.config;  // Current status.  this.status = Trex.status.WAITING;  this.jumping = false;  this.ducking = false;  this.jumpVelocity = 0;  this.reachedMinHeight = false;  this.speedDrop = false;  this.jumpCount = 0;  this.jumpspotX = 0;  this.init();};/** * T-rex player config. * @enum {number} */Trex.config = {  DROP_VELOCITY: -5,  GRAVITY: 0.6,  HEIGHT: 47,  HEIGHT_DUCK: 25,  INIITAL_JUMP_VELOCITY: -10,  INTRO_DURATION: 1500,  MAX_JUMP_HEIGHT: 30,  MIN_JUMP_HEIGHT: 30,  SPEED_DROP_COEFFICIENT: 3,  SPRITE_WIDTH: 262,  START_X_POS: 50,  WIDTH: 44,  WIDTH_DUCK: 59};/** * Used in collision detection. * @type {Array<CollisionBox>} */Trex.collisionBoxes = {  DUCKING: [    new CollisionBox(1, 18, 55, 25)  ],  RUNNING: [    new CollisionBox(22, 0, 17, 16),    new CollisionBox(1, 18, 30, 9),    new CollisionBox(10, 35, 14, 8),    new CollisionBox(1, 24, 29, 5),    new CollisionBox(5, 30, 21, 4),    new CollisionBox(9, 34, 15, 4)  ]};/** * Animation states. * @enum {string} */Trex.status = {  CRASHED: 'CRASHED',  DUCKING: 'DUCKING',  JUMPING: 'JUMPING',  RUNNING: 'RUNNING',  WAITING: 'WAITING'};/** * Blinking coefficient. * @const */Trex.BLINK_TIMING = 7000;/** * Animation config for different states. * @enum {Object} */Trex.animFrames = {  WAITING: {    frames: [44, 0],    msPerFrame: 1000 / 3  },  RUNNING: {    frames: [88, 132],    msPerFrame: 1000 / 12  },  CRASHED: {    frames: [220],    msPerFrame: 1000 / 60  },  JUMPING: {    frames: [0],    msPerFrame: 1000 / 60  },  DUCKING: {    frames: [262, 321],    msPerFrame: 1000 / 8  }};Trex.prototype = {  /**   * T-rex player initaliser.   * Sets the t-rex to blink at random intervals.   */  init: function() {    this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -        Runner.config.BOTTOM_PAD;    this.yPos = this.groundYPos;    this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;    this.draw(0, 0);    this.update(0, Trex.status.WAITING);  },  /**   * Setter for the jump velocity.   * The approriate drop velocity is also set.   */  setJumpVelocity: function(setting) {    this.config.INIITAL_JUMP_VELOCITY = -setting;    this.config.DROP_VELOCITY = -setting / 2;  },  /**   * Set the animation status.   * @param {!number} deltaTime   * @param {Trex.status} status Optional status to switch to.   */  update: function(deltaTime, opt_status) {    this.timer += deltaTime;    // Update the status.    if (opt_status) {      this.status = opt_status;      this.currentFrame = 0;      this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;      this.currentAnimFrames = Trex.animFrames[opt_status].frames;      if (opt_status == Trex.status.WAITING) {        this.animStartTime = getTimeStamp();        this.setBlinkDelay();      }    }    // Game intro animation, T-rex moves in from the left.    if (this.playingIntro && this.xPos < this.config.START_X_POS) {      this.xPos += Math.round((this.config.START_X_POS /          this.config.INTRO_DURATION) * deltaTime);    }    if (this.status == Trex.status.WAITING) {      this.blink(getTimeStamp());    } else {      this.draw(this.currentAnimFrames[this.currentFrame], 0);    }    // Update the frame position.    if (this.timer >= this.msPerFrame) {      this.currentFrame = this.currentFrame ==          this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;      this.timer = 0;    }    // Speed drop becomes duck if the down key is still being pressed.    if (this.speedDrop && this.yPos == this.groundYPos) {      this.speedDrop = false;      this.setDuck(true);    }  },  /**   * Draw the t-rex to a particular position.   * @param {number} x   * @param {number} y   */  draw: function(x, y) {    var sourceX = x;    var sourceY = y;    var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?        this.config.WIDTH_DUCK : this.config.WIDTH;    var sourceHeight = this.config.HEIGHT;    if (IS_HIDPI) {      sourceX *= 2;      sourceY *= 2;      sourceWidth *= 2;      sourceHeight *= 2;    }    // Adjustments for sprite sheet position.    sourceX += this.spritePos.x;    sourceY += this.spritePos.y;    // Ducking.    if (this.ducking && this.status != Trex.status.CRASHED) {      this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,          sourceWidth, sourceHeight,          this.xPos, this.yPos,          this.config.WIDTH_DUCK, this.config.HEIGHT);    } else {      // Crashed whilst ducking. Trex is standing up so needs adjustment.      if (this.ducking && this.status == Trex.status.CRASHED) {        this.xPos++;      }      // Standing / running      this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,          sourceWidth, sourceHeight,          this.xPos, this.yPos,          this.config.WIDTH, this.config.HEIGHT);    }  },  /**   * Sets a random time for the blink to happen.   */  setBlinkDelay: function() {    this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);  },  /**   * Make t-rex blink at random intervals.   * @param {number} time Current time in milliseconds.   */  blink: function(time) {    var deltaTime = time - this.animStartTime;    if (deltaTime >= this.blinkDelay) {      this.draw(this.currentAnimFrames[this.currentFrame], 0);      if (this.currentFrame == 1) {        // Set new random delay to blink.        this.setBlinkDelay();        this.animStartTime = time;        this.blinkCount++;      }    }  },  /**   * Initialise a jump.   * @param {number} speed   */  startJump: function(speed) {    if (!this.jumping) {      this.update(0, Trex.status.JUMPING);      // Tweak the jump velocity based on the speed.      this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);      this.jumping = true;      this.reachedMinHeight = false;      this.speedDrop = false;    }  },  /**   * Jump is complete, falling down.   */  endJump: function() {    if (this.reachedMinHeight &&        this.jumpVelocity < this.config.DROP_VELOCITY) {      this.jumpVelocity = this.config.DROP_VELOCITY;    }  },  /**   * Update frame for a jump.   * @param {number} deltaTime   * @param {number} speed   */  updateJump: function(deltaTime, speed) {    var msPerFrame = Trex.animFrames[this.status].msPerFrame;    var framesElapsed = deltaTime / msPerFrame;    // Speed drop makes Trex fall faster.    if (this.speedDrop) {      this.yPos += Math.round(this.jumpVelocity *          this.config.SPEED_DROP_COEFFICIENT * framesElapsed);    } else {      this.yPos += Math.round(this.jumpVelocity * framesElapsed);    }    this.jumpVelocity += this.config.GRAVITY * framesElapsed;    // Minimum height has been reached.    if (this.yPos < this.minJumpHeight || this.speedDrop) {      this.reachedMinHeight = true;    }    // Reached max height    if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {      this.endJump();    }    // Back down at ground level. Jump completed.    if (this.yPos > this.groundYPos) {      this.reset();      this.jumpCount++;    }    this.update(deltaTime);  },  /**   * Set the speed drop. Immediately cancels the current jump.   */  setSpeedDrop: function() {    this.speedDrop = true;    this.jumpVelocity = 1;  },  /**   * @param {boolean} isDucking.   */  setDuck: function(isDucking) {    if (isDucking && this.status != Trex.status.DUCKING) {      this.update(0, Trex.status.DUCKING);      this.ducking = true;    } else if (this.status == Trex.status.DUCKING) {      this.update(0, Trex.status.RUNNING);      this.ducking = false;    }  },  /**   * Reset the t-rex to running at start of game.   */  reset: function() {    this.yPos = this.groundYPos;    this.jumpVelocity = 0;    this.jumping = false;    this.ducking = false;    this.update(0, Trex.status.RUNNING);    this.midair = false;    this.speedDrop = false;    this.jumpCount = 0;  }};//******************************************************************************/** * Handles displaying the distance meter. * @param {!HTMLCanvasElement} canvas * @param {Object} spritePos Image position in sprite. * @param {number} canvasWidth * @constructor */function DistanceMeter(canvas, spritePos, canvasWidth) {  this.canvas = canvas;  this.canvasCtx = canvas.getContext('2d');  this.image = Runner.imageSprite;  this.spritePos = spritePos;  this.x = 0;  this.y = 5;  this.currentDistance = 0;  this.maxScore = 0;  this.highScore = 0;  this.container = null;  this.digits = [];  this.acheivement = false;  this.defaultString = '';  this.flashTimer = 0;  this.flashIterations = 0;  this.invertTrigger = false;  this.config = DistanceMeter.config;  this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;  this.init(canvasWidth);};/** * @enum {number} */DistanceMeter.dimensions = {  WIDTH: 10,  HEIGHT: 13,  DEST_WIDTH: 11};/** * Y positioning of the digits in the sprite sheet. * X position is always 0. * @type {Array<number>} */DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];/** * Distance meter config. * @enum {number} */DistanceMeter.config = {  // Number of digits.  MAX_DISTANCE_UNITS: 5,  // Distance that causes achievement animation.  ACHIEVEMENT_DISTANCE: 100,  // Used for conversion from pixel distance to a scaled unit.  COEFFICIENT: 0.025,  // Flash duration in milliseconds.  FLASH_DURATION: 1000 / 4,  // Flash iterations for achievement animation.  FLASH_ITERATIONS: 3};DistanceMeter.prototype = {  /**   * Initialise the distance meter to '00000'.   * @param {number} width Canvas width in px.   */  init: function(width) {    var maxDistanceStr = '';    this.calcXPos(width);    this.maxScore = this.maxScoreUnits;    for (var i = 0; i < this.maxScoreUnits; i++) {      this.draw(i, 0);      this.defaultString += '0';      maxDistanceStr += '9';    }    this.maxScore = parseInt(maxDistanceStr);  },  /**   * Calculate the xPos in the canvas.   * @param {number} canvasWidth   */  calcXPos: function(canvasWidth) {    this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *        (this.maxScoreUnits + 1));  },  /**   * Draw a digit to canvas.   * @param {number} digitPos Position of the digit.   * @param {number} value Digit value 0-9.   * @param {boolean} opt_highScore Whether drawing the high score.   */  draw: function(digitPos, value, opt_highScore) {    var sourceWidth = DistanceMeter.dimensions.WIDTH;    var sourceHeight = DistanceMeter.dimensions.HEIGHT;    var sourceX = DistanceMeter.dimensions.WIDTH * value;    var sourceY = 0;    var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;    var targetY = this.y;    var targetWidth = DistanceMeter.dimensions.WIDTH;    var targetHeight = DistanceMeter.dimensions.HEIGHT;    // For high DPI we 2x source values.    if (IS_HIDPI) {      sourceWidth *= 2;      sourceHeight *= 2;      sourceX *= 2;    }    sourceX += this.spritePos.x;    sourceY += this.spritePos.y;    this.canvasCtx.save();    if (opt_highScore) {      // Left of the current score.      var highScoreX = this.x - (this.maxScoreUnits * 2) *          DistanceMeter.dimensions.WIDTH;      this.canvasCtx.translate(highScoreX, this.y);    } else {      this.canvasCtx.translate(this.x, this.y);    }    this.canvasCtx.drawImage(this.image, sourceX, sourceY,        sourceWidth, sourceHeight,        targetX, targetY,        targetWidth, targetHeight      );    this.canvasCtx.restore();  },  /**   * Covert pixel distance to a 'real' distance.   * @param {number} distance Pixel distance ran.   * @return {number} The 'real' distance ran.   */  getActualDistance: function(distance) {    return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;  },  /**   * Update the distance meter.   * @param {number} distance   * @param {number} deltaTime   * @return {boolean} Whether the acheivement sound fx should be played.   */  update: function(deltaTime, distance) {    var paint = true;    var playSound = false;    if (!this.acheivement) {      distance = this.getActualDistance(distance);      // Score has gone beyond the initial digit count.      if (distance > this.maxScore && this.maxScoreUnits ==        this.config.MAX_DISTANCE_UNITS) {        this.maxScoreUnits++;        this.maxScore = parseInt(this.maxScore + '9');      } else {        this.distance = 0;      }      if (distance > 0) {        // Acheivement unlocked        if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {          // Flash score and play sound.          this.acheivement = true;          this.flashTimer = 0;          playSound = true;        }        // Create a string representation of the distance with leading 0.        var distanceStr = (this.defaultString +            distance).substr(-this.maxScoreUnits);        this.digits = distanceStr.split('');      } else {        this.digits = this.defaultString.split('');      }    } else {      // Control flashing of the score on reaching acheivement.      if (this.flashIterations <= this.config.FLASH_ITERATIONS) {        this.flashTimer += deltaTime;        if (this.flashTimer < this.config.FLASH_DURATION) {          paint = false;        } else if (this.flashTimer >            this.config.FLASH_DURATION * 2) {          this.flashTimer = 0;          this.flashIterations++;        }      } else {        this.acheivement = false;        this.flashIterations = 0;        this.flashTimer = 0;      }    }    // Draw the digits if not flashing.    if (paint) {      for (var i = this.digits.length - 1; i >= 0; i--) {        this.draw(i, parseInt(this.digits[i]));      }    }    this.drawHighScore();    return playSound;  },  /**   * Draw the high score.   */  drawHighScore: function() {    this.canvasCtx.save();    this.canvasCtx.globalAlpha = .8;    for (var i = this.highScore.length - 1; i >= 0; i--) {      this.draw(i, parseInt(this.highScore[i], 10), true);    }    this.canvasCtx.restore();  },  /**   * Set the highscore as a array string.   * Position of char in the sprite: H - 10, I - 11.   * @param {number} distance Distance ran in pixels.   */  setHighScore: function(distance) {    distance = this.getActualDistance(distance);    var highScoreStr = (this.defaultString +        distance).substr(-this.maxScoreUnits);    this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));  },  /**   * Reset the distance meter back to '00000'.   */  reset: function() {    this.update(0);    this.acheivement = false;  }};//******************************************************************************/** * Cloud background item. * Similar to an obstacle object but without collision boxes. * @param {HTMLCanvasElement} canvas Canvas element. * @param {Object} spritePos Position of image in sprite. * @param {number} containerWidth */function Cloud(canvas, spritePos, containerWidth) {  this.canvas = canvas;  this.canvasCtx = this.canvas.getContext('2d');  this.spritePos = spritePos;  this.containerWidth = containerWidth;  this.xPos = containerWidth;  this.yPos = 0;  this.remove = false;  this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,      Cloud.config.MAX_CLOUD_GAP);  this.init();};/** * Cloud object config. * @enum {number} */Cloud.config = {  HEIGHT: 14,  MAX_CLOUD_GAP: 400,  MAX_SKY_LEVEL: 30,  MIN_CLOUD_GAP: 100,  MIN_SKY_LEVEL: 71,  WIDTH: 46};Cloud.prototype = {  /**   * Initialise the cloud. Sets the Cloud height.   */  init: function() {    this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,        Cloud.config.MIN_SKY_LEVEL);    this.draw();  },  /**   * Draw the cloud.   */  draw: function() {    this.canvasCtx.save();    var sourceWidth = Cloud.config.WIDTH;    var sourceHeight = Cloud.config.HEIGHT;    if (IS_HIDPI) {      sourceWidth = sourceWidth * 2;      sourceHeight = sourceHeight * 2;    }    this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,        this.spritePos.y,        sourceWidth, sourceHeight,        this.xPos, this.yPos,        Cloud.config.WIDTH, Cloud.config.HEIGHT);    this.canvasCtx.restore();  },  /**   * Update the cloud position.   * @param {number} speed   */  update: function(speed) {    if (!this.remove) {      this.xPos -= Math.ceil(speed);      this.draw();      // Mark as removeable if no longer in the canvas.      if (!this.isVisible()) {        this.remove = true;      }    }  },  /**   * Check if the cloud is visible on the stage.   * @return {boolean}   */  isVisible: function() {    return this.xPos + Cloud.config.WIDTH > 0;  }};//******************************************************************************/** * Nightmode shows a moon and stars on the horizon. */function NightMode(canvas, spritePos, containerWidth) {  this.spritePos = spritePos;  this.canvas = canvas;  this.canvasCtx = canvas.getContext('2d');  this.xPos = containerWidth - 50;  this.yPos = 30;  this.currentPhase = 0;  this.opacity = 0;  this.containerWidth = containerWidth;  this.stars = [];  this.drawStars = false;  this.placeStars();};/** * @enum {number} */NightMode.config = {  FADE_SPEED: 0.035,  HEIGHT: 40,  MOON_SPEED: 0.25,  NUM_STARS: 2,  STAR_SIZE: 9,  STAR_SPEED: 0.3,  STAR_MAX_Y: 70,  WIDTH: 20};NightMode.phases = [140, 120, 100, 60, 40, 20, 0];NightMode.prototype = {  /**   * Update moving moon, changing phases.   * @param {boolean} activated Whether night mode is activated.   * @param {number} delta   */  update: function(activated, delta) {    // Moon phase.    if (activated && this.opacity == 0) {      this.currentPhase++;      if (this.currentPhase >= NightMode.phases.length) {        this.currentPhase = 0;      }    }    // Fade in / out.    if (activated && (this.opacity < 1 || this.opacity == 0)) {      this.opacity += NightMode.config.FADE_SPEED;    } else if (this.opacity > 0) {      this.opacity -= NightMode.config.FADE_SPEED;    }    // Set moon positioning.    if (this.opacity > 0) {      this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);      // Update stars.      if (this.drawStars) {         for (var i = 0; i < NightMode.config.NUM_STARS; i++) {            this.stars[i].x = this.updateXPos(this.stars[i].x,                NightMode.config.STAR_SPEED);         }      }      this.draw();    } else {      this.opacity = 0;      this.placeStars();    }    this.drawStars = true;  },  updateXPos: function(currentPos, speed) {    if (currentPos < -NightMode.config.WIDTH) {      currentPos = this.containerWidth;    } else {      currentPos -= speed;    }    return currentPos;  },  draw: function() {    var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :         NightMode.config.WIDTH;    var moonSourceHeight = NightMode.config.HEIGHT;    var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];    var moonOutputWidth = moonSourceWidth;    var starSize = NightMode.config.STAR_SIZE;    var starSourceX = Runner.spriteDefinition.LDPI.STAR.x;    if (IS_HIDPI) {      moonSourceWidth *= 2;      moonSourceHeight *= 2;      moonSourceX = this.spritePos.x +          (NightMode.phases[this.currentPhase] * 2);      starSize *= 2;      starSourceX = Runner.spriteDefinition.HDPI.STAR.x;    }    this.canvasCtx.save();    this.canvasCtx.globalAlpha = this.opacity;    // Stars.    if (this.drawStars) {      for (var i = 0; i < NightMode.config.NUM_STARS; i++) {        this.canvasCtx.drawImage(Runner.imageSprite,            starSourceX, this.stars[i].sourceY, starSize, starSize,            Math.round(this.stars[i].x), this.stars[i].y,            NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE);      }    }    // Moon.    this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX,        this.spritePos.y, moonSourceWidth, moonSourceHeight,        Math.round(this.xPos), this.yPos,        moonOutputWidth, NightMode.config.HEIGHT);    this.canvasCtx.globalAlpha = 1;    this.canvasCtx.restore();  },  // Do star placement.  placeStars: function() {    var segmentSize = Math.round(this.containerWidth /        NightMode.config.NUM_STARS);    for (var i = 0; i < NightMode.config.NUM_STARS; i++) {      this.stars[i] = {};      this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));      this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);      if (IS_HIDPI) {        this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y +            NightMode.config.STAR_SIZE * 2 * i;      } else {        this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +            NightMode.config.STAR_SIZE * i;      }    }  },  reset: function() {    this.currentPhase = 0;    this.opacity = 0;    this.update(false);  }};//******************************************************************************/** * Horizon Line. * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon. * @param {HTMLCanvasElement} canvas * @param {Object} spritePos Horizon position in sprite. * @constructor */function HorizonLine(canvas, spritePos) {  this.spritePos = spritePos;  this.canvas = canvas;  this.canvasCtx = canvas.getContext('2d');  this.sourceDimensions = {};  this.dimensions = HorizonLine.dimensions;  this.sourceXPos = [this.spritePos.x, this.spritePos.x +      this.dimensions.WIDTH];  this.xPos = [];  this.yPos = 0;  this.bumpThreshold = 0.5;  this.setSourceDimensions();  this.draw();};/** * Horizon line dimensions. * @enum {number} */HorizonLine.dimensions = {  WIDTH: 600,  HEIGHT: 12,  YPOS: 127};HorizonLine.prototype = {  /**   * Set the source dimensions of the horizon line.   */  setSourceDimensions: function() {    for (var dimension in HorizonLine.dimensions) {      if (IS_HIDPI) {        if (dimension != 'YPOS') {          this.sourceDimensions[dimension] =              HorizonLine.dimensions[dimension] * 2;        }      } else {        this.sourceDimensions[dimension] =            HorizonLine.dimensions[dimension];      }      this.dimensions[dimension] = HorizonLine.dimensions[dimension];    }    this.xPos = [0, HorizonLine.dimensions.WIDTH];    this.yPos = HorizonLine.dimensions.YPOS;  },  /**   * Return the crop x position of a type.   */  getRandomType: function() {    return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;  },  /**   * Draw the horizon line.   */  draw: function() {    this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],        this.spritePos.y,        this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,        this.xPos[0], this.yPos,        this.dimensions.WIDTH, this.dimensions.HEIGHT);    this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],        this.spritePos.y,        this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,        this.xPos[1], this.yPos,        this.dimensions.WIDTH, this.dimensions.HEIGHT);  },  /**   * Update the x position of an indivdual piece of the line.   * @param {number} pos Line position.   * @param {number} increment   */  updateXPos: function(pos, increment) {    var line1 = pos;    var line2 = pos == 0 ? 1 : 0;    this.xPos[line1] -= increment;    this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;    if (this.xPos[line1] <= -this.dimensions.WIDTH) {      this.xPos[line1] += this.dimensions.WIDTH * 2;      this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;      this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;    }  },  /**   * Update the horizon line.   * @param {number} deltaTime   * @param {number} speed   */  update: function(deltaTime, speed) {    var increment = Math.floor(speed * (FPS / 1000) * deltaTime);    if (this.xPos[0] <= 0) {      this.updateXPos(0, increment);    } else {      this.updateXPos(1, increment);    }    this.draw();  },  /**   * Reset horizon to the starting position.   */  reset: function() {    this.xPos[0] = 0;    this.xPos[1] = HorizonLine.dimensions.WIDTH;  }};//******************************************************************************/** * Horizon background class. * @param {HTMLCanvasElement} canvas * @param {Object} spritePos Sprite positioning. * @param {Object} dimensions Canvas dimensions. * @param {number} gapCoefficient * @constructor */function Horizon(canvas, spritePos, dimensions, gapCoefficient) {  this.canvas = canvas;  this.canvasCtx = this.canvas.getContext('2d');  this.config = Horizon.config;  this.dimensions = dimensions;  this.gapCoefficient = gapCoefficient;  this.obstacles = [];  this.obstacleHistory = [];  this.horizonOffsets = [0, 0];  this.cloudFrequency = this.config.CLOUD_FREQUENCY;  this.spritePos = spritePos;  this.nightMode = null;  // Cloud  this.clouds = [];  this.cloudSpeed = this.config.BG_CLOUD_SPEED;  // Horizon  this.horizonLine = null;  this.init();};/** * Horizon config. * @enum {number} */Horizon.config = {  BG_CLOUD_SPEED: 0.2,  BUMPY_THRESHOLD: .3,  CLOUD_FREQUENCY: .5,  HORIZON_HEIGHT: 16,  MAX_CLOUDS: 6};Horizon.prototype = {  /**   * Initialise the horizon. Just add the line and a cloud. No obstacles.   */  init: function() {    this.addCloud();    this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);    this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,        this.dimensions.WIDTH);  },  /**   * @param {number} deltaTime   * @param {number} currentSpeed   * @param {boolean} updateObstacles Used as an override to prevent   *     the obstacles from being updated / added. This happens in the   *     ease in section.   * @param {boolean} showNightMode Night mode activated.   */  update: function(deltaTime, currentSpeed, updateObstacles, showNightMode) {    this.runningTime += deltaTime;    this.horizonLine.update(deltaTime, currentSpeed);    this.nightMode.update(showNightMode);    this.updateClouds(deltaTime, currentSpeed);    if (updateObstacles) {      this.updateObstacles(deltaTime, currentSpeed);    }  },  /**   * Update the cloud positions.   * @param {number} deltaTime   * @param {number} currentSpeed   */  updateClouds: function(deltaTime, speed) {    var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;    var numClouds = this.clouds.length;    if (numClouds) {      for (var i = numClouds - 1; i >= 0; i--) {        this.clouds[i].update(cloudSpeed);      }      var lastCloud = this.clouds[numClouds - 1];      // Check for adding a new cloud.      if (numClouds < this.config.MAX_CLOUDS &&          (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&          this.cloudFrequency > Math.random()) {        this.addCloud();      }      // Remove expired clouds.      this.clouds = this.clouds.filter(function(obj) {        return !obj.remove;      });    } else {      this.addCloud();    }  },  /**   * Update the obstacle positions.   * @param {number} deltaTime   * @param {number} currentSpeed   */  updateObstacles: function(deltaTime, currentSpeed) {    // Obstacles, move to Horizon layer.    var updatedObstacles = this.obstacles.slice(0);    for (var i = 0; i < this.obstacles.length; i++) {      var obstacle = this.obstacles[i];      obstacle.update(deltaTime, currentSpeed);      // Clean up existing obstacles.      if (obstacle.remove) {        updatedObstacles.shift();      }    }    this.obstacles = updatedObstacles;    if (this.obstacles.length > 0) {      var lastObstacle = this.obstacles[this.obstacles.length - 1];      if (lastObstacle && !lastObstacle.followingObstacleCreated &&          lastObstacle.isVisible() &&          (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <          this.dimensions.WIDTH) {        this.addNewObstacle(currentSpeed);        lastObstacle.followingObstacleCreated = true;      }    } else {      // Create new obstacles.      this.addNewObstacle(currentSpeed);    }  },  removeFirstObstacle: function() {    this.obstacles.shift();  },  /**   * Add a new obstacle.   * @param {number} currentSpeed   */  addNewObstacle: function(currentSpeed) {    var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);    var obstacleType = Obstacle.types[obstacleTypeIndex];    // Check for multiples of the same type of obstacle.    // Also check obstacle is available at current speed.    if (this.duplicateObstacleCheck(obstacleType.type) ||        currentSpeed < obstacleType.minSpeed) {      this.addNewObstacle(currentSpeed);    } else {      var obstacleSpritePos = this.spritePos[obstacleType.type];      this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,          obstacleSpritePos, this.dimensions,          this.gapCoefficient, currentSpeed, obstacleType.width));      this.obstacleHistory.unshift(obstacleType.type);      if (this.obstacleHistory.length > 1) {        this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);      }    }  },  /**   * Returns whether the previous two obstacles are the same as the next one.   * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.   * @return {boolean}   */  duplicateObstacleCheck: function(nextObstacleType) {    var duplicateCount = 0;    for (var i = 0; i < this.obstacleHistory.length; i++) {      duplicateCount = this.obstacleHistory[i] == nextObstacleType ?          duplicateCount + 1 : 0;    }    return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;  },  /**   * Reset the horizon layer.   * Remove existing obstacles and reposition the horizon line.   */  reset: function() {    this.obstacles = [];    this.horizonLine.reset();    this.nightMode.reset();  },  /**   * Update the canvas width and scaling.   * @param {number} width Canvas width.   * @param {number} height Canvas height.   */  resize: function(width, height) {    this.canvas.width = width;    this.canvas.height = height;  },  /**   * Add a new cloud to the horizon.   */  addCloud: function() {    this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,        this.dimensions.WIDTH));  }};})();
原创粉丝点击