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.