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;
91 var IS_IOS = window.navigator.userAgent.indexOf('CriOS') > -1;
94 * Default game configuration.
102 CLOUD_FREQUENCY: 0.5,
103 GAMEOVER_CLEAR_TIME: 750,
104 GAP_COEFFICIENT: 0.6,
106 INITIAL_JUMP_VELOCITY: 12,
108 MAX_OBSTACLE_LENGTH: 3,
111 MOBILE_SPEED_COEFFICIENT: 1.2,
112 RESOURCE_TEMPLATE_ID: 'audio-resources',
114 SPEED_DROP_COEFFICIENT: 3
119 * Default dimensions.
122 Runner.defaultDimensions = {
123 WIDTH: DEFAULT_WIDTH,
133 CANVAS: 'runner-canvas',
134 CONTAINER: 'runner-container',
136 ICON: 'icon-offline',
137 TOUCH_CONTROLLER: 'controller'
143 * @enum {array.<object>}
145 Runner.imageSources = {
147 {name: 'CACTUS_LARGE', id: '1x-obstacle-large'},
148 {name: 'CACTUS_SMALL', id: '1x-obstacle-small'},
149 {name: 'CLOUD', id: '1x-cloud'},
150 {name: 'HORIZON', id: '1x-horizon'},
151 {name: 'RESTART', id: '1x-restart'},
152 {name: 'TEXT_SPRITE', id: '1x-text'},
153 {name: 'TREX', id: '1x-trex'}
156 {name: 'CACTUS_LARGE', id: '2x-obstacle-large'},
157 {name: 'CACTUS_SMALL', id: '2x-obstacle-small'},
158 {name: 'CLOUD', id: '2x-cloud'},
159 {name: 'HORIZON', id: '2x-horizon'},
160 {name: 'RESTART', id: '2x-restart'},
161 {name: 'TEXT_SPRITE', id: '2x-text'},
162 {name: 'TREX', id: '2x-trex'}
168 * Sound FX. Reference to the ID of the audio tag on interstitial page.
172 BUTTON_PRESS: 'offline-sound-press',
173 HIT: 'offline-sound-hit',
174 SCORE: 'offline-sound-reached'
183 JUMP: {'38': 1, '32': 1}, // Up, spacebar
184 DUCK: {'40': 1}, // Down
185 RESTART: {'13': 1} // Enter
190 * Runner event names.
194 ANIM_END: 'webkitAnimationEnd',
198 MOUSEDOWN: 'mousedown',
201 TOUCHEND: 'touchend',
202 TOUCHSTART: 'touchstart',
203 VISIBILITY: 'visibilitychange',
212 * Setting individual settings for debugging.
213 * @param {string} setting
216 updateConfigSetting: function(setting, value) {
217 if (setting in this.config && value != undefined) {
218 this.config[setting] = value;
222 case 'MIN_JUMP_HEIGHT':
223 case 'SPEED_DROP_COEFFICIENT':
224 this.tRex.config[setting] = value;
226 case 'INITIAL_JUMP_VELOCITY':
227 this.tRex.setJumpVelocity(value);
230 this.setSpeed(value);
237 * Load and cache the image assets from the page.
239 loadImages: function() {
240 var imageSources = IS_HIDPI ? Runner.imageSources.HDPI :
241 Runner.imageSources.LDPI;
243 var numImages = imageSources.length;
245 for (var i = numImages - 1; i >= 0; i--) {
246 var imgSource = imageSources[i];
247 this.images[imgSource.name] = document.getElementById(imgSource.id);
253 * Load and decode base 64 encoded sounds.
255 loadSounds: function() {
257 this.audioContext = new AudioContext();
258 var resourceTemplate =
259 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
261 for (var sound in Runner.sounds) {
263 resourceTemplate.getElementById(Runner.sounds[sound]).src;
264 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
265 var buffer = decodeBase64ToArrayBuffer(soundSrc);
267 // Async, so no guarantee of order in array.
268 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
269 this.soundFx[index] = audioData;
270 }.bind(this, sound));
276 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
277 * @param {number} opt_speed
279 setSpeed: function(opt_speed) {
280 var speed = opt_speed || this.currentSpeed;
282 // Reduce the speed on smaller mobile screens.
283 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
284 var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
285 this.config.MOBILE_SPEED_COEFFICIENT;
286 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
287 } else if (opt_speed) {
288 this.currentSpeed = opt_speed;
296 // Hide the static icon.
297 document.querySelector('.' + Runner.classes.ICON).style.visibility =
300 this.adjustDimensions();
303 this.containerEl = document.createElement('div');
304 this.containerEl.className = Runner.classes.CONTAINER;
306 // Player canvas container.
307 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
308 this.dimensions.HEIGHT, Runner.classes.PLAYER);
310 this.canvasCtx = this.canvas.getContext('2d');
311 this.canvasCtx.fillStyle = '#f7f7f7';
312 this.canvasCtx.fill();
313 Runner.updateCanvasScaling(this.canvas);
315 // Horizon contains clouds, obstacles and the ground.
316 this.horizon = new Horizon(this.canvas, this.images, this.dimensions,
317 this.config.GAP_COEFFICIENT);
320 this.distanceMeter = new DistanceMeter(this.canvas,
321 this.images.TEXT_SPRITE, this.dimensions.WIDTH);
324 this.tRex = new Trex(this.canvas, this.images.TREX);
326 this.outerContainerEl.appendChild(this.containerEl);
329 this.createTouchController();
332 this.startListening();
335 window.addEventListener(Runner.events.RESIZE,
336 this.debounceResize.bind(this));
340 * Create the touch controller. A div that covers whole screen.
342 createTouchController: function() {
343 this.touchController = document.createElement('div');
344 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
348 * Debounce the resize event.
350 debounceResize: function() {
351 if (!this.resizeTimerId_) {
352 this.resizeTimerId_ =
353 setInterval(this.adjustDimensions.bind(this), 250);
358 * Adjust game space dimensions on resize.
360 adjustDimensions: function() {
361 clearInterval(this.resizeTimerId_);
362 this.resizeTimerId_ = null;
364 var boxStyles = window.getComputedStyle(this.outerContainerEl);
365 var padding = Number(boxStyles.paddingLeft.substr(0,
366 boxStyles.paddingLeft.length - 2));
368 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
370 // Redraw the elements back onto the canvas.
372 this.canvas.width = this.dimensions.WIDTH;
373 this.canvas.height = this.dimensions.HEIGHT;
375 Runner.updateCanvasScaling(this.canvas);
377 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
379 this.horizon.update(0, 0, true);
382 // Outer container and distance meter.
383 if (this.activated || this.crashed) {
384 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
385 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
386 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
389 this.tRex.draw(0, 0);
393 if (this.crashed && this.gameOverPanel) {
394 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
395 this.gameOverPanel.draw();
401 * Play the game intro.
402 * Canvas container width expands out to the full width.
404 playIntro: function() {
405 if (!this.started && !this.crashed) {
406 this.playingIntro = true;
407 this.tRex.playingIntro = true;
409 // CSS animation definition.
410 var keyframes = '@-webkit-keyframes intro { ' +
411 'from { width:' + Trex.config.WIDTH + 'px }' +
412 'to { width: ' + this.dimensions.WIDTH + 'px }' +
414 document.styleSheets[0].insertRule(keyframes, 0);
416 this.containerEl.addEventListener(Runner.events.ANIM_END,
417 this.startGame.bind(this));
419 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
420 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
422 if (this.touchController) {
423 this.outerContainerEl.appendChild(this.touchController);
425 this.activated = true;
427 } else if (this.crashed) {
434 * Update the game status to started.
436 startGame: function() {
437 this.runningTime = 0;
438 this.playingIntro = false;
439 this.tRex.playingIntro = false;
440 this.containerEl.style.webkitAnimation = '';
443 // Handle tabbing off the page. Pause the current game.
444 window.addEventListener(Runner.events.VISIBILITY,
445 this.onVisibilityChange.bind(this));
447 window.addEventListener(Runner.events.BLUR,
448 this.onVisibilityChange.bind(this));
450 window.addEventListener(Runner.events.FOCUS,
451 this.onVisibilityChange.bind(this));
454 clearCanvas: function() {
455 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
456 this.dimensions.HEIGHT);
460 * Update the game frame.
463 this.drawPending = false;
465 var now = getTimeStamp();
466 var deltaTime = now - (this.time || now);
469 if (this.activated) {
472 if (this.tRex.jumping) {
473 this.tRex.updateJump(deltaTime, this.config);
476 this.runningTime += deltaTime;
477 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
479 // First jump triggers the intro.
480 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
484 // The horizon doesn't move until the intro is over.
485 if (this.playingIntro) {
486 this.horizon.update(0, this.currentSpeed, hasObstacles);
488 deltaTime = !this.started ? 0 : deltaTime;
489 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
492 // Check for collisions.
493 var collision = hasObstacles &&
494 checkForCollision(this.horizon.obstacles[0], this.tRex);
497 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
499 if (this.currentSpeed < this.config.MAX_SPEED) {
500 this.currentSpeed += this.config.ACCELERATION;
506 if (this.distanceMeter.getActualDistance(this.distanceRan) >
507 this.distanceMeter.maxScore) {
508 this.distanceRan = 0;
511 var playAcheivementSound = this.distanceMeter.update(deltaTime,
512 Math.ceil(this.distanceRan));
514 if (playAcheivementSound) {
515 this.playSound(this.soundFx.SCORE);
520 this.tRex.update(deltaTime);
528 handleEvent: function(e) {
529 return (function(evtType, events) {
532 case events.TOUCHSTART:
533 case events.MOUSEDOWN:
537 case events.TOUCHEND:
542 }.bind(this))(e.type, Runner.events);
546 * Bind relevant key / mouse / touch listeners.
548 startListening: function() {
550 document.addEventListener(Runner.events.KEYDOWN, this);
551 document.addEventListener(Runner.events.KEYUP, this);
554 // Mobile only touch devices.
555 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
556 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
557 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
560 document.addEventListener(Runner.events.MOUSEDOWN, this);
561 document.addEventListener(Runner.events.MOUSEUP, this);
566 * Remove all listeners.
568 stopListening: function() {
569 document.removeEventListener(Runner.events.KEYDOWN, this);
570 document.removeEventListener(Runner.events.KEYUP, this);
573 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
574 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
575 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
577 document.removeEventListener(Runner.events.MOUSEDOWN, this);
578 document.removeEventListener(Runner.events.MOUSEUP, this);
586 onKeyDown: function(e) {
587 if (e.target != this.detailsButton) {
588 if (!this.crashed && (Runner.keycodes.JUMP[String(e.keyCode)] ||
589 e.type == Runner.events.TOUCHSTART)) {
590 if (!this.activated) {
592 this.activated = true;
595 if (!this.tRex.jumping) {
596 this.playSound(this.soundFx.BUTTON_PRESS);
597 this.tRex.startJump();
601 if (this.crashed && e.type == Runner.events.TOUCHSTART &&
602 e.currentTarget == this.containerEl) {
607 // Speed drop, activated only when jump key is not pressed.
608 if (Runner.keycodes.DUCK[e.keyCode] && this.tRex.jumping) {
610 this.tRex.setSpeedDrop();
619 onKeyUp: function(e) {
620 var keyCode = String(e.keyCode);
621 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
622 e.type == Runner.events.TOUCHEND ||
623 e.type == Runner.events.MOUSEDOWN;
625 if (this.isRunning() && isjumpKey) {
627 } else if (Runner.keycodes.DUCK[keyCode]) {
628 this.tRex.speedDrop = false;
629 } else if (this.crashed) {
630 // Check that enough time has elapsed before allowing jump key to restart.
631 var deltaTime = getTimeStamp() - this.time;
633 if (Runner.keycodes.RESTART[keyCode] ||
634 (e.type == Runner.events.MOUSEUP && e.target == this.canvas) ||
635 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
636 Runner.keycodes.JUMP[keyCode])) {
639 } else if (this.paused && isjumpKey) {
645 * RequestAnimationFrame wrapper.
648 if (!this.drawPending) {
649 this.drawPending = true;
650 this.raqId = requestAnimationFrame(this.update.bind(this));
655 * Whether the game is running.
658 isRunning: function() {
665 gameOver: function() {
666 this.playSound(this.soundFx.HIT);
671 this.distanceMeter.acheivement = false;
673 this.tRex.update(100, Trex.status.CRASHED);
676 if (!this.gameOverPanel) {
677 this.gameOverPanel = new GameOverPanel(this.canvas,
678 this.images.TEXT_SPRITE, this.images.RESTART,
681 this.gameOverPanel.draw();
684 // Update the high score.
685 if (this.distanceRan > this.highestScore) {
686 this.highestScore = Math.ceil(this.distanceRan);
687 this.distanceMeter.setHighScore(this.highestScore);
690 // Reset the time clock.
691 this.time = getTimeStamp();
695 this.activated = false;
697 cancelAnimationFrame(this.raqId);
703 this.activated = true;
705 this.tRex.update(0, Trex.status.RUNNING);
706 this.time = getTimeStamp();
711 restart: function() {
714 this.runningTime = 0;
715 this.activated = true;
716 this.crashed = false;
717 this.distanceRan = 0;
718 this.setSpeed(this.config.SPEED);
720 this.time = getTimeStamp();
721 this.containerEl.classList.remove(Runner.classes.CRASHED);
723 this.distanceMeter.reset(this.highestScore);
724 this.horizon.reset();
726 this.playSound(this.soundFx.BUTTON_PRESS);
733 * Pause the game if the tab is not in focus.
735 onVisibilityChange: function(e) {
736 if (document.hidden || document.webkitHidden || e.type == 'blur') {
745 * @param {SoundBuffer} soundBuffer
747 playSound: function(soundBuffer) {
749 var sourceNode = this.audioContext.createBufferSource();
750 sourceNode.buffer = soundBuffer;
751 sourceNode.connect(this.audioContext.destination);
759 * Updates the canvas size taking into
760 * account the backing store pixel ratio and
761 * the device pixel ratio.
763 * See article by Paul Lewis:
764 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
766 * @param {HTMLCanvasElement} canvas
767 * @param {number} opt_width
768 * @param {number} opt_height
769 * @return {boolean} Whether the canvas was scaled.
771 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
772 var context = canvas.getContext('2d');
774 // Query the various pixel ratios
775 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
776 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
777 var ratio = devicePixelRatio / backingStoreRatio;
779 // Upscale the canvas if the two ratios don't match
780 if (devicePixelRatio !== backingStoreRatio) {
782 var oldWidth = opt_width || canvas.width;
783 var oldHeight = opt_height || canvas.height;
785 canvas.width = oldWidth * ratio;
786 canvas.height = oldHeight * ratio;
788 canvas.style.width = oldWidth + 'px';
789 canvas.style.height = oldHeight + 'px';
791 // Scale the context to counter the fact that we've manually scaled
792 // our canvas element.
793 context.scale(ratio, ratio);
802 * @param {number} min
803 * @param {number} max
806 function getRandomNum(min, max) {
807 return Math.floor(Math.random() * (max - min + 1)) + min;
812 * Vibrate on mobile devices.
813 * @param {number} duration Duration of the vibration in milliseconds.
815 function vibrate(duration) {
816 if (IS_MOBILE && window.navigator.vibrate) {
817 window.navigator.vibrate(duration);
823 * Create canvas element.
824 * @param {HTMLElement} container Element to append canvas to.
825 * @param {number} width
826 * @param {number} height
827 * @param {string} opt_classname
828 * @return {HTMLCanvasElement}
830 function createCanvas(container, width, height, opt_classname) {
831 var canvas = document.createElement('canvas');
832 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
833 opt_classname : Runner.classes.CANVAS;
834 canvas.width = width;
835 canvas.height = height;
836 container.appendChild(canvas);
843 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
844 * @param {string} base64String
846 function decodeBase64ToArrayBuffer(base64String) {
847 var len = (base64String.length / 4) * 3;
848 var str = atob(base64String);
849 var arrayBuffer = new ArrayBuffer(len);
850 var bytes = new Uint8Array(arrayBuffer);
852 for (var i = 0; i < len; i++) {
853 bytes[i] = str.charCodeAt(i);
860 * Return the current timestamp.
863 function getTimeStamp() {
864 return IS_IOS ? new Date().getTime() : performance.now();
868 //******************************************************************************
873 * @param {!HTMLCanvasElement} canvas
874 * @param {!HTMLImage} textSprite
875 * @param {!HTMLImage} restartImg
876 * @param {!Object} dimensions Canvas dimensions.
879 function GameOverPanel(canvas, textSprite, restartImg, dimensions) {
880 this.canvas = canvas;
881 this.canvasCtx = canvas.getContext('2d');
882 this.canvasDimensions = dimensions;
883 this.textSprite = textSprite;
884 this.restartImg = restartImg;
890 * Dimensions used in the panel.
893 GameOverPanel.dimensions = {
903 GameOverPanel.prototype = {
905 * Update the panel dimensions.
906 * @param {number} width New canvas width.
907 * @param {number} opt_height Optional new canvas height.
909 updateDimensions: function(width, opt_height) {
910 this.canvasDimensions.WIDTH = width;
912 this.canvasDimensions.HEIGHT = opt_height;
920 var dimensions = GameOverPanel.dimensions;
922 var centerX = this.canvasDimensions.WIDTH / 2;
925 var textSourceX = dimensions.TEXT_X;
926 var textSourceY = dimensions.TEXT_Y;
927 var textSourceWidth = dimensions.TEXT_WIDTH;
928 var textSourceHeight = dimensions.TEXT_HEIGHT;
930 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
931 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
932 var textTargetWidth = dimensions.TEXT_WIDTH;
933 var textTargetHeight = dimensions.TEXT_HEIGHT;
935 var restartSourceWidth = dimensions.RESTART_WIDTH;
936 var restartSourceHeight = dimensions.RESTART_HEIGHT;
937 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
938 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
943 textSourceWidth *= 2;
944 textSourceHeight *= 2;
945 restartSourceWidth *= 2;
946 restartSourceHeight *= 2;
949 // Game over text from sprite.
950 this.canvasCtx.drawImage(this.textSprite,
951 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
952 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
955 this.canvasCtx.drawImage(this.restartImg, 0, 0,
956 restartSourceWidth, restartSourceHeight,
957 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
958 dimensions.RESTART_HEIGHT);
963 //******************************************************************************
966 * Check for a collision.
967 * @param {!Obstacle} obstacle
968 * @param {!Trex} tRex T-rex object.
969 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
971 * @return {Array.<CollisionBox>}
973 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
974 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
976 // Adjustments are made to the bounding box as there is a 1 pixel white
977 // border around the t-rex and obstacles.
978 var tRexBox = new CollisionBox(
981 tRex.config.WIDTH - 2,
982 tRex.config.HEIGHT - 2);
984 var obstacleBox = new CollisionBox(
987 obstacle.typeConfig.width * obstacle.size - 2,
988 obstacle.typeConfig.height - 2);
992 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
995 // Simple outer bounds check.
996 if (boxCompare(tRexBox, obstacleBox)) {
997 var collisionBoxes = obstacle.collisionBoxes;
998 var tRexCollisionBoxes = Trex.collisionBoxes;
1000 // Detailed axis aligned box check.
1001 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
1002 for (var i = 0; i < collisionBoxes.length; i++) {
1003 // Adjust the box to actual positions.
1005 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
1006 var adjObstacleBox =
1007 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
1008 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
1010 // Draw boxes for debug.
1011 if (opt_canvasCtx) {
1012 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1016 return [adjTrexBox, adjObstacleBox];
1026 * Adjust the collision box.
1027 * @param {!CollisionBox} box The original box.
1028 * @param {!CollisionBox} adjustment Adjustment box.
1029 * @return {CollisionBox} The adjusted collision box object.
1031 function createAdjustedCollisionBox(box, adjustment) {
1032 return new CollisionBox(
1033 box.x + adjustment.x,
1034 box.y + adjustment.y,
1041 * Draw the collision boxes for debug.
1043 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1045 canvasCtx.strokeStyle = '#f00';
1046 canvasCtx.strokeRect(tRexBox.x, tRexBox.y,
1047 tRexBox.width, tRexBox.height);
1049 canvasCtx.strokeStyle = '#0f0';
1050 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1051 obstacleBox.width, obstacleBox.height);
1052 canvasCtx.restore();
1057 * Compare two collision boxes for a collision.
1058 * @param {CollisionBox} tRexBox
1059 * @param {CollisionBox} obstacleBox
1060 * @return {boolean} Whether the boxes intersected.
1062 function boxCompare(tRexBox, obstacleBox) {
1063 var crashed = false;
1064 var tRexBoxX = tRexBox.x;
1065 var tRexBoxY = tRexBox.y;
1067 var obstacleBoxX = obstacleBox.x;
1068 var obstacleBoxY = obstacleBox.y;
1070 // Axis-Aligned Bounding Box method.
1071 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1072 tRexBox.x + tRexBox.width > obstacleBoxX &&
1073 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1074 tRexBox.height + tRexBox.y > obstacleBox.y) {
1082 //******************************************************************************
1085 * Collision box object.
1086 * @param {number} x X position.
1087 * @param {number} y Y Position.
1088 * @param {number} w Width.
1089 * @param {number} h Height.
1091 function CollisionBox(x, y, w, h) {
1099 //******************************************************************************
1103 * @param {HTMLCanvasCtx} canvasCtx
1104 * @param {Obstacle.type} type
1105 * @param {image} obstacleImg Image sprite.
1106 * @param {Object} dimensions
1107 * @param {number} gapCoefficient Mutipler in determining the gap.
1108 * @param {number} speed
1110 function Obstacle(canvasCtx, type, obstacleImg, dimensions,
1111 gapCoefficient, speed) {
1113 this.canvasCtx = canvasCtx;
1114 this.image = obstacleImg;
1115 this.typeConfig = type;
1116 this.gapCoefficient = gapCoefficient;
1117 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1118 this.dimensions = dimensions;
1119 this.remove = false;
1121 this.yPos = this.typeConfig.yPos;
1123 this.collisionBoxes = [];
1130 * Coefficient for calculating the maximum gap.
1133 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1136 * Maximum obstacle grouping count.
1139 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1142 Obstacle.prototype = {
1144 * Initialise the DOM for the obstacle.
1145 * @param {number} speed
1147 init: function(speed) {
1148 this.cloneCollisionBoxes();
1150 // Only allow sizing if we're at the right speed.
1151 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1155 this.width = this.typeConfig.width * this.size;
1156 this.xPos = this.dimensions.WIDTH - this.width;
1160 // Make collision box adjustments,
1161 // Central box is adjusted to the size as one box.
1162 // ____ ______ ________
1163 // _| |-| _| |-| _| |-|
1164 // | |<->| | | |<--->| | | |<----->| |
1165 // | | 1 | | | | 2 | | | | 3 | |
1166 // |_|___|_| |_|_____|_| |_|_______|_|
1168 if (this.size > 1) {
1169 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1170 this.collisionBoxes[2].width;
1171 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1174 this.gap = this.getGap(this.gapCoefficient, speed);
1178 * Draw and crop based on size.
1181 var sourceWidth = this.typeConfig.width;
1182 var sourceHeight = this.typeConfig.height;
1185 sourceWidth = sourceWidth * 2;
1186 sourceHeight = sourceHeight * 2;
1190 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1));
1191 this.canvasCtx.drawImage(this.image,
1193 sourceWidth * this.size, sourceHeight,
1194 this.xPos, this.yPos,
1195 this.typeConfig.width * this.size, this.typeConfig.height);
1199 * Obstacle frame update.
1200 * @param {number} deltaTime
1201 * @param {number} speed
1203 update: function(deltaTime, speed) {
1205 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1208 if (!this.isVisible()) {
1215 * Calculate a random gap size.
1216 * - Minimum gap gets wider as speed increses
1217 * @param {number} gapCoefficient
1218 * @param {number} speed
1219 * @return {number} The gap size.
1221 getGap: function(gapCoefficient, speed) {
1222 var minGap = Math.round(this.width * speed +
1223 this.typeConfig.minGap * gapCoefficient);
1224 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1225 return getRandomNum(minGap, maxGap);
1229 * Check if obstacle is visible.
1230 * @return {boolean} Whether the obstacle is in the game area.
1232 isVisible: function() {
1233 return this.xPos + this.width > 0;
1237 * Make a copy of the collision boxes, since these will change based on
1238 * obstacle type and size.
1240 cloneCollisionBoxes: function() {
1241 var collisionBoxes = this.typeConfig.collisionBoxes;
1243 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1244 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1245 collisionBoxes[i].y, collisionBoxes[i].width,
1246 collisionBoxes[i].height);
1253 * Obstacle definitions.
1254 * minGap: minimum pixel space betweeen obstacles.
1255 * multipleSpeed: Speed at which multiples are allowed.
1259 type: 'CACTUS_SMALL',
1260 className: ' cactus cactus-small ',
1267 new CollisionBox(0, 7, 5, 27),
1268 new CollisionBox(4, 0, 6, 34),
1269 new CollisionBox(10, 4, 7, 14)
1273 type: 'CACTUS_LARGE',
1274 className: ' cactus cactus-large ',
1281 new CollisionBox(0, 12, 7, 38),
1282 new CollisionBox(8, 0, 7, 49),
1283 new CollisionBox(13, 10, 10, 38)
1289 //******************************************************************************
1291 * T-rex game character.
1292 * @param {HTMLCanvas} canvas
1293 * @param {HTMLImage} image Character image.
1296 function Trex(canvas, image) {
1297 this.canvas = canvas;
1298 this.canvasCtx = canvas.getContext('2d');
1302 // Position when on the ground.
1303 this.groundYPos = 0;
1304 this.currentFrame = 0;
1305 this.currentAnimFrames = [];
1306 this.blinkDelay = 0;
1307 this.animStartTime = 0;
1309 this.msPerFrame = 1000 / FPS;
1310 this.config = Trex.config;
1312 this.status = Trex.status.WAITING;
1314 this.jumping = false;
1315 this.jumpVelocity = 0;
1316 this.reachedMinHeight = false;
1317 this.speedDrop = false;
1326 * T-rex player config.
1333 INIITAL_JUMP_VELOCITY: -10,
1334 INTRO_DURATION: 1500,
1335 MAX_JUMP_HEIGHT: 30,
1336 MIN_JUMP_HEIGHT: 30,
1337 SPEED_DROP_COEFFICIENT: 3,
1345 * Used in collision detection.
1346 * @type {Array.<CollisionBox>}
1348 Trex.collisionBoxes = [
1349 new CollisionBox(1, -1, 30, 26),
1350 new CollisionBox(32, 0, 8, 16),
1351 new CollisionBox(10, 35, 14, 8),
1352 new CollisionBox(1, 24, 29, 5),
1353 new CollisionBox(5, 30, 21, 4),
1354 new CollisionBox(9, 34, 15, 4)
1370 * Blinking coefficient.
1373 Trex.BLINK_TIMING = 7000;
1377 * Animation config for different states.
1383 msPerFrame: 1000 / 3
1387 msPerFrame: 1000 / 12
1391 msPerFrame: 1000 / 60
1395 msPerFrame: 1000 / 60
1402 * T-rex player initaliser.
1403 * Sets the t-rex to blink at random intervals.
1406 this.blinkDelay = this.setBlinkDelay();
1407 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1408 Runner.config.BOTTOM_PAD;
1409 this.yPos = this.groundYPos;
1410 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1413 this.update(0, Trex.status.WAITING);
1417 * Setter for the jump velocity.
1418 * The approriate drop velocity is also set.
1420 setJumpVelocity: function(setting) {
1421 this.config.INIITAL_JUMP_VELOCITY = -setting;
1422 this.config.DROP_VELOCITY = -setting / 2;
1426 * Set the animation status.
1427 * @param {!number} deltaTime
1428 * @param {Trex.status} status Optional status to switch to.
1430 update: function(deltaTime, opt_status) {
1431 this.timer += deltaTime;
1433 // Update the status.
1435 this.status = opt_status;
1436 this.currentFrame = 0;
1437 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1438 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1440 if (opt_status == Trex.status.WAITING) {
1441 this.animStartTime = getTimeStamp();
1442 this.setBlinkDelay();
1446 // Game intro animation, T-rex moves in from the left.
1447 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1448 this.xPos += Math.round((this.config.START_X_POS /
1449 this.config.INTRO_DURATION) * deltaTime);
1452 if (this.status == Trex.status.WAITING) {
1453 this.blink(getTimeStamp());
1455 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1458 // Update the frame position.
1459 if (this.timer >= this.msPerFrame) {
1460 this.currentFrame = this.currentFrame ==
1461 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1467 * Draw the t-rex to a particular position.
1471 draw: function(x, y) {
1474 var sourceWidth = this.config.WIDTH;
1475 var sourceHeight = this.config.HEIGHT;
1484 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1485 sourceWidth, sourceHeight,
1486 this.xPos, this.yPos,
1487 this.config.WIDTH, this.config.HEIGHT);
1491 * Sets a random time for the blink to happen.
1493 setBlinkDelay: function() {
1494 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1498 * Make t-rex blink at random intervals.
1499 * @param {number} time Current time in milliseconds.
1501 blink: function(time) {
1502 var deltaTime = time - this.animStartTime;
1504 if (deltaTime >= this.blinkDelay) {
1505 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1507 if (this.currentFrame == 1) {
1508 // Set new random delay to blink.
1509 this.setBlinkDelay();
1510 this.animStartTime = time;
1516 * Initialise a jump.
1518 startJump: function() {
1519 if (!this.jumping) {
1520 this.update(0, Trex.status.JUMPING);
1521 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY;
1522 this.jumping = true;
1523 this.reachedMinHeight = false;
1524 this.speedDrop = false;
1529 * Jump is complete, falling down.
1531 endJump: function() {
1532 if (this.reachedMinHeight &&
1533 this.jumpVelocity < this.config.DROP_VELOCITY) {
1534 this.jumpVelocity = this.config.DROP_VELOCITY;
1539 * Update frame for a jump.
1540 * @param {number} deltaTime
1542 updateJump: function(deltaTime) {
1543 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1544 var framesElapsed = deltaTime / msPerFrame;
1546 // Speed drop makes Trex fall faster.
1547 if (this.speedDrop) {
1548 this.yPos += Math.round(this.jumpVelocity *
1549 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1551 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1554 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1556 // Minimum height has been reached.
1557 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1558 this.reachedMinHeight = true;
1561 // Reached max height
1562 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1566 // Back down at ground level. Jump completed.
1567 if (this.yPos > this.groundYPos) {
1572 this.update(deltaTime);
1576 * Set the speed drop. Immediately cancels the current jump.
1578 setSpeedDrop: function() {
1579 this.speedDrop = true;
1580 this.jumpVelocity = 1;
1584 * Reset the t-rex to running at start of game.
1587 this.yPos = this.groundYPos;
1588 this.jumpVelocity = 0;
1589 this.jumping = false;
1590 this.update(0, Trex.status.RUNNING);
1591 this.midair = false;
1592 this.speedDrop = false;
1598 //******************************************************************************
1601 * Handles displaying the distance meter.
1602 * @param {!HTMLCanvasElement} canvas
1603 * @param {!HTMLImage} spriteSheet Image sprite.
1604 * @param {number} canvasWidth
1607 function DistanceMeter(canvas, spriteSheet, canvasWidth) {
1608 this.canvas = canvas;
1609 this.canvasCtx = canvas.getContext('2d');
1610 this.image = spriteSheet;
1614 this.currentDistance = 0;
1617 this.container = null;
1620 this.acheivement = false;
1621 this.defaultString = '';
1622 this.flashTimer = 0;
1623 this.flashIterations = 0;
1625 this.config = DistanceMeter.config;
1626 this.init(canvasWidth);
1633 DistanceMeter.dimensions = {
1641 * Y positioning of the digits in the sprite sheet.
1642 * X position is always 0.
1643 * @type {array.<number>}
1645 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1649 * Distance meter config.
1652 DistanceMeter.config = {
1653 // Number of digits.
1654 MAX_DISTANCE_UNITS: 5,
1656 // Distance that causes achievement animation.
1657 ACHIEVEMENT_DISTANCE: 100,
1659 // Used for conversion from pixel distance to a scaled unit.
1662 // Flash duration in milliseconds.
1663 FLASH_DURATION: 1000 / 4,
1665 // Flash iterations for achievement animation.
1670 DistanceMeter.prototype = {
1672 * Initialise the distance meter to '00000'.
1673 * @param {number} width Canvas width in px.
1675 init: function(width) {
1676 var maxDistanceStr = '';
1678 this.calcXPos(width);
1679 this.maxScore = this.config.MAX_DISTANCE_UNITS;
1680 for (var i = 0; i < this.config.MAX_DISTANCE_UNITS; i++) {
1682 this.defaultString += '0';
1683 maxDistanceStr += '9';
1686 this.maxScore = parseInt(maxDistanceStr);
1690 * Calculate the xPos in the canvas.
1691 * @param {number} canvasWidth
1693 calcXPos: function(canvasWidth) {
1694 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1695 (this.config.MAX_DISTANCE_UNITS + 1));
1699 * Draw a digit to canvas.
1700 * @param {number} digitPos Position of the digit.
1701 * @param {number} value Digit value 0-9.
1702 * @param {boolean} opt_highScore Whether drawing the high score.
1704 draw: function(digitPos, value, opt_highScore) {
1705 var sourceWidth = DistanceMeter.dimensions.WIDTH;
1706 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1707 var sourceX = DistanceMeter.dimensions.WIDTH * value;
1709 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1710 var targetY = this.y;
1711 var targetWidth = DistanceMeter.dimensions.WIDTH;
1712 var targetHeight = DistanceMeter.dimensions.HEIGHT;
1714 // For high DPI we 2x source values.
1721 this.canvasCtx.save();
1723 if (opt_highScore) {
1724 // Left of the current score.
1725 var highScoreX = this.x - (this.config.MAX_DISTANCE_UNITS * 2) *
1726 DistanceMeter.dimensions.WIDTH;
1727 this.canvasCtx.translate(highScoreX, this.y);
1729 this.canvasCtx.translate(this.x, this.y);
1732 this.canvasCtx.drawImage(this.image, sourceX, 0,
1733 sourceWidth, sourceHeight,
1735 targetWidth, targetHeight
1738 this.canvasCtx.restore();
1742 * Covert pixel distance to a 'real' distance.
1743 * @param {number} distance Pixel distance ran.
1744 * @return {number} The 'real' distance ran.
1746 getActualDistance: function(distance) {
1748 Math.round(distance * this.config.COEFFICIENT) : 0;
1752 * Update the distance meter.
1753 * @param {number} deltaTime
1754 * @param {number} distance
1755 * @return {boolean} Whether the acheivement sound fx should be played.
1757 update: function(deltaTime, distance) {
1759 var playSound = false;
1761 if (!this.acheivement) {
1762 distance = this.getActualDistance(distance);
1765 // Acheivement unlocked
1766 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1767 // Flash score and play sound.
1768 this.acheivement = true;
1769 this.flashTimer = 0;
1773 // Create a string representation of the distance with leading 0.
1774 var distanceStr = (this.defaultString +
1775 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1776 this.digits = distanceStr.split('');
1778 this.digits = this.defaultString.split('');
1781 // Control flashing of the score on reaching acheivement.
1782 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1783 this.flashTimer += deltaTime;
1785 if (this.flashTimer < this.config.FLASH_DURATION) {
1787 } else if (this.flashTimer >
1788 this.config.FLASH_DURATION * 2) {
1789 this.flashTimer = 0;
1790 this.flashIterations++;
1793 this.acheivement = false;
1794 this.flashIterations = 0;
1795 this.flashTimer = 0;
1799 // Draw the digits if not flashing.
1801 for (var i = this.digits.length - 1; i >= 0; i--) {
1802 this.draw(i, parseInt(this.digits[i]));
1806 this.drawHighScore();
1812 * Draw the high score.
1814 drawHighScore: function() {
1815 this.canvasCtx.save();
1816 this.canvasCtx.globalAlpha = .8;
1817 for (var i = this.highScore.length - 1; i >= 0; i--) {
1818 this.draw(i, parseInt(this.highScore[i], 10), true);
1820 this.canvasCtx.restore();
1824 * Set the highscore as a array string.
1825 * Position of char in the sprite: H - 10, I - 11.
1826 * @param {number} distance Distance ran in pixels.
1828 setHighScore: function(distance) {
1829 distance = this.getActualDistance(distance);
1830 var highScoreStr = (this.defaultString +
1831 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1833 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
1837 * Reset the distance meter back to '00000'.
1841 this.acheivement = false;
1846 //******************************************************************************
1849 * Cloud background item.
1850 * Similar to an obstacle object but without collision boxes.
1851 * @param {HTMLCanvasElement} canvas Canvas element.
1852 * @param {Image} cloudImg
1853 * @param {number} containerWidth
1855 function Cloud(canvas, cloudImg, containerWidth) {
1856 this.canvas = canvas;
1857 this.canvasCtx = this.canvas.getContext('2d');
1858 this.image = cloudImg;
1859 this.containerWidth = containerWidth;
1860 this.xPos = containerWidth;
1862 this.remove = false;
1863 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
1864 Cloud.config.MAX_CLOUD_GAP);
1871 * Cloud object config.
1886 * Initialise the cloud. Sets the Cloud height.
1889 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
1890 Cloud.config.MIN_SKY_LEVEL);
1898 this.canvasCtx.save();
1899 var sourceWidth = Cloud.config.WIDTH;
1900 var sourceHeight = Cloud.config.HEIGHT;
1903 sourceWidth = sourceWidth * 2;
1904 sourceHeight = sourceHeight * 2;
1907 this.canvasCtx.drawImage(this.image, 0, 0,
1908 sourceWidth, sourceHeight,
1909 this.xPos, this.yPos,
1910 Cloud.config.WIDTH, Cloud.config.HEIGHT);
1912 this.canvasCtx.restore();
1916 * Update the cloud position.
1917 * @param {number} speed
1919 update: function(speed) {
1921 this.xPos -= Math.ceil(speed);
1924 // Mark as removeable if no longer in the canvas.
1925 if (!this.isVisible()) {
1932 * Check if the cloud is visible on the stage.
1935 isVisible: function() {
1936 return this.xPos + Cloud.config.WIDTH > 0;
1941 //******************************************************************************
1945 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
1946 * @param {HTMLCanvasElement} canvas
1947 * @param {HTMLImage} bgImg Horizon line sprite.
1950 function HorizonLine(canvas, bgImg) {
1952 this.canvas = canvas;
1953 this.canvasCtx = canvas.getContext('2d');
1954 this.sourceDimensions = {};
1955 this.dimensions = HorizonLine.dimensions;
1956 this.sourceXPos = [0, this.dimensions.WIDTH];
1959 this.bumpThreshold = 0.5;
1961 this.setSourceDimensions();
1967 * Horizon line dimensions.
1970 HorizonLine.dimensions = {
1977 HorizonLine.prototype = {
1979 * Set the source dimensions of the horizon line.
1981 setSourceDimensions: function() {
1983 for (var dimension in HorizonLine.dimensions) {
1985 if (dimension != 'YPOS') {
1986 this.sourceDimensions[dimension] =
1987 HorizonLine.dimensions[dimension] * 2;
1990 this.sourceDimensions[dimension] =
1991 HorizonLine.dimensions[dimension];
1993 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
1996 this.xPos = [0, HorizonLine.dimensions.WIDTH];
1997 this.yPos = HorizonLine.dimensions.YPOS;
2001 * Return the crop x position of a type.
2003 getRandomType: function() {
2004 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
2008 * Draw the horizon line.
2011 this.canvasCtx.drawImage(this.image, this.sourceXPos[0], 0,
2012 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2013 this.xPos[0], this.yPos,
2014 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2016 this.canvasCtx.drawImage(this.image, this.sourceXPos[1], 0,
2017 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2018 this.xPos[1], this.yPos,
2019 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2023 * Update the x position of an indivdual piece of the line.
2024 * @param {number} pos Line position.
2025 * @param {number} increment
2027 updateXPos: function(pos, increment) {
2029 var line2 = pos == 0 ? 1 : 0;
2031 this.xPos[line1] -= increment;
2032 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2034 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2035 this.xPos[line1] += this.dimensions.WIDTH * 2;
2036 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2037 this.sourceXPos[line1] = this.getRandomType();
2042 * Update the horizon line.
2043 * @param {number} deltaTime
2044 * @param {number} speed
2046 update: function(deltaTime, speed) {
2047 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2049 if (this.xPos[0] <= 0) {
2050 this.updateXPos(0, increment);
2052 this.updateXPos(1, increment);
2058 * Reset horizon to the starting position.
2062 this.xPos[1] = HorizonLine.dimensions.WIDTH;
2067 //******************************************************************************
2070 * Horizon background class.
2071 * @param {HTMLCanvasElement} canvas
2072 * @param {Array.<HTMLImageElement>} images
2073 * @param {object} dimensions Canvas dimensions.
2074 * @param {number} gapCoefficient
2077 function Horizon(canvas, images, dimensions, gapCoefficient) {
2078 this.canvas = canvas;
2079 this.canvasCtx = this.canvas.getContext('2d');
2080 this.config = Horizon.config;
2081 this.dimensions = dimensions;
2082 this.gapCoefficient = gapCoefficient;
2083 this.obstacles = [];
2084 this.horizonOffsets = [0, 0];
2085 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2089 this.cloudImg = images.CLOUD;
2090 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2093 this.horizonImg = images.HORIZON;
2094 this.horizonLine = null;
2097 this.obstacleImgs = {
2098 CACTUS_SMALL: images.CACTUS_SMALL,
2099 CACTUS_LARGE: images.CACTUS_LARGE
2111 BG_CLOUD_SPEED: 0.2,
2112 BUMPY_THRESHOLD: .3,
2113 CLOUD_FREQUENCY: .5,
2119 Horizon.prototype = {
2121 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2125 this.horizonLine = new HorizonLine(this.canvas, this.horizonImg);
2129 * @param {number} deltaTime
2130 * @param {number} currentSpeed
2131 * @param {boolean} updateObstacles Used as an override to prevent
2132 * the obstacles from being updated / added. This happens in the
2135 update: function(deltaTime, currentSpeed, updateObstacles) {
2136 this.runningTime += deltaTime;
2137 this.horizonLine.update(deltaTime, currentSpeed);
2138 this.updateClouds(deltaTime, currentSpeed);
2140 if (updateObstacles) {
2141 this.updateObstacles(deltaTime, currentSpeed);
2146 * Update the cloud positions.
2147 * @param {number} deltaTime
2148 * @param {number} currentSpeed
2150 updateClouds: function(deltaTime, speed) {
2151 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2152 var numClouds = this.clouds.length;
2155 for (var i = numClouds - 1; i >= 0; i--) {
2156 this.clouds[i].update(cloudSpeed);
2159 var lastCloud = this.clouds[numClouds - 1];
2161 // Check for adding a new cloud.
2162 if (numClouds < this.config.MAX_CLOUDS &&
2163 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2164 this.cloudFrequency > Math.random()) {
2168 // Remove expired clouds.
2169 this.clouds = this.clouds.filter(function(obj) {
2176 * Update the obstacle positions.
2177 * @param {number} deltaTime
2178 * @param {number} currentSpeed
2180 updateObstacles: function(deltaTime, currentSpeed) {
2181 // Obstacles, move to Horizon layer.
2182 var updatedObstacles = this.obstacles.slice(0);
2184 for (var i = 0; i < this.obstacles.length; i++) {
2185 var obstacle = this.obstacles[i];
2186 obstacle.update(deltaTime, currentSpeed);
2188 // Clean up existing obstacles.
2189 if (obstacle.remove) {
2190 updatedObstacles.shift();
2193 this.obstacles = updatedObstacles;
2195 if (this.obstacles.length > 0) {
2196 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2198 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2199 lastObstacle.isVisible() &&
2200 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2201 this.dimensions.WIDTH) {
2202 this.addNewObstacle(currentSpeed);
2203 lastObstacle.followingObstacleCreated = true;
2206 // Create new obstacles.
2207 this.addNewObstacle(currentSpeed);
2212 * Add a new obstacle.
2213 * @param {number} currentSpeed
2215 addNewObstacle: function(currentSpeed) {
2216 var obstacleTypeIndex =
2217 getRandomNum(0, Obstacle.types.length - 1);
2218 var obstacleType = Obstacle.types[obstacleTypeIndex];
2219 var obstacleImg = this.obstacleImgs[obstacleType.type];
2221 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2222 obstacleImg, this.dimensions, this.gapCoefficient, currentSpeed));
2226 * Reset the horizon layer.
2227 * Remove existing obstacles and reposition the horizon line.
2230 this.obstacles = [];
2231 this.horizonLine.reset();
2235 * Update the canvas width and scaling.
2236 * @param {number} width Canvas width.
2237 * @param {number} height Canvas height.
2239 resize: function(width, height) {
2240 this.canvas.width = width;
2241 this.canvas.height = height;
2245 * Add a new cloud to the horizon.
2247 addCloud: function() {
2248 this.clouds.push(new Cloud(this.canvas, this.cloudImg,
2249 this.dimensions.WIDTH));