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;
86 window.navigator.userAgent.indexOf('UIWebViewForStaticFileContent') > -1;
89 var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1 || IS_IOS;
92 var IS_TOUCH_ENABLED = 'ontouchstart' in window;
95 * Default game configuration.
103 CLOUD_FREQUENCY: 0.5,
104 GAMEOVER_CLEAR_TIME: 750,
105 GAP_COEFFICIENT: 0.6,
107 INITIAL_JUMP_VELOCITY: 12,
109 MAX_OBSTACLE_LENGTH: 3,
112 MOBILE_SPEED_COEFFICIENT: 1.2,
113 RESOURCE_TEMPLATE_ID: 'audio-resources',
115 SPEED_DROP_COEFFICIENT: 3
120 * Default dimensions.
123 Runner.defaultDimensions = {
124 WIDTH: DEFAULT_WIDTH,
134 CANVAS: 'runner-canvas',
135 CONTAINER: 'runner-container',
137 ICON: 'icon-offline',
138 TOUCH_CONTROLLER: 'controller'
144 * @enum {array.<object>}
146 Runner.imageSources = {
148 {name: 'CACTUS_LARGE', id: '1x-obstacle-large'},
149 {name: 'CACTUS_SMALL', id: '1x-obstacle-small'},
150 {name: 'CLOUD', id: '1x-cloud'},
151 {name: 'HORIZON', id: '1x-horizon'},
152 {name: 'RESTART', id: '1x-restart'},
153 {name: 'TEXT_SPRITE', id: '1x-text'},
154 {name: 'TREX', id: '1x-trex'}
157 {name: 'CACTUS_LARGE', id: '2x-obstacle-large'},
158 {name: 'CACTUS_SMALL', id: '2x-obstacle-small'},
159 {name: 'CLOUD', id: '2x-cloud'},
160 {name: 'HORIZON', id: '2x-horizon'},
161 {name: 'RESTART', id: '2x-restart'},
162 {name: 'TEXT_SPRITE', id: '2x-text'},
163 {name: 'TREX', id: '2x-trex'}
169 * Sound FX. Reference to the ID of the audio tag on interstitial page.
173 BUTTON_PRESS: 'offline-sound-press',
174 HIT: 'offline-sound-hit',
175 SCORE: 'offline-sound-reached'
184 JUMP: {'38': 1, '32': 1}, // Up, spacebar
185 DUCK: {'40': 1}, // Down
186 RESTART: {'13': 1} // Enter
191 * Runner event names.
195 ANIM_END: 'webkitAnimationEnd',
199 MOUSEDOWN: 'mousedown',
202 TOUCHEND: 'touchend',
203 TOUCHSTART: 'touchstart',
204 VISIBILITY: 'visibilitychange',
213 * Setting individual settings for debugging.
214 * @param {string} setting
217 updateConfigSetting: function(setting, value) {
218 if (setting in this.config && value != undefined) {
219 this.config[setting] = value;
223 case 'MIN_JUMP_HEIGHT':
224 case 'SPEED_DROP_COEFFICIENT':
225 this.tRex.config[setting] = value;
227 case 'INITIAL_JUMP_VELOCITY':
228 this.tRex.setJumpVelocity(value);
231 this.setSpeed(value);
238 * Load and cache the image assets from the page.
240 loadImages: function() {
241 var imageSources = IS_HIDPI ? Runner.imageSources.HDPI :
242 Runner.imageSources.LDPI;
244 var numImages = imageSources.length;
246 for (var i = numImages - 1; i >= 0; i--) {
247 var imgSource = imageSources[i];
248 this.images[imgSource.name] = document.getElementById(imgSource.id);
254 * Load and decode base 64 encoded sounds.
256 loadSounds: function() {
258 this.audioContext = new AudioContext();
259 var resourceTemplate =
260 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
262 for (var sound in Runner.sounds) {
264 resourceTemplate.getElementById(Runner.sounds[sound]).src;
265 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
266 var buffer = decodeBase64ToArrayBuffer(soundSrc);
268 // Async, so no guarantee of order in array.
269 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
270 this.soundFx[index] = audioData;
271 }.bind(this, sound));
277 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
278 * @param {number} opt_speed
280 setSpeed: function(opt_speed) {
281 var speed = opt_speed || this.currentSpeed;
283 // Reduce the speed on smaller mobile screens.
284 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
285 var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
286 this.config.MOBILE_SPEED_COEFFICIENT;
287 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
288 } else if (opt_speed) {
289 this.currentSpeed = opt_speed;
297 // Hide the static icon.
298 document.querySelector('.' + Runner.classes.ICON).style.visibility =
301 this.adjustDimensions();
304 this.containerEl = document.createElement('div');
305 this.containerEl.className = Runner.classes.CONTAINER;
307 // Player canvas container.
308 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
309 this.dimensions.HEIGHT, Runner.classes.PLAYER);
311 this.canvasCtx = this.canvas.getContext('2d');
312 this.canvasCtx.fillStyle = '#f7f7f7';
313 this.canvasCtx.fill();
314 Runner.updateCanvasScaling(this.canvas);
316 // Horizon contains clouds, obstacles and the ground.
317 this.horizon = new Horizon(this.canvas, this.images, this.dimensions,
318 this.config.GAP_COEFFICIENT);
321 this.distanceMeter = new DistanceMeter(this.canvas,
322 this.images.TEXT_SPRITE, this.dimensions.WIDTH);
325 this.tRex = new Trex(this.canvas, this.images.TREX);
327 this.outerContainerEl.appendChild(this.containerEl);
330 this.createTouchController();
333 this.startListening();
336 window.addEventListener(Runner.events.RESIZE,
337 this.debounceResize.bind(this));
341 * Create the touch controller. A div that covers whole screen.
343 createTouchController: function() {
344 this.touchController = document.createElement('div');
345 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
349 * Debounce the resize event.
351 debounceResize: function() {
352 if (!this.resizeTimerId_) {
353 this.resizeTimerId_ =
354 setInterval(this.adjustDimensions.bind(this), 250);
359 * Adjust game space dimensions on resize.
361 adjustDimensions: function() {
362 clearInterval(this.resizeTimerId_);
363 this.resizeTimerId_ = null;
365 var boxStyles = window.getComputedStyle(this.outerContainerEl);
366 var padding = Number(boxStyles.paddingLeft.substr(0,
367 boxStyles.paddingLeft.length - 2));
369 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
371 // Redraw the elements back onto the canvas.
373 this.canvas.width = this.dimensions.WIDTH;
374 this.canvas.height = this.dimensions.HEIGHT;
376 Runner.updateCanvasScaling(this.canvas);
378 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
380 this.horizon.update(0, 0, true);
383 // Outer container and distance meter.
384 if (this.activated || this.crashed) {
385 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
386 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
387 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
390 this.tRex.draw(0, 0);
394 if (this.crashed && this.gameOverPanel) {
395 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
396 this.gameOverPanel.draw();
402 * Play the game intro.
403 * Canvas container width expands out to the full width.
405 playIntro: function() {
406 if (!this.started && !this.crashed) {
407 this.playingIntro = true;
408 this.tRex.playingIntro = true;
410 // CSS animation definition.
411 var keyframes = '@-webkit-keyframes intro { ' +
412 'from { width:' + Trex.config.WIDTH + 'px }' +
413 'to { width: ' + this.dimensions.WIDTH + 'px }' +
415 document.styleSheets[0].insertRule(keyframes, 0);
417 this.containerEl.addEventListener(Runner.events.ANIM_END,
418 this.startGame.bind(this));
420 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
421 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
423 if (this.touchController) {
424 this.outerContainerEl.appendChild(this.touchController);
426 this.activated = true;
428 } else if (this.crashed) {
435 * Update the game status to started.
437 startGame: function() {
438 this.runningTime = 0;
439 this.playingIntro = false;
440 this.tRex.playingIntro = false;
441 this.containerEl.style.webkitAnimation = '';
444 // Handle tabbing off the page. Pause the current game.
445 window.addEventListener(Runner.events.VISIBILITY,
446 this.onVisibilityChange.bind(this));
448 window.addEventListener(Runner.events.BLUR,
449 this.onVisibilityChange.bind(this));
451 window.addEventListener(Runner.events.FOCUS,
452 this.onVisibilityChange.bind(this));
455 clearCanvas: function() {
456 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
457 this.dimensions.HEIGHT);
461 * Update the game frame.
464 this.drawPending = false;
466 var now = getTimeStamp();
467 var deltaTime = now - (this.time || now);
470 if (this.activated) {
473 if (this.tRex.jumping) {
474 this.tRex.updateJump(deltaTime, this.config);
477 this.runningTime += deltaTime;
478 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
480 // First jump triggers the intro.
481 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
485 // The horizon doesn't move until the intro is over.
486 if (this.playingIntro) {
487 this.horizon.update(0, this.currentSpeed, hasObstacles);
489 deltaTime = !this.started ? 0 : deltaTime;
490 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
493 // Check for collisions.
494 var collision = hasObstacles &&
495 checkForCollision(this.horizon.obstacles[0], this.tRex);
498 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
500 if (this.currentSpeed < this.config.MAX_SPEED) {
501 this.currentSpeed += this.config.ACCELERATION;
507 if (this.distanceMeter.getActualDistance(this.distanceRan) >
508 this.distanceMeter.maxScore) {
509 this.distanceRan = 0;
512 var playAcheivementSound = this.distanceMeter.update(deltaTime,
513 Math.ceil(this.distanceRan));
515 if (playAcheivementSound) {
516 this.playSound(this.soundFx.SCORE);
521 this.tRex.update(deltaTime);
529 handleEvent: function(e) {
530 return (function(evtType, events) {
533 case events.TOUCHSTART:
534 case events.MOUSEDOWN:
538 case events.TOUCHEND:
543 }.bind(this))(e.type, Runner.events);
547 * Bind relevant key / mouse / touch listeners.
549 startListening: function() {
551 document.addEventListener(Runner.events.KEYDOWN, this);
552 document.addEventListener(Runner.events.KEYUP, this);
555 // Mobile only touch devices.
556 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
557 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
558 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
561 document.addEventListener(Runner.events.MOUSEDOWN, this);
562 document.addEventListener(Runner.events.MOUSEUP, this);
567 * Remove all listeners.
569 stopListening: function() {
570 document.removeEventListener(Runner.events.KEYDOWN, this);
571 document.removeEventListener(Runner.events.KEYUP, this);
574 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
575 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
576 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
578 document.removeEventListener(Runner.events.MOUSEDOWN, this);
579 document.removeEventListener(Runner.events.MOUSEUP, this);
587 onKeyDown: function(e) {
588 if (e.target != this.detailsButton) {
589 if (!this.crashed && (Runner.keycodes.JUMP[String(e.keyCode)] ||
590 e.type == Runner.events.TOUCHSTART)) {
591 if (!this.activated) {
593 this.activated = true;
596 if (!this.tRex.jumping) {
597 this.playSound(this.soundFx.BUTTON_PRESS);
598 this.tRex.startJump();
602 if (this.crashed && e.type == Runner.events.TOUCHSTART &&
603 e.currentTarget == this.containerEl) {
608 // Speed drop, activated only when jump key is not pressed.
609 if (Runner.keycodes.DUCK[e.keyCode] && this.tRex.jumping) {
611 this.tRex.setSpeedDrop();
620 onKeyUp: function(e) {
621 var keyCode = String(e.keyCode);
622 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
623 e.type == Runner.events.TOUCHEND ||
624 e.type == Runner.events.MOUSEDOWN;
626 if (this.isRunning() && isjumpKey) {
628 } else if (Runner.keycodes.DUCK[keyCode]) {
629 this.tRex.speedDrop = false;
630 } else if (this.crashed) {
631 // Check that enough time has elapsed before allowing jump key to restart.
632 var deltaTime = getTimeStamp() - this.time;
634 if (Runner.keycodes.RESTART[keyCode] ||
635 (e.type == Runner.events.MOUSEUP && e.target == this.canvas) ||
636 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
637 Runner.keycodes.JUMP[keyCode])) {
640 } else if (this.paused && isjumpKey) {
646 * RequestAnimationFrame wrapper.
649 if (!this.drawPending) {
650 this.drawPending = true;
651 this.raqId = requestAnimationFrame(this.update.bind(this));
656 * Whether the game is running.
659 isRunning: function() {
666 gameOver: function() {
667 this.playSound(this.soundFx.HIT);
672 this.distanceMeter.acheivement = false;
674 this.tRex.update(100, Trex.status.CRASHED);
677 if (!this.gameOverPanel) {
678 this.gameOverPanel = new GameOverPanel(this.canvas,
679 this.images.TEXT_SPRITE, this.images.RESTART,
682 this.gameOverPanel.draw();
685 // Update the high score.
686 if (this.distanceRan > this.highestScore) {
687 this.highestScore = Math.ceil(this.distanceRan);
688 this.distanceMeter.setHighScore(this.highestScore);
691 // Reset the time clock.
692 this.time = getTimeStamp();
696 this.activated = false;
698 cancelAnimationFrame(this.raqId);
704 this.activated = true;
706 this.tRex.update(0, Trex.status.RUNNING);
707 this.time = getTimeStamp();
712 restart: function() {
715 this.runningTime = 0;
716 this.activated = true;
717 this.crashed = false;
718 this.distanceRan = 0;
719 this.setSpeed(this.config.SPEED);
721 this.time = getTimeStamp();
722 this.containerEl.classList.remove(Runner.classes.CRASHED);
724 this.distanceMeter.reset(this.highestScore);
725 this.horizon.reset();
727 this.playSound(this.soundFx.BUTTON_PRESS);
734 * Pause the game if the tab is not in focus.
736 onVisibilityChange: function(e) {
737 if (document.hidden || document.webkitHidden || e.type == 'blur') {
746 * @param {SoundBuffer} soundBuffer
748 playSound: function(soundBuffer) {
750 var sourceNode = this.audioContext.createBufferSource();
751 sourceNode.buffer = soundBuffer;
752 sourceNode.connect(this.audioContext.destination);
760 * Updates the canvas size taking into
761 * account the backing store pixel ratio and
762 * the device pixel ratio.
764 * See article by Paul Lewis:
765 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
767 * @param {HTMLCanvasElement} canvas
768 * @param {number} opt_width
769 * @param {number} opt_height
770 * @return {boolean} Whether the canvas was scaled.
772 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
773 var context = canvas.getContext('2d');
775 // Query the various pixel ratios
776 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
777 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
778 var ratio = devicePixelRatio / backingStoreRatio;
780 // Upscale the canvas if the two ratios don't match
781 if (devicePixelRatio !== backingStoreRatio) {
783 var oldWidth = opt_width || canvas.width;
784 var oldHeight = opt_height || canvas.height;
786 canvas.width = oldWidth * ratio;
787 canvas.height = oldHeight * ratio;
789 canvas.style.width = oldWidth + 'px';
790 canvas.style.height = oldHeight + 'px';
792 // Scale the context to counter the fact that we've manually scaled
793 // our canvas element.
794 context.scale(ratio, ratio);
803 * @param {number} min
804 * @param {number} max
807 function getRandomNum(min, max) {
808 return Math.floor(Math.random() * (max - min + 1)) + min;
813 * Vibrate on mobile devices.
814 * @param {number} duration Duration of the vibration in milliseconds.
816 function vibrate(duration) {
817 if (IS_MOBILE && window.navigator.vibrate) {
818 window.navigator.vibrate(duration);
824 * Create canvas element.
825 * @param {HTMLElement} container Element to append canvas to.
826 * @param {number} width
827 * @param {number} height
828 * @param {string} opt_classname
829 * @return {HTMLCanvasElement}
831 function createCanvas(container, width, height, opt_classname) {
832 var canvas = document.createElement('canvas');
833 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
834 opt_classname : Runner.classes.CANVAS;
835 canvas.width = width;
836 canvas.height = height;
837 container.appendChild(canvas);
844 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
845 * @param {string} base64String
847 function decodeBase64ToArrayBuffer(base64String) {
848 var len = (base64String.length / 4) * 3;
849 var str = atob(base64String);
850 var arrayBuffer = new ArrayBuffer(len);
851 var bytes = new Uint8Array(arrayBuffer);
853 for (var i = 0; i < len; i++) {
854 bytes[i] = str.charCodeAt(i);
861 * Return the current timestamp.
864 function getTimeStamp() {
865 return IS_IOS ? new Date().getTime() : performance.now();
869 //******************************************************************************
874 * @param {!HTMLCanvasElement} canvas
875 * @param {!HTMLImage} textSprite
876 * @param {!HTMLImage} restartImg
877 * @param {!Object} dimensions Canvas dimensions.
880 function GameOverPanel(canvas, textSprite, restartImg, dimensions) {
881 this.canvas = canvas;
882 this.canvasCtx = canvas.getContext('2d');
883 this.canvasDimensions = dimensions;
884 this.textSprite = textSprite;
885 this.restartImg = restartImg;
891 * Dimensions used in the panel.
894 GameOverPanel.dimensions = {
904 GameOverPanel.prototype = {
906 * Update the panel dimensions.
907 * @param {number} width New canvas width.
908 * @param {number} opt_height Optional new canvas height.
910 updateDimensions: function(width, opt_height) {
911 this.canvasDimensions.WIDTH = width;
913 this.canvasDimensions.HEIGHT = opt_height;
921 var dimensions = GameOverPanel.dimensions;
923 var centerX = this.canvasDimensions.WIDTH / 2;
926 var textSourceX = dimensions.TEXT_X;
927 var textSourceY = dimensions.TEXT_Y;
928 var textSourceWidth = dimensions.TEXT_WIDTH;
929 var textSourceHeight = dimensions.TEXT_HEIGHT;
931 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
932 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
933 var textTargetWidth = dimensions.TEXT_WIDTH;
934 var textTargetHeight = dimensions.TEXT_HEIGHT;
936 var restartSourceWidth = dimensions.RESTART_WIDTH;
937 var restartSourceHeight = dimensions.RESTART_HEIGHT;
938 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
939 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
944 textSourceWidth *= 2;
945 textSourceHeight *= 2;
946 restartSourceWidth *= 2;
947 restartSourceHeight *= 2;
950 // Game over text from sprite.
951 this.canvasCtx.drawImage(this.textSprite,
952 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
953 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
956 this.canvasCtx.drawImage(this.restartImg, 0, 0,
957 restartSourceWidth, restartSourceHeight,
958 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
959 dimensions.RESTART_HEIGHT);
964 //******************************************************************************
967 * Check for a collision.
968 * @param {!Obstacle} obstacle
969 * @param {!Trex} tRex T-rex object.
970 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
972 * @return {Array.<CollisionBox>}
974 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
975 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
977 // Adjustments are made to the bounding box as there is a 1 pixel white
978 // border around the t-rex and obstacles.
979 var tRexBox = new CollisionBox(
982 tRex.config.WIDTH - 2,
983 tRex.config.HEIGHT - 2);
985 var obstacleBox = new CollisionBox(
988 obstacle.typeConfig.width * obstacle.size - 2,
989 obstacle.typeConfig.height - 2);
993 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
996 // Simple outer bounds check.
997 if (boxCompare(tRexBox, obstacleBox)) {
998 var collisionBoxes = obstacle.collisionBoxes;
999 var tRexCollisionBoxes = Trex.collisionBoxes;
1001 // Detailed axis aligned box check.
1002 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
1003 for (var i = 0; i < collisionBoxes.length; i++) {
1004 // Adjust the box to actual positions.
1006 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
1007 var adjObstacleBox =
1008 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
1009 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
1011 // Draw boxes for debug.
1012 if (opt_canvasCtx) {
1013 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1017 return [adjTrexBox, adjObstacleBox];
1027 * Adjust the collision box.
1028 * @param {!CollisionBox} box The original box.
1029 * @param {!CollisionBox} adjustment Adjustment box.
1030 * @return {CollisionBox} The adjusted collision box object.
1032 function createAdjustedCollisionBox(box, adjustment) {
1033 return new CollisionBox(
1034 box.x + adjustment.x,
1035 box.y + adjustment.y,
1042 * Draw the collision boxes for debug.
1044 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1046 canvasCtx.strokeStyle = '#f00';
1047 canvasCtx.strokeRect(tRexBox.x, tRexBox.y,
1048 tRexBox.width, tRexBox.height);
1050 canvasCtx.strokeStyle = '#0f0';
1051 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1052 obstacleBox.width, obstacleBox.height);
1053 canvasCtx.restore();
1058 * Compare two collision boxes for a collision.
1059 * @param {CollisionBox} tRexBox
1060 * @param {CollisionBox} obstacleBox
1061 * @return {boolean} Whether the boxes intersected.
1063 function boxCompare(tRexBox, obstacleBox) {
1064 var crashed = false;
1065 var tRexBoxX = tRexBox.x;
1066 var tRexBoxY = tRexBox.y;
1068 var obstacleBoxX = obstacleBox.x;
1069 var obstacleBoxY = obstacleBox.y;
1071 // Axis-Aligned Bounding Box method.
1072 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1073 tRexBox.x + tRexBox.width > obstacleBoxX &&
1074 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1075 tRexBox.height + tRexBox.y > obstacleBox.y) {
1083 //******************************************************************************
1086 * Collision box object.
1087 * @param {number} x X position.
1088 * @param {number} y Y Position.
1089 * @param {number} w Width.
1090 * @param {number} h Height.
1092 function CollisionBox(x, y, w, h) {
1100 //******************************************************************************
1104 * @param {HTMLCanvasCtx} canvasCtx
1105 * @param {Obstacle.type} type
1106 * @param {image} obstacleImg Image sprite.
1107 * @param {Object} dimensions
1108 * @param {number} gapCoefficient Mutipler in determining the gap.
1109 * @param {number} speed
1111 function Obstacle(canvasCtx, type, obstacleImg, dimensions,
1112 gapCoefficient, speed) {
1114 this.canvasCtx = canvasCtx;
1115 this.image = obstacleImg;
1116 this.typeConfig = type;
1117 this.gapCoefficient = gapCoefficient;
1118 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1119 this.dimensions = dimensions;
1120 this.remove = false;
1122 this.yPos = this.typeConfig.yPos;
1124 this.collisionBoxes = [];
1131 * Coefficient for calculating the maximum gap.
1134 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1137 * Maximum obstacle grouping count.
1140 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1143 Obstacle.prototype = {
1145 * Initialise the DOM for the obstacle.
1146 * @param {number} speed
1148 init: function(speed) {
1149 this.cloneCollisionBoxes();
1151 // Only allow sizing if we're at the right speed.
1152 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1156 this.width = this.typeConfig.width * this.size;
1157 this.xPos = this.dimensions.WIDTH - this.width;
1161 // Make collision box adjustments,
1162 // Central box is adjusted to the size as one box.
1163 // ____ ______ ________
1164 // _| |-| _| |-| _| |-|
1165 // | |<->| | | |<--->| | | |<----->| |
1166 // | | 1 | | | | 2 | | | | 3 | |
1167 // |_|___|_| |_|_____|_| |_|_______|_|
1169 if (this.size > 1) {
1170 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1171 this.collisionBoxes[2].width;
1172 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1175 this.gap = this.getGap(this.gapCoefficient, speed);
1179 * Draw and crop based on size.
1182 var sourceWidth = this.typeConfig.width;
1183 var sourceHeight = this.typeConfig.height;
1186 sourceWidth = sourceWidth * 2;
1187 sourceHeight = sourceHeight * 2;
1191 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1));
1192 this.canvasCtx.drawImage(this.image,
1194 sourceWidth * this.size, sourceHeight,
1195 this.xPos, this.yPos,
1196 this.typeConfig.width * this.size, this.typeConfig.height);
1200 * Obstacle frame update.
1201 * @param {number} deltaTime
1202 * @param {number} speed
1204 update: function(deltaTime, speed) {
1206 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1209 if (!this.isVisible()) {
1216 * Calculate a random gap size.
1217 * - Minimum gap gets wider as speed increses
1218 * @param {number} gapCoefficient
1219 * @param {number} speed
1220 * @return {number} The gap size.
1222 getGap: function(gapCoefficient, speed) {
1223 var minGap = Math.round(this.width * speed +
1224 this.typeConfig.minGap * gapCoefficient);
1225 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1226 return getRandomNum(minGap, maxGap);
1230 * Check if obstacle is visible.
1231 * @return {boolean} Whether the obstacle is in the game area.
1233 isVisible: function() {
1234 return this.xPos + this.width > 0;
1238 * Make a copy of the collision boxes, since these will change based on
1239 * obstacle type and size.
1241 cloneCollisionBoxes: function() {
1242 var collisionBoxes = this.typeConfig.collisionBoxes;
1244 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1245 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1246 collisionBoxes[i].y, collisionBoxes[i].width,
1247 collisionBoxes[i].height);
1254 * Obstacle definitions.
1255 * minGap: minimum pixel space betweeen obstacles.
1256 * multipleSpeed: Speed at which multiples are allowed.
1260 type: 'CACTUS_SMALL',
1261 className: ' cactus cactus-small ',
1268 new CollisionBox(0, 7, 5, 27),
1269 new CollisionBox(4, 0, 6, 34),
1270 new CollisionBox(10, 4, 7, 14)
1274 type: 'CACTUS_LARGE',
1275 className: ' cactus cactus-large ',
1282 new CollisionBox(0, 12, 7, 38),
1283 new CollisionBox(8, 0, 7, 49),
1284 new CollisionBox(13, 10, 10, 38)
1290 //******************************************************************************
1292 * T-rex game character.
1293 * @param {HTMLCanvas} canvas
1294 * @param {HTMLImage} image Character image.
1297 function Trex(canvas, image) {
1298 this.canvas = canvas;
1299 this.canvasCtx = canvas.getContext('2d');
1303 // Position when on the ground.
1304 this.groundYPos = 0;
1305 this.currentFrame = 0;
1306 this.currentAnimFrames = [];
1307 this.blinkDelay = 0;
1308 this.animStartTime = 0;
1310 this.msPerFrame = 1000 / FPS;
1311 this.config = Trex.config;
1313 this.status = Trex.status.WAITING;
1315 this.jumping = false;
1316 this.jumpVelocity = 0;
1317 this.reachedMinHeight = false;
1318 this.speedDrop = false;
1327 * T-rex player config.
1334 INIITAL_JUMP_VELOCITY: -10,
1335 INTRO_DURATION: 1500,
1336 MAX_JUMP_HEIGHT: 30,
1337 MIN_JUMP_HEIGHT: 30,
1338 SPEED_DROP_COEFFICIENT: 3,
1346 * Used in collision detection.
1347 * @type {Array.<CollisionBox>}
1349 Trex.collisionBoxes = [
1350 new CollisionBox(1, -1, 30, 26),
1351 new CollisionBox(32, 0, 8, 16),
1352 new CollisionBox(10, 35, 14, 8),
1353 new CollisionBox(1, 24, 29, 5),
1354 new CollisionBox(5, 30, 21, 4),
1355 new CollisionBox(9, 34, 15, 4)
1371 * Blinking coefficient.
1374 Trex.BLINK_TIMING = 7000;
1378 * Animation config for different states.
1384 msPerFrame: 1000 / 3
1388 msPerFrame: 1000 / 12
1392 msPerFrame: 1000 / 60
1396 msPerFrame: 1000 / 60
1403 * T-rex player initaliser.
1404 * Sets the t-rex to blink at random intervals.
1407 this.blinkDelay = this.setBlinkDelay();
1408 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1409 Runner.config.BOTTOM_PAD;
1410 this.yPos = this.groundYPos;
1411 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1414 this.update(0, Trex.status.WAITING);
1418 * Setter for the jump velocity.
1419 * The approriate drop velocity is also set.
1421 setJumpVelocity: function(setting) {
1422 this.config.INIITAL_JUMP_VELOCITY = -setting;
1423 this.config.DROP_VELOCITY = -setting / 2;
1427 * Set the animation status.
1428 * @param {!number} deltaTime
1429 * @param {Trex.status} status Optional status to switch to.
1431 update: function(deltaTime, opt_status) {
1432 this.timer += deltaTime;
1434 // Update the status.
1436 this.status = opt_status;
1437 this.currentFrame = 0;
1438 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1439 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1441 if (opt_status == Trex.status.WAITING) {
1442 this.animStartTime = getTimeStamp();
1443 this.setBlinkDelay();
1447 // Game intro animation, T-rex moves in from the left.
1448 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1449 this.xPos += Math.round((this.config.START_X_POS /
1450 this.config.INTRO_DURATION) * deltaTime);
1453 if (this.status == Trex.status.WAITING) {
1454 this.blink(getTimeStamp());
1456 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1459 // Update the frame position.
1460 if (this.timer >= this.msPerFrame) {
1461 this.currentFrame = this.currentFrame ==
1462 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1468 * Draw the t-rex to a particular position.
1472 draw: function(x, y) {
1475 var sourceWidth = this.config.WIDTH;
1476 var sourceHeight = this.config.HEIGHT;
1485 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1486 sourceWidth, sourceHeight,
1487 this.xPos, this.yPos,
1488 this.config.WIDTH, this.config.HEIGHT);
1492 * Sets a random time for the blink to happen.
1494 setBlinkDelay: function() {
1495 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1499 * Make t-rex blink at random intervals.
1500 * @param {number} time Current time in milliseconds.
1502 blink: function(time) {
1503 var deltaTime = time - this.animStartTime;
1505 if (deltaTime >= this.blinkDelay) {
1506 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1508 if (this.currentFrame == 1) {
1509 // Set new random delay to blink.
1510 this.setBlinkDelay();
1511 this.animStartTime = time;
1517 * Initialise a jump.
1519 startJump: function() {
1520 if (!this.jumping) {
1521 this.update(0, Trex.status.JUMPING);
1522 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY;
1523 this.jumping = true;
1524 this.reachedMinHeight = false;
1525 this.speedDrop = false;
1530 * Jump is complete, falling down.
1532 endJump: function() {
1533 if (this.reachedMinHeight &&
1534 this.jumpVelocity < this.config.DROP_VELOCITY) {
1535 this.jumpVelocity = this.config.DROP_VELOCITY;
1540 * Update frame for a jump.
1541 * @param {number} deltaTime
1543 updateJump: function(deltaTime) {
1544 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1545 var framesElapsed = deltaTime / msPerFrame;
1547 // Speed drop makes Trex fall faster.
1548 if (this.speedDrop) {
1549 this.yPos += Math.round(this.jumpVelocity *
1550 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1552 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1555 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1557 // Minimum height has been reached.
1558 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1559 this.reachedMinHeight = true;
1562 // Reached max height
1563 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1567 // Back down at ground level. Jump completed.
1568 if (this.yPos > this.groundYPos) {
1573 this.update(deltaTime);
1577 * Set the speed drop. Immediately cancels the current jump.
1579 setSpeedDrop: function() {
1580 this.speedDrop = true;
1581 this.jumpVelocity = 1;
1585 * Reset the t-rex to running at start of game.
1588 this.yPos = this.groundYPos;
1589 this.jumpVelocity = 0;
1590 this.jumping = false;
1591 this.update(0, Trex.status.RUNNING);
1592 this.midair = false;
1593 this.speedDrop = false;
1599 //******************************************************************************
1602 * Handles displaying the distance meter.
1603 * @param {!HTMLCanvasElement} canvas
1604 * @param {!HTMLImage} spriteSheet Image sprite.
1605 * @param {number} canvasWidth
1608 function DistanceMeter(canvas, spriteSheet, canvasWidth) {
1609 this.canvas = canvas;
1610 this.canvasCtx = canvas.getContext('2d');
1611 this.image = spriteSheet;
1615 this.currentDistance = 0;
1618 this.container = null;
1621 this.acheivement = false;
1622 this.defaultString = '';
1623 this.flashTimer = 0;
1624 this.flashIterations = 0;
1626 this.config = DistanceMeter.config;
1627 this.init(canvasWidth);
1634 DistanceMeter.dimensions = {
1642 * Y positioning of the digits in the sprite sheet.
1643 * X position is always 0.
1644 * @type {array.<number>}
1646 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1650 * Distance meter config.
1653 DistanceMeter.config = {
1654 // Number of digits.
1655 MAX_DISTANCE_UNITS: 5,
1657 // Distance that causes achievement animation.
1658 ACHIEVEMENT_DISTANCE: 100,
1660 // Used for conversion from pixel distance to a scaled unit.
1663 // Flash duration in milliseconds.
1664 FLASH_DURATION: 1000 / 4,
1666 // Flash iterations for achievement animation.
1671 DistanceMeter.prototype = {
1673 * Initialise the distance meter to '00000'.
1674 * @param {number} width Canvas width in px.
1676 init: function(width) {
1677 var maxDistanceStr = '';
1679 this.calcXPos(width);
1680 this.maxScore = this.config.MAX_DISTANCE_UNITS;
1681 for (var i = 0; i < this.config.MAX_DISTANCE_UNITS; i++) {
1683 this.defaultString += '0';
1684 maxDistanceStr += '9';
1687 this.maxScore = parseInt(maxDistanceStr);
1691 * Calculate the xPos in the canvas.
1692 * @param {number} canvasWidth
1694 calcXPos: function(canvasWidth) {
1695 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1696 (this.config.MAX_DISTANCE_UNITS + 1));
1700 * Draw a digit to canvas.
1701 * @param {number} digitPos Position of the digit.
1702 * @param {number} value Digit value 0-9.
1703 * @param {boolean} opt_highScore Whether drawing the high score.
1705 draw: function(digitPos, value, opt_highScore) {
1706 var sourceWidth = DistanceMeter.dimensions.WIDTH;
1707 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1708 var sourceX = DistanceMeter.dimensions.WIDTH * value;
1710 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1711 var targetY = this.y;
1712 var targetWidth = DistanceMeter.dimensions.WIDTH;
1713 var targetHeight = DistanceMeter.dimensions.HEIGHT;
1715 // For high DPI we 2x source values.
1722 this.canvasCtx.save();
1724 if (opt_highScore) {
1725 // Left of the current score.
1726 var highScoreX = this.x - (this.config.MAX_DISTANCE_UNITS * 2) *
1727 DistanceMeter.dimensions.WIDTH;
1728 this.canvasCtx.translate(highScoreX, this.y);
1730 this.canvasCtx.translate(this.x, this.y);
1733 this.canvasCtx.drawImage(this.image, sourceX, 0,
1734 sourceWidth, sourceHeight,
1736 targetWidth, targetHeight
1739 this.canvasCtx.restore();
1743 * Covert pixel distance to a 'real' distance.
1744 * @param {number} distance Pixel distance ran.
1745 * @return {number} The 'real' distance ran.
1747 getActualDistance: function(distance) {
1749 Math.round(distance * this.config.COEFFICIENT) : 0;
1753 * Update the distance meter.
1754 * @param {number} deltaTime
1755 * @param {number} distance
1756 * @return {boolean} Whether the acheivement sound fx should be played.
1758 update: function(deltaTime, distance) {
1760 var playSound = false;
1762 if (!this.acheivement) {
1763 distance = this.getActualDistance(distance);
1766 // Acheivement unlocked
1767 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1768 // Flash score and play sound.
1769 this.acheivement = true;
1770 this.flashTimer = 0;
1774 // Create a string representation of the distance with leading 0.
1775 var distanceStr = (this.defaultString +
1776 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1777 this.digits = distanceStr.split('');
1779 this.digits = this.defaultString.split('');
1782 // Control flashing of the score on reaching acheivement.
1783 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1784 this.flashTimer += deltaTime;
1786 if (this.flashTimer < this.config.FLASH_DURATION) {
1788 } else if (this.flashTimer >
1789 this.config.FLASH_DURATION * 2) {
1790 this.flashTimer = 0;
1791 this.flashIterations++;
1794 this.acheivement = false;
1795 this.flashIterations = 0;
1796 this.flashTimer = 0;
1800 // Draw the digits if not flashing.
1802 for (var i = this.digits.length - 1; i >= 0; i--) {
1803 this.draw(i, parseInt(this.digits[i]));
1807 this.drawHighScore();
1813 * Draw the high score.
1815 drawHighScore: function() {
1816 this.canvasCtx.save();
1817 this.canvasCtx.globalAlpha = .8;
1818 for (var i = this.highScore.length - 1; i >= 0; i--) {
1819 this.draw(i, parseInt(this.highScore[i], 10), true);
1821 this.canvasCtx.restore();
1825 * Set the highscore as a array string.
1826 * Position of char in the sprite: H - 10, I - 11.
1827 * @param {number} distance Distance ran in pixels.
1829 setHighScore: function(distance) {
1830 distance = this.getActualDistance(distance);
1831 var highScoreStr = (this.defaultString +
1832 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1834 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
1838 * Reset the distance meter back to '00000'.
1842 this.acheivement = false;
1847 //******************************************************************************
1850 * Cloud background item.
1851 * Similar to an obstacle object but without collision boxes.
1852 * @param {HTMLCanvasElement} canvas Canvas element.
1853 * @param {Image} cloudImg
1854 * @param {number} containerWidth
1856 function Cloud(canvas, cloudImg, containerWidth) {
1857 this.canvas = canvas;
1858 this.canvasCtx = this.canvas.getContext('2d');
1859 this.image = cloudImg;
1860 this.containerWidth = containerWidth;
1861 this.xPos = containerWidth;
1863 this.remove = false;
1864 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
1865 Cloud.config.MAX_CLOUD_GAP);
1872 * Cloud object config.
1887 * Initialise the cloud. Sets the Cloud height.
1890 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
1891 Cloud.config.MIN_SKY_LEVEL);
1899 this.canvasCtx.save();
1900 var sourceWidth = Cloud.config.WIDTH;
1901 var sourceHeight = Cloud.config.HEIGHT;
1904 sourceWidth = sourceWidth * 2;
1905 sourceHeight = sourceHeight * 2;
1908 this.canvasCtx.drawImage(this.image, 0, 0,
1909 sourceWidth, sourceHeight,
1910 this.xPos, this.yPos,
1911 Cloud.config.WIDTH, Cloud.config.HEIGHT);
1913 this.canvasCtx.restore();
1917 * Update the cloud position.
1918 * @param {number} speed
1920 update: function(speed) {
1922 this.xPos -= Math.ceil(speed);
1925 // Mark as removeable if no longer in the canvas.
1926 if (!this.isVisible()) {
1933 * Check if the cloud is visible on the stage.
1936 isVisible: function() {
1937 return this.xPos + Cloud.config.WIDTH > 0;
1942 //******************************************************************************
1946 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
1947 * @param {HTMLCanvasElement} canvas
1948 * @param {HTMLImage} bgImg Horizon line sprite.
1951 function HorizonLine(canvas, bgImg) {
1953 this.canvas = canvas;
1954 this.canvasCtx = canvas.getContext('2d');
1955 this.sourceDimensions = {};
1956 this.dimensions = HorizonLine.dimensions;
1957 this.sourceXPos = [0, this.dimensions.WIDTH];
1960 this.bumpThreshold = 0.5;
1962 this.setSourceDimensions();
1968 * Horizon line dimensions.
1971 HorizonLine.dimensions = {
1978 HorizonLine.prototype = {
1980 * Set the source dimensions of the horizon line.
1982 setSourceDimensions: function() {
1984 for (var dimension in HorizonLine.dimensions) {
1986 if (dimension != 'YPOS') {
1987 this.sourceDimensions[dimension] =
1988 HorizonLine.dimensions[dimension] * 2;
1991 this.sourceDimensions[dimension] =
1992 HorizonLine.dimensions[dimension];
1994 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
1997 this.xPos = [0, HorizonLine.dimensions.WIDTH];
1998 this.yPos = HorizonLine.dimensions.YPOS;
2002 * Return the crop x position of a type.
2004 getRandomType: function() {
2005 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
2009 * Draw the horizon line.
2012 this.canvasCtx.drawImage(this.image, this.sourceXPos[0], 0,
2013 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2014 this.xPos[0], this.yPos,
2015 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2017 this.canvasCtx.drawImage(this.image, this.sourceXPos[1], 0,
2018 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2019 this.xPos[1], this.yPos,
2020 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2024 * Update the x position of an indivdual piece of the line.
2025 * @param {number} pos Line position.
2026 * @param {number} increment
2028 updateXPos: function(pos, increment) {
2030 var line2 = pos == 0 ? 1 : 0;
2032 this.xPos[line1] -= increment;
2033 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2035 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2036 this.xPos[line1] += this.dimensions.WIDTH * 2;
2037 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2038 this.sourceXPos[line1] = this.getRandomType();
2043 * Update the horizon line.
2044 * @param {number} deltaTime
2045 * @param {number} speed
2047 update: function(deltaTime, speed) {
2048 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2050 if (this.xPos[0] <= 0) {
2051 this.updateXPos(0, increment);
2053 this.updateXPos(1, increment);
2059 * Reset horizon to the starting position.
2063 this.xPos[1] = HorizonLine.dimensions.WIDTH;
2068 //******************************************************************************
2071 * Horizon background class.
2072 * @param {HTMLCanvasElement} canvas
2073 * @param {Array.<HTMLImageElement>} images
2074 * @param {object} dimensions Canvas dimensions.
2075 * @param {number} gapCoefficient
2078 function Horizon(canvas, images, dimensions, gapCoefficient) {
2079 this.canvas = canvas;
2080 this.canvasCtx = this.canvas.getContext('2d');
2081 this.config = Horizon.config;
2082 this.dimensions = dimensions;
2083 this.gapCoefficient = gapCoefficient;
2084 this.obstacles = [];
2085 this.horizonOffsets = [0, 0];
2086 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2090 this.cloudImg = images.CLOUD;
2091 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2094 this.horizonImg = images.HORIZON;
2095 this.horizonLine = null;
2098 this.obstacleImgs = {
2099 CACTUS_SMALL: images.CACTUS_SMALL,
2100 CACTUS_LARGE: images.CACTUS_LARGE
2112 BG_CLOUD_SPEED: 0.2,
2113 BUMPY_THRESHOLD: .3,
2114 CLOUD_FREQUENCY: .5,
2120 Horizon.prototype = {
2122 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2126 this.horizonLine = new HorizonLine(this.canvas, this.horizonImg);
2130 * @param {number} deltaTime
2131 * @param {number} currentSpeed
2132 * @param {boolean} updateObstacles Used as an override to prevent
2133 * the obstacles from being updated / added. This happens in the
2136 update: function(deltaTime, currentSpeed, updateObstacles) {
2137 this.runningTime += deltaTime;
2138 this.horizonLine.update(deltaTime, currentSpeed);
2139 this.updateClouds(deltaTime, currentSpeed);
2141 if (updateObstacles) {
2142 this.updateObstacles(deltaTime, currentSpeed);
2147 * Update the cloud positions.
2148 * @param {number} deltaTime
2149 * @param {number} currentSpeed
2151 updateClouds: function(deltaTime, speed) {
2152 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2153 var numClouds = this.clouds.length;
2156 for (var i = numClouds - 1; i >= 0; i--) {
2157 this.clouds[i].update(cloudSpeed);
2160 var lastCloud = this.clouds[numClouds - 1];
2162 // Check for adding a new cloud.
2163 if (numClouds < this.config.MAX_CLOUDS &&
2164 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2165 this.cloudFrequency > Math.random()) {
2169 // Remove expired clouds.
2170 this.clouds = this.clouds.filter(function(obj) {
2177 * Update the obstacle positions.
2178 * @param {number} deltaTime
2179 * @param {number} currentSpeed
2181 updateObstacles: function(deltaTime, currentSpeed) {
2182 // Obstacles, move to Horizon layer.
2183 var updatedObstacles = this.obstacles.slice(0);
2185 for (var i = 0; i < this.obstacles.length; i++) {
2186 var obstacle = this.obstacles[i];
2187 obstacle.update(deltaTime, currentSpeed);
2189 // Clean up existing obstacles.
2190 if (obstacle.remove) {
2191 updatedObstacles.shift();
2194 this.obstacles = updatedObstacles;
2196 if (this.obstacles.length > 0) {
2197 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2199 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2200 lastObstacle.isVisible() &&
2201 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2202 this.dimensions.WIDTH) {
2203 this.addNewObstacle(currentSpeed);
2204 lastObstacle.followingObstacleCreated = true;
2207 // Create new obstacles.
2208 this.addNewObstacle(currentSpeed);
2213 * Add a new obstacle.
2214 * @param {number} currentSpeed
2216 addNewObstacle: function(currentSpeed) {
2217 var obstacleTypeIndex =
2218 getRandomNum(0, Obstacle.types.length - 1);
2219 var obstacleType = Obstacle.types[obstacleTypeIndex];
2220 var obstacleImg = this.obstacleImgs[obstacleType.type];
2222 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2223 obstacleImg, this.dimensions, this.gapCoefficient, currentSpeed));
2227 * Reset the horizon layer.
2228 * Remove existing obstacles and reposition the horizon line.
2231 this.obstacles = [];
2232 this.horizonLine.reset();
2236 * Update the canvas width and scaling.
2237 * @param {number} width Canvas width.
2238 * @param {number} height Canvas height.
2240 resize: function(width, height) {
2241 this.canvas.width = width;
2242 this.canvas.height = height;
2246 * Add a new cloud to the horizon.
2248 addCloud: function() {
2249 this.clouds.push(new Cloud(this.canvas, this.cloudImg,
2250 this.dimensions.WIDTH));