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.
8 * @param {string} outerContainerId Outer containing element id.
9 * @param {object} opt_config
13 function Runner(outerContainerId, opt_config) {
15 if (Runner.instance_) {
16 return Runner.instance_;
18 Runner.instance_ = this;
20 this.outerContainerEl = document.querySelector(outerContainerId);
21 this.containerEl = null;
22 this.detailsButton = this.outerContainerEl.querySelector('#details-button');
24 this.config = opt_config || Runner.config;
26 this.dimensions = Runner.defaultDimensions;
29 this.canvasCtx = null;
33 this.distanceMeter = null;
36 this.highestScore = 0;
40 this.msPerFrame = 1000 / FPS;
41 this.currentSpeed = this.config.SPEED;
46 this.activated = false;
50 this.resizeTimerId_ = null;
55 this.audioBuffer = null;
58 // Global web audio context for playing sounds.
59 this.audioContext = null;
63 this.imagesLoaded = 0;
66 window['Runner'] = Runner;
73 var DEFAULT_WIDTH = 600;
82 var IS_HIDPI = window.devicePixelRatio > 1;
85 var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1;
88 var IS_TOUCH_ENABLED = 'ontouchstart' in window;
92 * Default game configuration.
100 CLOUD_FREQUENCY: 0.5,
101 GAMEOVER_CLEAR_TIME: 750,
102 GAP_COEFFICIENT: 0.6,
104 INITIAL_JUMP_VELOCITY: 12,
106 MAX_OBSTACLE_LENGTH: 3,
109 MOBILE_SPEED_COEFFICIENT: 1.2,
110 RESOURCE_TEMPLATE_ID: 'audio-resources',
112 SPEED_DROP_COEFFICIENT: 3
117 * Default dimensions.
120 Runner.defaultDimensions = {
121 WIDTH: DEFAULT_WIDTH,
131 CANVAS: 'runner-canvas',
132 CONTAINER: 'runner-container',
134 ICON: 'icon-offline',
135 TOUCH_CONTROLLER: 'controller'
141 * @enum {array.<object>}
143 Runner.imageSources = {
145 {name: 'CACTUS_LARGE', id: '1x-obstacle-large'},
146 {name: 'CACTUS_SMALL', id: '1x-obstacle-small'},
147 {name: 'CLOUD', id: '1x-cloud'},
148 {name: 'HORIZON', id: '1x-horizon'},
149 {name: 'RESTART', id: '1x-restart'},
150 {name: 'TEXT_SPRITE', id: '1x-text'},
151 {name: 'TREX', id: '1x-trex'}
154 {name: 'CACTUS_LARGE', id: '2x-obstacle-large'},
155 {name: 'CACTUS_SMALL', id: '2x-obstacle-small'},
156 {name: 'CLOUD', id: '2x-cloud'},
157 {name: 'HORIZON', id: '2x-horizon'},
158 {name: 'RESTART', id: '2x-restart'},
159 {name: 'TEXT_SPRITE', id: '2x-text'},
160 {name: 'TREX', id: '2x-trex'}
166 * Sound FX. Reference to the ID of the audio tag on interstitial page.
170 BUTTON_PRESS: 'offline-sound-press',
171 HIT: 'offline-sound-hit',
172 SCORE: 'offline-sound-reached'
181 JUMP: {'38': 1, '32': 1}, // Up, spacebar
182 DUCK: {'40': 1}, // Down
183 RESTART: {'13': 1} // Enter
188 * Runner event names.
192 ANIM_END: 'webkitAnimationEnd',
196 MOUSEDOWN: 'mousedown',
199 TOUCHEND: 'touchend',
200 TOUCHSTART: 'touchstart',
201 VISIBILITY: 'visibilitychange',
210 * Setting individual settings for debugging.
211 * @param {string} setting
214 updateConfigSetting: function(setting, value) {
215 if (setting in this.config && value != undefined) {
216 this.config[setting] = value;
220 case 'MIN_JUMP_HEIGHT':
221 case 'SPEED_DROP_COEFFICIENT':
222 this.tRex.config[setting] = value;
224 case 'INITIAL_JUMP_VELOCITY':
225 this.tRex.setJumpVelocity(value);
228 this.setSpeed(value);
235 * Load and cache the image assets from the page.
237 loadImages: function() {
238 var imageSources = IS_HIDPI ? Runner.imageSources.HDPI :
239 Runner.imageSources.LDPI;
241 var numImages = imageSources.length;
243 for (var i = numImages - 1; i >= 0; i--) {
244 var imgSource = imageSources[i];
245 this.images[imgSource.name] = document.getElementById(imgSource.id);
251 * Load and decode base 64 encoded sounds.
253 loadSounds: function() {
254 this.audioContext = new AudioContext();
255 var resourceTemplate =
256 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
258 for (var sound in Runner.sounds) {
259 var soundSrc = resourceTemplate.getElementById(Runner.sounds[sound]).src;
260 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
261 var buffer = decodeBase64ToArrayBuffer(soundSrc);
263 // Async, so no guarantee of order in array.
264 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
265 this.soundFx[index] = audioData;
266 }.bind(this, sound));
271 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
272 * @param {number} opt_speed
274 setSpeed: function(opt_speed) {
275 var speed = opt_speed || this.currentSpeed;
277 // Reduce the speed on smaller mobile screens.
278 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
279 var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
280 this.config.MOBILE_SPEED_COEFFICIENT;
281 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
282 } else if (opt_speed) {
283 this.currentSpeed = opt_speed;
291 // Hide the static icon.
292 document.querySelector('.' + Runner.classes.ICON).style.visibility =
295 this.adjustDimensions();
298 this.containerEl = document.createElement('div');
299 this.containerEl.className = Runner.classes.CONTAINER;
301 // Player canvas container.
302 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
303 this.dimensions.HEIGHT, Runner.classes.PLAYER);
305 this.canvasCtx = this.canvas.getContext('2d');
306 this.canvasCtx.fillStyle = '#f7f7f7';
307 this.canvasCtx.fill();
308 Runner.updateCanvasScaling(this.canvas);
310 // Horizon contains clouds, obstacles and the ground.
311 this.horizon = new Horizon(this.canvas, this.images, this.dimensions,
312 this.config.GAP_COEFFICIENT);
315 this.distanceMeter = new DistanceMeter(this.canvas,
316 this.images.TEXT_SPRITE, this.dimensions.WIDTH);
319 this.tRex = new Trex(this.canvas, this.images.TREX);
321 this.outerContainerEl.appendChild(this.containerEl);
324 this.createTouchController();
327 this.startListening();
330 window.addEventListener(Runner.events.RESIZE,
331 this.debounceResize.bind(this));
335 * Create the touch controller. A div that covers whole screen.
337 createTouchController: function() {
338 this.touchController = document.createElement('div');
339 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
343 * Debounce the resize event.
345 debounceResize: function() {
346 if (!this.resizeTimerId_) {
347 this.resizeTimerId_ =
348 setInterval(this.adjustDimensions.bind(this), 250);
353 * Adjust game space dimensions on resize.
355 adjustDimensions: function() {
356 clearInterval(this.resizeTimerId_);
357 this.resizeTimerId_ = null;
359 var boxStyles = window.getComputedStyle(this.outerContainerEl);
360 var padding = Number(boxStyles.paddingLeft.substr(0,
361 boxStyles.paddingLeft.length - 2));
363 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
365 // Redraw the elements back onto the canvas.
367 this.canvas.width = this.dimensions.WIDTH;
368 this.canvas.height = this.dimensions.HEIGHT;
370 Runner.updateCanvasScaling(this.canvas);
372 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
374 this.horizon.update(0, 0, true);
377 // Outer container and distance meter.
378 if (this.activated || this.crashed) {
379 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
380 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
381 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
384 this.tRex.draw(0, 0);
388 if (this.crashed && this.gameOverPanel) {
389 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
390 this.gameOverPanel.draw();
396 * Play the game intro.
397 * Canvas container width expands out to the full width.
399 playIntro: function() {
400 if (!this.started && !this.crashed) {
401 this.playingIntro = true;
402 this.tRex.playingIntro = true;
404 // CSS animation definition.
405 var keyframes = '@-webkit-keyframes intro { ' +
406 'from { width:' + Trex.config.WIDTH + 'px }' +
407 'to { width: ' + this.dimensions.WIDTH + 'px }' +
409 document.styleSheets[0].insertRule(keyframes, 0);
411 this.containerEl.addEventListener(Runner.events.ANIM_END,
412 this.startGame.bind(this));
414 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
415 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
417 if (this.touchController) {
418 this.outerContainerEl.appendChild(this.touchController);
420 this.activated = true;
422 } else if (this.crashed) {
429 * Update the game status to started.
431 startGame: function() {
432 this.runningTime = 0;
433 this.playingIntro = false;
434 this.tRex.playingIntro = false;
435 this.containerEl.style.webkitAnimation = '';
438 // Handle tabbing off the page. Pause the current game.
439 window.addEventListener(Runner.events.VISIBILITY,
440 this.onVisibilityChange.bind(this));
442 window.addEventListener(Runner.events.BLUR,
443 this.onVisibilityChange.bind(this));
445 window.addEventListener(Runner.events.FOCUS,
446 this.onVisibilityChange.bind(this));
449 clearCanvas: function() {
450 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
451 this.dimensions.HEIGHT);
455 * Update the game frame.
458 this.drawPending = false;
460 var now = performance.now();
461 var deltaTime = now - (this.time || now);
464 if (this.activated) {
467 if (this.tRex.jumping) {
468 this.tRex.updateJump(deltaTime, this.config);
471 this.runningTime += deltaTime;
472 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
474 // First jump triggers the intro.
475 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
479 // The horizon doesn't move until the intro is over.
480 if (this.playingIntro) {
481 this.horizon.update(0, this.currentSpeed, hasObstacles);
483 deltaTime = !this.started ? 0 : deltaTime;
484 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
487 // Check for collisions.
488 var collision = hasObstacles &&
489 checkForCollision(this.horizon.obstacles[0], this.tRex);
492 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
494 if (this.currentSpeed < this.config.MAX_SPEED) {
495 this.currentSpeed += this.config.ACCELERATION;
501 if (this.distanceMeter.getActualDistance(this.distanceRan) >
502 this.distanceMeter.maxScore) {
503 this.distanceRan = 0;
506 var playAcheivementSound = this.distanceMeter.update(deltaTime,
507 Math.ceil(this.distanceRan));
509 if (playAcheivementSound) {
510 this.playSound(this.soundFx.SCORE);
515 this.tRex.update(deltaTime);
523 handleEvent: function(e) {
524 return (function(evtType, events) {
527 case events.TOUCHSTART:
528 case events.MOUSEDOWN:
532 case events.TOUCHEND:
537 }.bind(this))(e.type, Runner.events);
541 * Bind relevant key / mouse / touch listeners.
543 startListening: function() {
545 document.addEventListener(Runner.events.KEYDOWN, this);
546 document.addEventListener(Runner.events.KEYUP, this);
549 // Mobile only touch devices.
550 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
551 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
552 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
555 document.addEventListener(Runner.events.MOUSEDOWN, this);
556 document.addEventListener(Runner.events.MOUSEUP, this);
561 * Remove all listeners.
563 stopListening: function() {
564 document.removeEventListener(Runner.events.KEYDOWN, this);
565 document.removeEventListener(Runner.events.KEYUP, this);
568 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
569 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
570 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
572 document.removeEventListener(Runner.events.MOUSEDOWN, this);
573 document.removeEventListener(Runner.events.MOUSEUP, this);
581 onKeyDown: function(e) {
582 if (e.target != this.detailsButton) {
583 if (!this.crashed && (Runner.keycodes.JUMP[String(e.keyCode)] ||
584 e.type == Runner.events.TOUCHSTART)) {
585 if (!this.activated) {
587 this.activated = true;
590 if (!this.tRex.jumping) {
591 this.playSound(this.soundFx.BUTTON_PRESS);
592 this.tRex.startJump();
596 if (this.crashed && e.type == Runner.events.TOUCHSTART &&
597 e.currentTarget == this.containerEl) {
602 // Speed drop, activated only when jump key is not pressed.
603 if (Runner.keycodes.DUCK[e.keyCode] && this.tRex.jumping) {
605 this.tRex.setSpeedDrop();
614 onKeyUp: function(e) {
615 var keyCode = String(e.keyCode);
616 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
617 e.type == Runner.events.TOUCHEND ||
618 e.type == Runner.events.MOUSEDOWN;
620 if (this.isRunning() && isjumpKey) {
622 } else if (Runner.keycodes.DUCK[keyCode]) {
623 this.tRex.speedDrop = false;
624 } else if (this.crashed) {
625 // Check that enough time has elapsed before allowing jump key to restart.
626 var deltaTime = performance.now() - this.time;
628 if (Runner.keycodes.RESTART[keyCode] ||
629 (e.type == Runner.events.MOUSEUP && e.target == this.canvas) ||
630 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
631 Runner.keycodes.JUMP[keyCode])) {
634 } else if (this.paused && isjumpKey) {
640 * RequestAnimationFrame wrapper.
643 if (!this.drawPending) {
644 this.drawPending = true;
645 this.raqId = requestAnimationFrame(this.update.bind(this));
650 * Whether the game is running.
653 isRunning: function() {
660 gameOver: function() {
661 this.playSound(this.soundFx.HIT);
666 this.distanceMeter.acheivement = false;
668 this.tRex.update(100, Trex.status.CRASHED);
671 if (!this.gameOverPanel) {
672 this.gameOverPanel = new GameOverPanel(this.canvas,
673 this.images.TEXT_SPRITE, this.images.RESTART,
676 this.gameOverPanel.draw();
679 // Update the high score.
680 if (this.distanceRan > this.highestScore) {
681 this.highestScore = Math.ceil(this.distanceRan);
682 this.distanceMeter.setHighScore(this.highestScore);
685 // Reset the time clock.
686 this.time = performance.now();
690 this.activated = false;
692 cancelAnimationFrame(this.raqId);
698 this.activated = true;
700 this.tRex.update(0, Trex.status.RUNNING);
701 this.time = performance.now();
706 restart: function() {
709 this.runningTime = 0;
710 this.activated = true;
711 this.crashed = false;
712 this.distanceRan = 0;
713 this.setSpeed(this.config.SPEED);
715 this.time = performance.now();
716 this.containerEl.classList.remove(Runner.classes.CRASHED);
718 this.distanceMeter.reset(this.highestScore);
719 this.horizon.reset();
721 this.playSound(this.soundFx.BUTTON_PRESS);
728 * Pause the game if the tab is not in focus.
730 onVisibilityChange: function(e) {
731 if (document.hidden || document.webkitHidden || e.type == 'blur') {
740 * @param {SoundBuffer} soundBuffer
742 playSound: function(soundBuffer) {
744 var sourceNode = this.audioContext.createBufferSource();
745 sourceNode.buffer = soundBuffer;
746 sourceNode.connect(this.audioContext.destination);
754 * Updates the canvas size taking into
755 * account the backing store pixel ratio and
756 * the device pixel ratio.
758 * See article by Paul Lewis:
759 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
761 * @param {HTMLCanvasElement} canvas
762 * @param {number} opt_width
763 * @param {number} opt_height
764 * @return {boolean} Whether the canvas was scaled.
766 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
767 var context = canvas.getContext('2d');
769 // Query the various pixel ratios
770 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
771 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
772 var ratio = devicePixelRatio / backingStoreRatio;
774 // Upscale the canvas if the two ratios don't match
775 if (devicePixelRatio !== backingStoreRatio) {
777 var oldWidth = opt_width || canvas.width;
778 var oldHeight = opt_height || canvas.height;
780 canvas.width = oldWidth * ratio;
781 canvas.height = oldHeight * ratio;
783 canvas.style.width = oldWidth + 'px';
784 canvas.style.height = oldHeight + 'px';
786 // Scale the context to counter the fact that we've manually scaled
787 // our canvas element.
788 context.scale(ratio, ratio);
797 * @param {number} min
798 * @param {number} max
801 function getRandomNum(min, max) {
802 return Math.floor(Math.random() * (max - min + 1)) + min;
807 * Vibrate on mobile devices.
808 * @param {number} duration Duration of the vibration in milliseconds.
810 function vibrate(duration) {
812 window.navigator['vibrate'](duration);
818 * Create canvas element.
819 * @param {HTMLElement} container Element to append canvas to.
820 * @param {number} width
821 * @param {number} height
822 * @param {string} opt_classname
823 * @return {HTMLCanvasElement}
825 function createCanvas(container, width, height, opt_classname) {
826 var canvas = document.createElement('canvas');
827 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
828 opt_classname : Runner.classes.CANVAS;
829 canvas.width = width;
830 canvas.height = height;
831 container.appendChild(canvas);
838 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
839 * @param {string} base64String
841 function decodeBase64ToArrayBuffer(base64String) {
842 var len = (base64String.length / 4) * 3;
843 var str = atob(base64String);
844 var arrayBuffer = new ArrayBuffer(len);
845 var bytes = new Uint8Array(arrayBuffer);
847 for (var i = 0; i < len; i++) {
848 bytes[i] = str.charCodeAt(i);
854 //******************************************************************************
859 * @param {!HTMLCanvasElement} canvas
860 * @param {!HTMLImage} textSprite
861 * @param {!HTMLImage} restartImg
862 * @param {!Object} dimensions Canvas dimensions.
865 function GameOverPanel(canvas, textSprite, restartImg, dimensions) {
866 this.canvas = canvas;
867 this.canvasCtx = canvas.getContext('2d');
868 this.canvasDimensions = dimensions;
869 this.textSprite = textSprite;
870 this.restartImg = restartImg;
876 * Dimensions used in the panel.
879 GameOverPanel.dimensions = {
889 GameOverPanel.prototype = {
891 * Update the panel dimensions.
892 * @param {number} width New canvas width.
893 * @param {number} opt_height Optional new canvas height.
895 updateDimensions: function(width, opt_height) {
896 this.canvasDimensions.WIDTH = width;
898 this.canvasDimensions.HEIGHT = opt_height;
906 var dimensions = GameOverPanel.dimensions;
908 var centerX = this.canvasDimensions.WIDTH / 2;
911 var textSourceX = dimensions.TEXT_X;
912 var textSourceY = dimensions.TEXT_Y;
913 var textSourceWidth = dimensions.TEXT_WIDTH;
914 var textSourceHeight = dimensions.TEXT_HEIGHT;
916 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
917 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
918 var textTargetWidth = dimensions.TEXT_WIDTH;
919 var textTargetHeight = dimensions.TEXT_HEIGHT;
921 var restartSourceWidth = dimensions.RESTART_WIDTH;
922 var restartSourceHeight = dimensions.RESTART_HEIGHT;
923 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
924 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
929 textSourceWidth *= 2;
930 textSourceHeight *= 2;
931 restartSourceWidth *= 2;
932 restartSourceHeight *= 2;
935 // Game over text from sprite.
936 this.canvasCtx.drawImage(this.textSprite,
937 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
938 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
941 this.canvasCtx.drawImage(this.restartImg, 0, 0,
942 restartSourceWidth, restartSourceHeight,
943 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
944 dimensions.RESTART_HEIGHT);
949 //******************************************************************************
952 * Check for a collision.
953 * @param {!Obstacle} obstacle
954 * @param {!Trex} tRex T-rex object.
955 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
957 * @return {Array.<CollisionBox>}
959 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
960 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
962 // Adjustments are made to the bounding box as there is a 1 pixel white
963 // border around the t-rex and obstacles.
964 var tRexBox = new CollisionBox(
967 tRex.config.WIDTH - 2,
968 tRex.config.HEIGHT - 2);
970 var obstacleBox = new CollisionBox(
973 obstacle.typeConfig.width * obstacle.size - 2,
974 obstacle.typeConfig.height - 2);
978 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
981 // Simple outer bounds check.
982 if (boxCompare(tRexBox, obstacleBox)) {
983 var collisionBoxes = obstacle.collisionBoxes;
984 var tRexCollisionBoxes = Trex.collisionBoxes;
986 // Detailed axis aligned box check.
987 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
988 for (var i = 0; i < collisionBoxes.length; i++) {
989 // Adjust the box to actual positions.
991 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
993 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
994 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
996 // Draw boxes for debug.
998 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1002 return [adjTrexBox, adjObstacleBox];
1012 * Adjust the collision box.
1013 * @param {!CollisionBox} box The original box.
1014 * @param {!CollisionBox} adjustment Adjustment box.
1015 * @return {CollisionBox} The adjusted collision box object.
1017 function createAdjustedCollisionBox(box, adjustment) {
1018 return new CollisionBox(
1019 box.x + adjustment.x,
1020 box.y + adjustment.y,
1027 * Draw the collision boxes for debug.
1029 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1031 canvasCtx.strokeStyle = '#f00';
1032 canvasCtx.strokeRect(tRexBox.x, tRexBox.y,
1033 tRexBox.width, tRexBox.height);
1035 canvasCtx.strokeStyle = '#0f0';
1036 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1037 obstacleBox.width, obstacleBox.height);
1038 canvasCtx.restore();
1043 * Compare two collision boxes for a collision.
1044 * @param {CollisionBox} tRexBox
1045 * @param {CollisionBox} obstacleBox
1046 * @return {boolean} Whether the boxes intersected.
1048 function boxCompare(tRexBox, obstacleBox) {
1049 var crashed = false;
1050 var tRexBoxX = tRexBox.x;
1051 var tRexBoxY = tRexBox.y;
1053 var obstacleBoxX = obstacleBox.x;
1054 var obstacleBoxY = obstacleBox.y;
1056 // Axis-Aligned Bounding Box method.
1057 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1058 tRexBox.x + tRexBox.width > obstacleBoxX &&
1059 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1060 tRexBox.height + tRexBox.y > obstacleBox.y) {
1068 //******************************************************************************
1071 * Collision box object.
1072 * @param {number} x X position.
1073 * @param {number} y Y Position.
1074 * @param {number} w Width.
1075 * @param {number} h Height.
1077 function CollisionBox(x, y, w, h) {
1085 //******************************************************************************
1089 * @param {HTMLCanvasCtx} canvasCtx
1090 * @param {Obstacle.type} type
1091 * @param {image} obstacleImg Image sprite.
1092 * @param {Object} dimensions
1093 * @param {number} gapCoefficient Mutipler in determining the gap.
1094 * @param {number} speed
1096 function Obstacle(canvasCtx, type, obstacleImg, dimensions,
1097 gapCoefficient, speed) {
1099 this.canvasCtx = canvasCtx;
1100 this.image = obstacleImg;
1101 this.typeConfig = type;
1102 this.gapCoefficient = gapCoefficient;
1103 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1104 this.dimensions = dimensions;
1105 this.remove = false;
1107 this.yPos = this.typeConfig.yPos;
1109 this.collisionBoxes = [];
1116 * Coefficient for calculating the maximum gap.
1119 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1122 * Maximum obstacle grouping count.
1125 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1128 Obstacle.prototype = {
1130 * Initialise the DOM for the obstacle.
1131 * @param {number} speed
1133 init: function(speed) {
1134 this.cloneCollisionBoxes();
1136 // Only allow sizing if we're at the right speed.
1137 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1141 this.width = this.typeConfig.width * this.size;
1142 this.xPos = this.dimensions.WIDTH - this.width;
1146 // Make collision box adjustments,
1147 // Central box is adjusted to the size as one box.
1148 // ____ ______ ________
1149 // _| |-| _| |-| _| |-|
1150 // | |<->| | | |<--->| | | |<----->| |
1151 // | | 1 | | | | 2 | | | | 3 | |
1152 // |_|___|_| |_|_____|_| |_|_______|_|
1154 if (this.size > 1) {
1155 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1156 this.collisionBoxes[2].width;
1157 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1160 this.gap = this.getGap(this.gapCoefficient, speed);
1164 * Draw and crop based on size.
1167 var sourceWidth = this.typeConfig.width;
1168 var sourceHeight = this.typeConfig.height;
1171 sourceWidth = sourceWidth * 2;
1172 sourceHeight = sourceHeight * 2;
1176 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1));
1177 this.canvasCtx.drawImage(this.image,
1179 sourceWidth * this.size, sourceHeight,
1180 this.xPos, this.yPos,
1181 this.typeConfig.width * this.size, this.typeConfig.height);
1185 * Obstacle frame update.
1186 * @param {number} deltaTime
1187 * @param {number} speed
1189 update: function(deltaTime, speed) {
1191 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1194 if (!this.isVisible()) {
1201 * Calculate a random gap size.
1202 * - Minimum gap gets wider as speed increses
1203 * @param {number} gapCoefficient
1204 * @param {number} speed
1205 * @return {number} The gap size.
1207 getGap: function(gapCoefficient, speed) {
1208 var minGap = Math.round(this.width * speed +
1209 this.typeConfig.minGap * gapCoefficient);
1210 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1211 return getRandomNum(minGap, maxGap);
1215 * Check if obstacle is visible.
1216 * @return {boolean} Whether the obstacle is in the game area.
1218 isVisible: function() {
1219 return this.xPos + this.width > 0;
1223 * Make a copy of the collision boxes, since these will change based on
1224 * obstacle type and size.
1226 cloneCollisionBoxes: function() {
1227 var collisionBoxes = this.typeConfig.collisionBoxes;
1229 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1230 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1231 collisionBoxes[i].y, collisionBoxes[i].width,
1232 collisionBoxes[i].height);
1239 * Obstacle definitions.
1240 * minGap: minimum pixel space betweeen obstacles.
1241 * multipleSpeed: Speed at which multiples are allowed.
1245 type: 'CACTUS_SMALL',
1246 className: ' cactus cactus-small ',
1253 new CollisionBox(0, 7, 5, 27),
1254 new CollisionBox(4, 0, 6, 34),
1255 new CollisionBox(10, 4, 7, 14)
1259 type: 'CACTUS_LARGE',
1260 className: ' cactus cactus-large ',
1267 new CollisionBox(0, 12, 7, 38),
1268 new CollisionBox(8, 0, 7, 49),
1269 new CollisionBox(13, 10, 10, 38)
1275 //******************************************************************************
1277 * T-rex game character.
1278 * @param {HTMLCanvas} canvas
1279 * @param {HTMLImage} image Character image.
1282 function Trex(canvas, image) {
1283 this.canvas = canvas;
1284 this.canvasCtx = canvas.getContext('2d');
1288 // Position when on the ground.
1289 this.groundYPos = 0;
1290 this.currentFrame = 0;
1291 this.currentAnimFrames = [];
1292 this.blinkDelay = 0;
1293 this.animStartTime = 0;
1295 this.msPerFrame = 1000 / FPS;
1296 this.config = Trex.config;
1298 this.status = Trex.status.WAITING;
1300 this.jumping = false;
1301 this.jumpVelocity = 0;
1302 this.reachedMinHeight = false;
1303 this.speedDrop = false;
1312 * T-rex player config.
1319 INIITAL_JUMP_VELOCITY: -10,
1320 INTRO_DURATION: 1500,
1321 MAX_JUMP_HEIGHT: 30,
1322 MIN_JUMP_HEIGHT: 30,
1323 SPEED_DROP_COEFFICIENT: 3,
1331 * Used in collision detection.
1332 * @type {Array.<CollisionBox>}
1334 Trex.collisionBoxes = [
1335 new CollisionBox(1, -1, 30, 26),
1336 new CollisionBox(32, 0, 8, 16),
1337 new CollisionBox(10, 35, 14, 8),
1338 new CollisionBox(1, 24, 29, 5),
1339 new CollisionBox(5, 30, 21, 4),
1340 new CollisionBox(9, 34, 15, 4)
1356 * Blinking coefficient.
1359 Trex.BLINK_TIMING = 7000;
1363 * Animation config for different states.
1369 msPerFrame: 1000 / 3
1373 msPerFrame: 1000 / 12
1377 msPerFrame: 1000 / 60
1381 msPerFrame: 1000 / 60
1388 * T-rex player initaliser.
1389 * Sets the t-rex to blink at random intervals.
1392 this.blinkDelay = this.setBlinkDelay();
1393 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1394 Runner.config.BOTTOM_PAD;
1395 this.yPos = this.groundYPos;
1396 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1399 this.update(0, Trex.status.WAITING);
1403 * Setter for the jump velocity.
1404 * The approriate drop velocity is also set.
1406 setJumpVelocity: function(setting) {
1407 this.config.INIITAL_JUMP_VELOCITY = -setting;
1408 this.config.DROP_VELOCITY = -setting / 2;
1412 * Set the animation status.
1413 * @param {!number} deltaTime
1414 * @param {Trex.status} status Optional status to switch to.
1416 update: function(deltaTime, opt_status) {
1417 this.timer += deltaTime;
1419 // Update the status.
1421 this.status = opt_status;
1422 this.currentFrame = 0;
1423 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1424 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1426 if (opt_status == Trex.status.WAITING) {
1427 this.animStartTime = performance.now();
1428 this.setBlinkDelay();
1432 // Game intro animation, T-rex moves in from the left.
1433 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1434 this.xPos += Math.round((this.config.START_X_POS /
1435 this.config.INTRO_DURATION) * deltaTime);
1438 if (this.status == Trex.status.WAITING) {
1439 this.blink(performance.now());
1441 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1444 // Update the frame position.
1445 if (this.timer >= this.msPerFrame) {
1446 this.currentFrame = this.currentFrame ==
1447 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1453 * Draw the t-rex to a particular position.
1457 draw: function(x, y) {
1460 var sourceWidth = this.config.WIDTH;
1461 var sourceHeight = this.config.HEIGHT;
1470 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1471 sourceWidth, sourceHeight,
1472 this.xPos, this.yPos,
1473 this.config.WIDTH, this.config.HEIGHT);
1477 * Sets a random time for the blink to happen.
1479 setBlinkDelay: function() {
1480 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1484 * Make t-rex blink at random intervals.
1485 * @param {number} time Current time in milliseconds.
1487 blink: function(time) {
1488 var deltaTime = time - this.animStartTime;
1490 if (deltaTime >= this.blinkDelay) {
1491 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1493 if (this.currentFrame == 1) {
1494 // Set new random delay to blink.
1495 this.setBlinkDelay();
1496 this.animStartTime = time;
1502 * Initialise a jump.
1504 startJump: function() {
1505 if (!this.jumping) {
1506 this.update(0, Trex.status.JUMPING);
1507 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY;
1508 this.jumping = true;
1509 this.reachedMinHeight = false;
1510 this.speedDrop = false;
1515 * Jump is complete, falling down.
1517 endJump: function() {
1518 if (this.reachedMinHeight &&
1519 this.jumpVelocity < this.config.DROP_VELOCITY) {
1520 this.jumpVelocity = this.config.DROP_VELOCITY;
1525 * Update frame for a jump.
1526 * @param {number} deltaTime
1528 updateJump: function(deltaTime) {
1529 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1530 var framesElapsed = deltaTime / msPerFrame;
1532 // Speed drop makes Trex fall faster.
1533 if (this.speedDrop) {
1534 this.yPos += Math.round(this.jumpVelocity *
1535 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1537 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1540 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1542 // Minimum height has been reached.
1543 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1544 this.reachedMinHeight = true;
1547 // Reached max height
1548 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1552 // Back down at ground level. Jump completed.
1553 if (this.yPos > this.groundYPos) {
1558 this.update(deltaTime);
1562 * Set the speed drop. Immediately cancels the current jump.
1564 setSpeedDrop: function() {
1565 this.speedDrop = true;
1566 this.jumpVelocity = 1;
1570 * Reset the t-rex to running at start of game.
1573 this.yPos = this.groundYPos;
1574 this.jumpVelocity = 0;
1575 this.jumping = false;
1576 this.update(0, Trex.status.RUNNING);
1577 this.midair = false;
1578 this.speedDrop = false;
1584 //******************************************************************************
1587 * Handles displaying the distance meter.
1588 * @param {!HTMLCanvasElement} canvas
1589 * @param {!HTMLImage} spriteSheet Image sprite.
1590 * @param {number} canvasWidth
1593 function DistanceMeter(canvas, spriteSheet, canvasWidth) {
1594 this.canvas = canvas;
1595 this.canvasCtx = canvas.getContext('2d');
1596 this.image = spriteSheet;
1600 this.currentDistance = 0;
1603 this.container = null;
1606 this.acheivement = false;
1607 this.defaultString = '';
1608 this.flashTimer = 0;
1609 this.flashIterations = 0;
1611 this.config = DistanceMeter.config;
1612 this.init(canvasWidth);
1619 DistanceMeter.dimensions = {
1627 * Y positioning of the digits in the sprite sheet.
1628 * X position is always 0.
1629 * @type {array.<number>}
1631 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1635 * Distance meter config.
1638 DistanceMeter.config = {
1639 // Number of digits.
1640 MAX_DISTANCE_UNITS: 5,
1642 // Distance that causes achievement animation.
1643 ACHIEVEMENT_DISTANCE: 100,
1645 // Used for conversion from pixel distance to a scaled unit.
1648 // Flash duration in milliseconds.
1649 FLASH_DURATION: 1000 / 4,
1651 // Flash iterations for achievement animation.
1656 DistanceMeter.prototype = {
1658 * Initialise the distance meter to '00000'.
1659 * @param {number} width Canvas width in px.
1661 init: function(width) {
1662 var maxDistanceStr = '';
1664 this.calcXPos(width);
1665 this.maxScore = this.config.MAX_DISTANCE_UNITS;
1666 for (var i = 0; i < this.config.MAX_DISTANCE_UNITS; i++) {
1668 this.defaultString += '0';
1669 maxDistanceStr += '9';
1672 this.maxScore = parseInt(maxDistanceStr);
1676 * Calculate the xPos in the canvas.
1677 * @param {number} canvasWidth
1679 calcXPos: function(canvasWidth) {
1680 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1681 (this.config.MAX_DISTANCE_UNITS + 1));
1685 * Draw a digit to canvas.
1686 * @param {number} digitPos Position of the digit.
1687 * @param {number} value Digit value 0-9.
1688 * @param {boolean} opt_highScore Whether drawing the high score.
1690 draw: function(digitPos, value, opt_highScore) {
1691 var sourceWidth = DistanceMeter.dimensions.WIDTH;
1692 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1693 var sourceX = DistanceMeter.dimensions.WIDTH * value;
1695 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1696 var targetY = this.y;
1697 var targetWidth = DistanceMeter.dimensions.WIDTH;
1698 var targetHeight = DistanceMeter.dimensions.HEIGHT;
1700 // For high DPI we 2x source values.
1707 this.canvasCtx.save();
1709 if (opt_highScore) {
1710 // Left of the current score.
1711 var highScoreX = this.x - (this.config.MAX_DISTANCE_UNITS * 2) *
1712 DistanceMeter.dimensions.WIDTH;
1713 this.canvasCtx.translate(highScoreX, this.y);
1715 this.canvasCtx.translate(this.x, this.y);
1718 this.canvasCtx.drawImage(this.image, sourceX, 0,
1719 sourceWidth, sourceHeight,
1721 targetWidth, targetHeight
1724 this.canvasCtx.restore();
1728 * Covert pixel distance to a 'real' distance.
1729 * @param {number} distance Pixel distance ran.
1730 * @return {number} The 'real' distance ran.
1732 getActualDistance: function(distance) {
1734 Math.round(distance * this.config.COEFFICIENT) : 0;
1738 * Update the distance meter.
1739 * @param {number} deltaTime
1740 * @param {number} distance
1741 * @return {boolean} Whether the acheivement sound fx should be played.
1743 update: function(deltaTime, distance) {
1745 var playSound = false;
1747 if (!this.acheivement) {
1748 distance = this.getActualDistance(distance);
1751 // Acheivement unlocked
1752 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1753 // Flash score and play sound.
1754 this.acheivement = true;
1755 this.flashTimer = 0;
1759 // Create a string representation of the distance with leading 0.
1760 var distanceStr = (this.defaultString +
1761 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1762 this.digits = distanceStr.split('');
1764 this.digits = this.defaultString.split('');
1767 // Control flashing of the score on reaching acheivement.
1768 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1769 this.flashTimer += deltaTime;
1771 if (this.flashTimer < this.config.FLASH_DURATION) {
1773 } else if (this.flashTimer >
1774 this.config.FLASH_DURATION * 2) {
1775 this.flashTimer = 0;
1776 this.flashIterations++;
1779 this.acheivement = false;
1780 this.flashIterations = 0;
1781 this.flashTimer = 0;
1785 // Draw the digits if not flashing.
1787 for (var i = this.digits.length - 1; i >= 0; i--) {
1788 this.draw(i, parseInt(this.digits[i]));
1792 this.drawHighScore();
1798 * Draw the high score.
1800 drawHighScore: function() {
1801 this.canvasCtx.save();
1802 this.canvasCtx.globalAlpha = .8;
1803 for (var i = this.highScore.length - 1; i >= 0; i--) {
1804 this.draw(i, parseInt(this.highScore[i], 10), true);
1806 this.canvasCtx.restore();
1810 * Set the highscore as a array string.
1811 * Position of char in the sprite: H - 10, I - 11.
1812 * @param {number} distance Distance ran in pixels.
1814 setHighScore: function(distance) {
1815 distance = this.getActualDistance(distance);
1816 var highScoreStr = (this.defaultString +
1817 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1819 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
1823 * Reset the distance meter back to '00000'.
1827 this.acheivement = false;
1832 //******************************************************************************
1835 * Cloud background item.
1836 * Similar to an obstacle object but without collision boxes.
1837 * @param {HTMLCanvasElement} canvas Canvas element.
1838 * @param {Image} cloudImg
1839 * @param {number} containerWidth
1841 function Cloud(canvas, cloudImg, containerWidth) {
1842 this.canvas = canvas;
1843 this.canvasCtx = this.canvas.getContext('2d');
1844 this.image = cloudImg;
1845 this.containerWidth = containerWidth;
1846 this.xPos = containerWidth;
1848 this.remove = false;
1849 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
1850 Cloud.config.MAX_CLOUD_GAP);
1857 * Cloud object config.
1872 * Initialise the cloud. Sets the Cloud height.
1875 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
1876 Cloud.config.MIN_SKY_LEVEL);
1884 this.canvasCtx.save();
1885 var sourceWidth = Cloud.config.WIDTH;
1886 var sourceHeight = Cloud.config.HEIGHT;
1889 sourceWidth = sourceWidth * 2;
1890 sourceHeight = sourceHeight * 2;
1893 this.canvasCtx.drawImage(this.image, 0, 0,
1894 sourceWidth, sourceHeight,
1895 this.xPos, this.yPos,
1896 Cloud.config.WIDTH, Cloud.config.HEIGHT);
1898 this.canvasCtx.restore();
1902 * Update the cloud position.
1903 * @param {number} speed
1905 update: function(speed) {
1907 this.xPos -= Math.ceil(speed);
1910 // Mark as removeable if no longer in the canvas.
1911 if (!this.isVisible()) {
1918 * Check if the cloud is visible on the stage.
1921 isVisible: function() {
1922 return this.xPos + Cloud.config.WIDTH > 0;
1927 //******************************************************************************
1931 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
1932 * @param {HTMLCanvasElement} canvas
1933 * @param {HTMLImage} bgImg Horizon line sprite.
1936 function HorizonLine(canvas, bgImg) {
1938 this.canvas = canvas;
1939 this.canvasCtx = canvas.getContext('2d');
1940 this.sourceDimensions = {};
1941 this.dimensions = HorizonLine.dimensions;
1942 this.sourceXPos = [0, this.dimensions.WIDTH];
1945 this.bumpThreshold = 0.5;
1947 this.setSourceDimensions();
1953 * Horizon line dimensions.
1956 HorizonLine.dimensions = {
1963 HorizonLine.prototype = {
1965 * Set the source dimensions of the horizon line.
1967 setSourceDimensions: function() {
1969 for (var dimension in HorizonLine.dimensions) {
1971 if (dimension != 'YPOS') {
1972 this.sourceDimensions[dimension] =
1973 HorizonLine.dimensions[dimension] * 2;
1976 this.sourceDimensions[dimension] =
1977 HorizonLine.dimensions[dimension];
1979 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
1982 this.xPos = [0, HorizonLine.dimensions.WIDTH];
1983 this.yPos = HorizonLine.dimensions.YPOS;
1987 * Return the crop x position of a type.
1989 getRandomType: function() {
1990 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
1994 * Draw the horizon line.
1997 this.canvasCtx.drawImage(this.image, this.sourceXPos[0], 0,
1998 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
1999 this.xPos[0], this.yPos,
2000 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2002 this.canvasCtx.drawImage(this.image, this.sourceXPos[1], 0,
2003 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2004 this.xPos[1], this.yPos,
2005 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2009 * Update the x position of an indivdual piece of the line.
2010 * @param {number} pos Line position.
2011 * @param {number} increment
2013 updateXPos: function(pos, increment) {
2015 var line2 = pos == 0 ? 1 : 0;
2017 this.xPos[line1] -= increment;
2018 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2020 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2021 this.xPos[line1] += this.dimensions.WIDTH * 2;
2022 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2023 this.sourceXPos[line1] = this.getRandomType();
2028 * Update the horizon line.
2029 * @param {number} deltaTime
2030 * @param {number} speed
2032 update: function(deltaTime, speed) {
2033 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2035 if (this.xPos[0] <= 0) {
2036 this.updateXPos(0, increment);
2038 this.updateXPos(1, increment);
2044 * Reset horizon to the starting position.
2048 this.xPos[1] = HorizonLine.dimensions.WIDTH;
2053 //******************************************************************************
2056 * Horizon background class.
2057 * @param {HTMLCanvasElement} canvas
2058 * @param {Array.<HTMLImageElement>} images
2059 * @param {object} dimensions Canvas dimensions.
2060 * @param {number} gapCoefficient
2063 function Horizon(canvas, images, dimensions, gapCoefficient) {
2064 this.canvas = canvas;
2065 this.canvasCtx = this.canvas.getContext('2d');
2066 this.config = Horizon.config;
2067 this.dimensions = dimensions;
2068 this.gapCoefficient = gapCoefficient;
2069 this.obstacles = [];
2070 this.horizonOffsets = [0, 0];
2071 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2075 this.cloudImg = images.CLOUD;
2076 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2079 this.horizonImg = images.HORIZON;
2080 this.horizonLine = null;
2083 this.obstacleImgs = {
2084 CACTUS_SMALL: images.CACTUS_SMALL,
2085 CACTUS_LARGE: images.CACTUS_LARGE
2097 BG_CLOUD_SPEED: 0.2,
2098 BUMPY_THRESHOLD: .3,
2099 CLOUD_FREQUENCY: .5,
2105 Horizon.prototype = {
2107 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2111 this.horizonLine = new HorizonLine(this.canvas, this.horizonImg);
2115 * @param {number} deltaTime
2116 * @param {number} currentSpeed
2117 * @param {boolean} updateObstacles Used as an override to prevent
2118 * the obstacles from being updated / added. This happens in the
2121 update: function(deltaTime, currentSpeed, updateObstacles) {
2122 this.runningTime += deltaTime;
2123 this.horizonLine.update(deltaTime, currentSpeed);
2124 this.updateClouds(deltaTime, currentSpeed);
2126 if (updateObstacles) {
2127 this.updateObstacles(deltaTime, currentSpeed);
2132 * Update the cloud positions.
2133 * @param {number} deltaTime
2134 * @param {number} currentSpeed
2136 updateClouds: function(deltaTime, speed) {
2137 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2138 var numClouds = this.clouds.length;
2141 for (var i = numClouds - 1; i >= 0; i--) {
2142 this.clouds[i].update(cloudSpeed);
2145 var lastCloud = this.clouds[numClouds - 1];
2147 // Check for adding a new cloud.
2148 if (numClouds < this.config.MAX_CLOUDS &&
2149 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2150 this.cloudFrequency > Math.random()) {
2154 // Remove expired clouds.
2155 this.clouds = this.clouds.filter(function(obj) {
2162 * Update the obstacle positions.
2163 * @param {number} deltaTime
2164 * @param {number} currentSpeed
2166 updateObstacles: function(deltaTime, currentSpeed) {
2167 // Obstacles, move to Horizon layer.
2168 var updatedObstacles = this.obstacles.slice(0);
2170 for (var i = 0; i < this.obstacles.length; i++) {
2171 var obstacle = this.obstacles[i];
2172 obstacle.update(deltaTime, currentSpeed);
2174 // Clean up existing obstacles.
2175 if (obstacle.remove) {
2176 updatedObstacles.shift();
2179 this.obstacles = updatedObstacles;
2181 if (this.obstacles.length > 0) {
2182 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2184 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2185 lastObstacle.isVisible() &&
2186 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2187 this.dimensions.WIDTH) {
2188 this.addNewObstacle(currentSpeed);
2189 lastObstacle.followingObstacleCreated = true;
2192 // Create new obstacles.
2193 this.addNewObstacle(currentSpeed);
2198 * Add a new obstacle.
2199 * @param {number} currentSpeed
2201 addNewObstacle: function(currentSpeed) {
2202 var obstacleTypeIndex =
2203 getRandomNum(0, Obstacle.types.length - 1);
2204 var obstacleType = Obstacle.types[obstacleTypeIndex];
2205 var obstacleImg = this.obstacleImgs[obstacleType.type];
2207 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2208 obstacleImg, this.dimensions, this.gapCoefficient, currentSpeed));
2212 * Reset the horizon layer.
2213 * Remove existing obstacles and reposition the horizon line.
2216 this.obstacles = [];
2217 this.horizonLine.reset();
2221 * Update the canvas width and scaling.
2222 * @param {number} width Canvas width.
2223 * @param {number} height Canvas height.
2225 resize: function(width, height) {
2226 this.canvas.width = width;
2227 this.canvas.height = height;
2231 * Add a new cloud to the horizon.
2233 addCloud: function() {
2234 this.clouds.push(new Cloud(this.canvas, this.cloudImg,
2235 this.dimensions.WIDTH));