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;
23 this.config = opt_config || Runner.config;
25 this.dimensions = Runner.defaultDimensions;
28 this.canvasCtx = null;
32 this.distanceMeter = null;
35 this.highestScore = 0;
39 this.msPerFrame = 1000 / FPS;
40 this.currentSpeed = this.config.SPEED;
45 this.activated = false;
49 this.resizeTimerId_ = null;
54 this.audioBuffer = null;
57 // Global web audio context for playing sounds.
58 this.audioContext = null;
62 this.imagesLoaded = 0;
65 window['Runner'] = Runner;
72 var DEFAULT_WIDTH = 600;
81 var IS_HIDPI = window.devicePixelRatio > 1;
84 var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1;
87 var IS_TOUCH_ENABLED = 'ontouchstart' in window;
91 * Default game configuration.
100 GAMEOVER_CLEAR_TIME: 750,
101 GAP_COEFFICIENT: 0.6,
103 INITIAL_JUMP_VELOCITY: 12,
105 MAX_OBSTACLE_LENGTH: 3,
108 MOBILE_SPEED_COEFFICIENT: 1.2,
109 RESOURCE_TEMPLATE_ID: 'audio-resources',
111 SPEED_DROP_COEFFICIENT: 3
116 * Default dimensions.
119 Runner.defaultDimensions = {
120 WIDTH: DEFAULT_WIDTH,
130 CANVAS: 'runner-canvas',
131 CONTAINER: 'runner-container',
133 ICON: 'icon-offline',
134 TOUCH_CONTROLLER: 'controller'
140 * @enum {array.<object>}
142 Runner.imageSources = {
144 {name: 'CACTUS_LARGE', id: '1x-obstacle-large'},
145 {name: 'CACTUS_SMALL', id: '1x-obstacle-small'},
146 {name: 'CLOUD', id: '1x-cloud'},
147 {name: 'HORIZON', id: '1x-horizon'},
148 {name: 'RESTART', id: '1x-restart'},
149 {name: 'TEXT_SPRITE', id: '1x-text'},
150 {name: 'TREX', id: '1x-trex'}
153 {name: 'CACTUS_LARGE', id: '2x-obstacle-large'},
154 {name: 'CACTUS_SMALL', id: '2x-obstacle-small'},
155 {name: 'CLOUD', id: '2x-cloud'},
156 {name: 'HORIZON', id: '2x-horizon'},
157 {name: 'RESTART', id: '2x-restart'},
158 {name: 'TEXT_SPRITE', id: '2x-text'},
159 {name: 'TREX', id: '2x-trex'}
165 * Sound FX. Reference to the ID of the audio tag on interstitial page.
169 BUTTON_PRESS: 'offline-sound-press',
170 HIT: 'offline-sound-hit',
171 SCORE: 'offline-sound-reached'
180 JUMP: {'38': 1, '32': 1}, // Up, spacebar
181 DUCK: {'40': 1}, // Down
182 RESTART: {'13': 1} // Enter
187 * Runner event names.
191 ANIM_END: 'webkitAnimationEnd',
195 MOUSEDOWN: 'mousedown',
198 TOUCHEND: 'touchend',
199 TOUCHSTART: 'touchstart',
200 VISIBILITY: 'visibilitychange',
209 * Setting individual settings for debugging.
210 * @param {string} setting
213 updateConfigSetting: function(setting, value) {
214 if (setting in this.config && value != undefined) {
215 this.config[setting] = value;
219 case 'MIN_JUMP_HEIGHT':
220 case 'SPEED_DROP_COEFFICIENT':
221 this.tRex.config[setting] = value;
223 case 'INITIAL_JUMP_VELOCITY':
224 this.tRex.setJumpVelocity(value);
227 this.setSpeed(value);
234 * Load and cache the image assets from the page.
236 loadImages: function() {
237 var imageSources = IS_HIDPI ? Runner.imageSources.HDPI :
238 Runner.imageSources.LDPI;
240 var numImages = imageSources.length;
242 for (var i = numImages - 1; i >= 0; i--) {
243 var imgSource = imageSources[i];
244 this.images[imgSource.name] = document.getElementById(imgSource.id);
250 * Load and decode base 64 encoded sounds.
252 loadSounds: function() {
253 this.audioContext = new AudioContext();
254 var resourceTemplate =
255 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
257 for (var sound in Runner.sounds) {
258 var soundSrc = resourceTemplate.getElementById(Runner.sounds[sound]).src;
259 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
260 var buffer = decodeBase64ToArrayBuffer(soundSrc);
262 // Async, so no guarantee of order in array.
263 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
264 this.soundFx[index] = audioData;
265 }.bind(this, sound));
270 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
271 * @param {number} opt_speed
273 setSpeed: function(opt_speed) {
274 var speed = opt_speed || this.currentSpeed;
276 // Reduce the speed on smaller mobile screens.
277 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
278 var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
279 this.config.MOBILE_SPEED_COEFFICIENT;
280 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
281 } else if (opt_speed) {
282 this.currentSpeed = opt_speed;
290 // Hide the static icon.
291 document.querySelector('.' + Runner.classes.ICON).style.visibility =
294 this.adjustDimensions();
297 this.containerEl = document.createElement('div');
298 this.containerEl.className = Runner.classes.CONTAINER;
300 // Player canvas container.
301 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
302 this.dimensions.HEIGHT, Runner.classes.PLAYER);
304 this.canvasCtx = this.canvas.getContext('2d');
305 this.canvasCtx.fillStyle = '#f7f7f7';
306 this.canvasCtx.fill();
307 Runner.updateCanvasScaling(this.canvas);
309 // Horizon contains clouds, obstacles and the ground.
310 this.horizon = new Horizon(this.canvas, this.images, this.dimensions,
311 this.config.GAP_COEFFICIENT);
314 this.distanceMeter = new DistanceMeter(this.canvas,
315 this.images.TEXT_SPRITE, this.dimensions.WIDTH);
318 this.tRex = new Trex(this.canvas, this.images.TREX);
320 this.outerContainerEl.appendChild(this.containerEl);
323 this.createTouchController();
326 this.startListening();
329 window.addEventListener(Runner.events.RESIZE,
330 this.debounceResize.bind(this));
334 * Create the touch controller. A div that covers whole screen.
336 createTouchController: function() {
337 this.touchController = document.createElement('div');
338 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
342 * Debounce the resize event.
344 debounceResize: function() {
345 if (!this.resizeTimerId_) {
346 this.resizeTimerId_ =
347 setInterval(this.adjustDimensions.bind(this), 250);
352 * Adjust game space dimensions on resize.
354 adjustDimensions: function() {
355 clearInterval(this.resizeTimerId_);
356 this.resizeTimerId_ = null;
358 var boxStyles = window.getComputedStyle(this.outerContainerEl);
359 var padding = Number(boxStyles.paddingLeft.substr(0,
360 boxStyles.paddingLeft.length - 2));
362 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
364 // Redraw the elements back onto the canvas.
366 this.canvas.width = this.dimensions.WIDTH;
367 this.canvas.height = this.dimensions.HEIGHT;
369 Runner.updateCanvasScaling(this.canvas);
371 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
373 this.horizon.update(0, 0, true);
376 // Outer container and distance meter.
377 if (this.activated || this.crashed) {
378 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
379 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
380 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
383 this.tRex.draw(0, 0);
387 if (this.crashed && this.gameOverPanel) {
388 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
389 this.gameOverPanel.draw();
395 * Play the game intro.
396 * Canvas container width expands out to the full width.
398 playIntro: function() {
399 if (!this.started && !this.crashed) {
400 this.playingIntro = true;
401 this.tRex.playingIntro = true;
403 // CSS animation definition.
404 var keyframes = '@-webkit-keyframes intro { ' +
405 'from { width:' + Trex.config.WIDTH + 'px }' +
406 'to { width: ' + this.dimensions.WIDTH + 'px }' +
408 document.styleSheets[0].insertRule(keyframes, 0);
410 this.containerEl.addEventListener(Runner.events.ANIM_END,
411 this.startGame.bind(this));
413 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
414 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
416 if (this.touchController) {
417 this.outerContainerEl.appendChild(this.touchController);
419 this.activated = true;
421 } else if (this.crashed) {
428 * Update the game status to started.
430 startGame: function() {
431 this.runningTime = 0;
432 this.playingIntro = false;
433 this.tRex.playingIntro = false;
434 this.containerEl.style.webkitAnimation = '';
437 // Handle tabbing off the page. Pause the current game.
438 window.addEventListener(Runner.events.VISIBILITY,
439 this.onVisibilityChange.bind(this));
441 window.addEventListener(Runner.events.BLUR,
442 this.onVisibilityChange.bind(this));
444 window.addEventListener(Runner.events.FOCUS,
445 this.onVisibilityChange.bind(this));
448 clearCanvas: function() {
449 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
450 this.dimensions.HEIGHT);
454 * Update the game frame.
457 this.drawPending = false;
459 var now = performance.now();
460 var deltaTime = now - (this.time || now);
463 if (this.activated) {
466 if (this.tRex.jumping) {
467 this.tRex.updateJump(deltaTime, this.config);
470 this.runningTime += deltaTime;
471 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
473 // First jump triggers the intro.
474 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
478 // The horizon doesn't move until the intro is over.
479 if (this.playingIntro) {
480 this.horizon.update(0, this.currentSpeed, hasObstacles);
482 deltaTime = !this.started ? 0 : deltaTime;
483 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
486 // Check for collisions.
487 var collision = hasObstacles &&
488 checkForCollision(this.horizon.obstacles[0], this.tRex);
491 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
493 if (this.currentSpeed < this.config.MAX_SPEED) {
494 this.currentSpeed += this.config.ACCELERATION;
500 if (this.distanceMeter.getActualDistance(this.distanceRan) >
501 this.distanceMeter.maxScore) {
502 this.distanceRan = 0;
505 var playAcheivementSound = this.distanceMeter.update(deltaTime,
506 Math.ceil(this.distanceRan));
508 if (playAcheivementSound) {
509 this.playSound(this.soundFx.SCORE);
514 this.tRex.update(deltaTime);
522 handleEvent: function(e) {
523 return (function(evtType, events) {
526 case events.TOUCHSTART:
527 case events.MOUSEDOWN:
531 case events.TOUCHEND:
536 }.bind(this))(e.type, Runner.events);
540 * Bind relevant key / mouse / touch listeners.
542 startListening: function() {
544 document.addEventListener(Runner.events.KEYDOWN, this);
545 document.addEventListener(Runner.events.KEYUP, this);
548 // Mobile only touch devices.
549 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
550 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
551 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
554 document.addEventListener(Runner.events.MOUSEDOWN, this);
555 document.addEventListener(Runner.events.MOUSEUP, this);
560 * Remove all listeners.
562 stopListening: function() {
563 document.removeEventListener(Runner.events.KEYDOWN, this);
564 document.removeEventListener(Runner.events.KEYUP, this);
567 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
568 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
569 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
571 document.removeEventListener(Runner.events.MOUSEDOWN, this);
572 document.removeEventListener(Runner.events.MOUSEUP, this);
580 onKeyDown: function(e) {
581 if (!this.crashed && (Runner.keycodes.JUMP[String(e.keyCode)] ||
582 e.type == Runner.events.TOUCHSTART)) {
583 if (!this.activated) {
585 this.activated = true;
588 if (!this.tRex.jumping) {
589 this.playSound(this.soundFx.BUTTON_PRESS);
590 this.tRex.startJump();
594 if (this.crashed && e.type == Runner.events.TOUCHSTART &&
595 e.currentTarget == this.containerEl) {
599 // Speed drop, activated only when jump key is not pressed.
600 if (Runner.keycodes.DUCK[e.keyCode] && this.tRex.jumping) {
602 this.tRex.setSpeedDrop();
611 onKeyUp: function(e) {
612 var keyCode = String(e.keyCode);
613 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
614 e.type == Runner.events.TOUCHEND ||
615 e.type == Runner.events.MOUSEDOWN;
617 if (this.isRunning() && isjumpKey) {
619 } else if (Runner.keycodes.DUCK[keyCode]) {
620 this.tRex.speedDrop = false;
621 } else if (this.crashed) {
622 // Check that enough time has elapsed before allowing jump key to restart.
623 var deltaTime = performance.now() - this.time;
625 if (Runner.keycodes.RESTART[keyCode] ||
626 (e.type == Runner.events.MOUSEUP && e.target == this.canvas) ||
627 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
628 Runner.keycodes.JUMP[keyCode])) {
631 } else if (this.paused && isjumpKey) {
637 * RequestAnimationFrame wrapper.
640 if (!this.drawPending) {
641 this.drawPending = true;
642 this.raqId = requestAnimationFrame(this.update.bind(this));
647 * Whether the game is running.
650 isRunning: function() {
657 gameOver: function() {
658 this.playSound(this.soundFx.HIT);
663 this.distanceMeter.acheivement = false;
665 this.tRex.update(100, Trex.status.CRASHED);
668 if (!this.gameOverPanel) {
669 this.gameOverPanel = new GameOverPanel(this.canvas,
670 this.images.TEXT_SPRITE, this.images.RESTART,
673 this.gameOverPanel.draw();
676 // Update the high score.
677 if (this.distanceRan > this.highestScore) {
678 this.highestScore = Math.ceil(this.distanceRan);
679 this.distanceMeter.setHighScore(this.highestScore);
682 // Reset the time clock.
683 this.time = performance.now();
687 this.activated = false;
689 cancelAnimationFrame(this.raqId);
695 this.activated = true;
697 this.tRex.update(0, Trex.status.RUNNING);
698 this.time = performance.now();
703 restart: function() {
706 this.runningTime = 0;
707 this.activated = true;
708 this.crashed = false;
709 this.distanceRan = 0;
710 this.setSpeed(this.config.SPEED);
712 this.time = performance.now();
713 this.containerEl.classList.remove(Runner.classes.CRASHED);
715 this.distanceMeter.reset(this.highestScore);
716 this.horizon.reset();
718 this.playSound(this.soundFx.BUTTON_PRESS);
725 * Pause the game if the tab is not in focus.
727 onVisibilityChange: function(e) {
728 if (document.hidden || document.webkitHidden || e.type == 'blur') {
737 * @param {SoundBuffer} soundBuffer
739 playSound: function(soundBuffer) {
741 var sourceNode = this.audioContext.createBufferSource();
742 sourceNode.buffer = soundBuffer;
743 sourceNode.connect(this.audioContext.destination);
751 * Updates the canvas size taking into
752 * account the backing store pixel ratio and
753 * the device pixel ratio.
755 * See article by Paul Lewis:
756 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
758 * @param {HTMLCanvasElement} canvas
759 * @param {number} opt_width
760 * @param {number} opt_height
761 * @return {boolean} Whether the canvas was scaled.
763 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
764 var context = canvas.getContext('2d');
766 // Query the various pixel ratios
767 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
768 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
769 var ratio = devicePixelRatio / backingStoreRatio;
771 // Upscale the canvas if the two ratios don't match
772 if (devicePixelRatio !== backingStoreRatio) {
774 var oldWidth = opt_width || canvas.width;
775 var oldHeight = opt_height || canvas.height;
777 canvas.width = oldWidth * ratio;
778 canvas.height = oldHeight * ratio;
780 canvas.style.width = oldWidth + 'px';
781 canvas.style.height = oldHeight + 'px';
783 // Scale the context to counter the fact that we've manually scaled
784 // our canvas element.
785 context.scale(ratio, ratio);
794 * @param {number} min
795 * @param {number} max
798 function getRandomNum(min, max) {
799 return Math.floor(Math.random() * (max - min + 1)) + min;
804 * Vibrate on mobile devices.
805 * @param {number} duration Duration of the vibration in milliseconds.
807 function vibrate(duration) {
809 window.navigator['vibrate'](duration);
815 * Create canvas element.
816 * @param {HTMLElement} container Element to append canvas to.
817 * @param {number} width
818 * @param {number} height
819 * @param {string} opt_classname
820 * @return {HTMLCanvasElement}
822 function createCanvas(container, width, height, opt_classname) {
823 var canvas = document.createElement('canvas');
824 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
825 opt_classname : Runner.classes.CANVAS;
826 canvas.width = width;
827 canvas.height = height;
828 container.appendChild(canvas);
835 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
836 * @param {string} base64String
838 function decodeBase64ToArrayBuffer(base64String) {
839 var len = (base64String.length / 4) * 3;
840 var str = atob(base64String);
841 var arrayBuffer = new ArrayBuffer(len);
842 var bytes = new Uint8Array(arrayBuffer);
844 for (var i = 0; i < len; i++) {
845 bytes[i] = str.charCodeAt(i);
851 //******************************************************************************
856 * @param {!HTMLCanvasElement} canvas
857 * @param {!HTMLImage} textSprite
858 * @param {!HTMLImage} restartImg
859 * @param {!Object} dimensions Canvas dimensions.
862 function GameOverPanel(canvas, textSprite, restartImg, dimensions) {
863 this.canvas = canvas;
864 this.canvasCtx = canvas.getContext('2d');
865 this.canvasDimensions = dimensions;
866 this.textSprite = textSprite;
867 this.restartImg = restartImg;
873 * Dimensions used in the panel.
876 GameOverPanel.dimensions = {
886 GameOverPanel.prototype = {
888 * Update the panel dimensions.
889 * @param {number} width New canvas width.
890 * @param {number} opt_height Optional new canvas height.
892 updateDimensions: function(width, opt_height) {
893 this.canvasDimensions.WIDTH = width;
895 this.canvasDimensions.HEIGHT = opt_height;
903 var dimensions = GameOverPanel.dimensions;
905 var centerX = this.canvasDimensions.WIDTH / 2;
908 var textSourceX = dimensions.TEXT_X;
909 var textSourceY = dimensions.TEXT_Y;
910 var textSourceWidth = dimensions.TEXT_WIDTH;
911 var textSourceHeight = dimensions.TEXT_HEIGHT;
913 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
914 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
915 var textTargetWidth = dimensions.TEXT_WIDTH;
916 var textTargetHeight = dimensions.TEXT_HEIGHT;
918 var restartSourceWidth = dimensions.RESTART_WIDTH;
919 var restartSourceHeight = dimensions.RESTART_HEIGHT;
920 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
921 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
926 textSourceWidth *= 2;
927 textSourceHeight *= 2;
928 restartSourceWidth *= 2;
929 restartSourceHeight *= 2;
932 // Game over text from sprite.
933 this.canvasCtx.drawImage(this.textSprite,
934 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
935 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
938 this.canvasCtx.drawImage(this.restartImg, 0, 0,
939 restartSourceWidth, restartSourceHeight,
940 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
941 dimensions.RESTART_HEIGHT);
946 //******************************************************************************
949 * Check for a collision.
950 * @param {!Obstacle} obstacle
951 * @param {!Trex} tRex T-rex object.
952 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
954 * @return {Array.<CollisionBox>}
956 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
957 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
959 // Adjustments are made to the bounding box as there is a 1 pixel white
960 // border around the t-rex and obstacles.
961 var tRexBox = new CollisionBox(
964 tRex.config.WIDTH - 2,
965 tRex.config.HEIGHT - 2);
967 var obstacleBox = new CollisionBox(
970 obstacle.typeConfig.width * obstacle.size - 2,
971 obstacle.typeConfig.height - 2);
975 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
978 // Simple outer bounds check.
979 if (boxCompare(tRexBox, obstacleBox)) {
980 var collisionBoxes = obstacle.collisionBoxes;
981 var tRexCollisionBoxes = Trex.collisionBoxes;
983 // Detailed axis aligned box check.
984 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
985 for (var i = 0; i < collisionBoxes.length; i++) {
986 // Adjust the box to actual positions.
988 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
990 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
991 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
993 // Draw boxes for debug.
995 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
999 return [adjTrexBox, adjObstacleBox];
1009 * Adjust the collision box.
1010 * @param {!CollisionBox} box The original box.
1011 * @param {!CollisionBox} adjustment Adjustment box.
1012 * @return {CollisionBox} The adjusted collision box object.
1014 function createAdjustedCollisionBox(box, adjustment) {
1015 return new CollisionBox(
1016 box.x + adjustment.x,
1017 box.y + adjustment.y,
1024 * Draw the collision boxes for debug.
1026 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1028 canvasCtx.strokeStyle = '#f00';
1029 canvasCtx.strokeRect(tRexBox.x, tRexBox.y,
1030 tRexBox.width, tRexBox.height);
1032 canvasCtx.strokeStyle = '#0f0';
1033 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1034 obstacleBox.width, obstacleBox.height);
1035 canvasCtx.restore();
1040 * Compare two collision boxes for a collision.
1041 * @param {CollisionBox} tRexBox
1042 * @param {CollisionBox} obstacleBox
1043 * @return {boolean} Whether the boxes intersected.
1045 function boxCompare(tRexBox, obstacleBox) {
1046 var crashed = false;
1047 var tRexBoxX = tRexBox.x;
1048 var tRexBoxY = tRexBox.y;
1050 var obstacleBoxX = obstacleBox.x;
1051 var obstacleBoxY = obstacleBox.y;
1053 // Axis-Aligned Bounding Box method.
1054 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1055 tRexBox.x + tRexBox.width > obstacleBoxX &&
1056 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1057 tRexBox.height + tRexBox.y > obstacleBox.y) {
1065 //******************************************************************************
1068 * Collision box object.
1069 * @param {number} x X position.
1070 * @param {number} y Y Position.
1071 * @param {number} w Width.
1072 * @param {number} h Height.
1074 function CollisionBox(x, y, w, h) {
1082 //******************************************************************************
1086 * @param {HTMLCanvasCtx} canvasCtx
1087 * @param {Obstacle.type} type
1088 * @param {image} obstacleImg Image sprite.
1089 * @param {Object} dimensions
1090 * @param {number} gapCoefficient Mutipler in determining the gap.
1091 * @param {number} speed
1093 function Obstacle(canvasCtx, type, obstacleImg, dimensions,
1094 gapCoefficient, speed) {
1096 this.canvasCtx = canvasCtx;
1097 this.image = obstacleImg;
1098 this.typeConfig = type;
1099 this.gapCoefficient = gapCoefficient;
1100 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1101 this.dimensions = dimensions;
1102 this.remove = false;
1104 this.yPos = this.typeConfig.yPos;
1106 this.collisionBoxes = [];
1113 * Coefficient for calculating the maximum gap.
1116 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1119 * Maximum obstacle grouping count.
1122 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1125 Obstacle.prototype = {
1127 * Initialise the DOM for the obstacle.
1128 * @param {number} speed
1130 init: function(speed) {
1131 this.cloneCollisionBoxes();
1133 // Only allow sizing if we're at the right speed.
1134 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1138 this.width = this.typeConfig.width * this.size;
1139 this.xPos = this.dimensions.WIDTH - this.width;
1143 // Make collision box adjustments,
1144 // Central box is adjusted to the size as one box.
1145 // ____ ______ ________
1146 // _| |-| _| |-| _| |-|
1147 // | |<->| | | |<--->| | | |<----->| |
1148 // | | 1 | | | | 2 | | | | 3 | |
1149 // |_|___|_| |_|_____|_| |_|_______|_|
1151 if (this.size > 1) {
1152 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1153 this.collisionBoxes[2].width;
1154 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1157 this.gap = this.getGap(this.gapCoefficient, speed);
1161 * Draw and crop based on size.
1164 var sourceWidth = this.typeConfig.width;
1165 var sourceHeight = this.typeConfig.height;
1168 sourceWidth = sourceWidth * 2;
1169 sourceHeight = sourceHeight * 2;
1173 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1));
1174 this.canvasCtx.drawImage(this.image,
1176 sourceWidth * this.size, sourceHeight,
1177 this.xPos, this.yPos,
1178 this.typeConfig.width * this.size, this.typeConfig.height);
1182 * Obstacle frame update.
1183 * @param {number} deltaTime
1184 * @param {number} speed
1186 update: function(deltaTime, speed) {
1188 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1191 if (!this.isVisible()) {
1198 * Calculate a random gap size.
1199 * - Minimum gap gets wider as speed increses
1200 * @param {number} gapCoefficient
1201 * @param {number} speed
1202 * @return {number} The gap size.
1204 getGap: function(gapCoefficient, speed) {
1205 var minGap = Math.round(this.width * speed +
1206 this.typeConfig.minGap * gapCoefficient);
1207 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1208 return getRandomNum(minGap, maxGap);
1212 * Check if obstacle is visible.
1213 * @return {boolean} Whether the obstacle is in the game area.
1215 isVisible: function() {
1216 return this.xPos + this.width > 0;
1220 * Make a copy of the collision boxes, since these will change based on
1221 * obstacle type and size.
1223 cloneCollisionBoxes: function() {
1224 var collisionBoxes = this.typeConfig.collisionBoxes;
1226 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1227 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1228 collisionBoxes[i].y, collisionBoxes[i].width,
1229 collisionBoxes[i].height);
1236 * Obstacle definitions.
1237 * minGap: minimum pixel space betweeen obstacles.
1238 * multipleSpeed: Speed at which multiples are allowed.
1242 type: 'CACTUS_SMALL',
1243 className: ' cactus cactus-small ',
1250 new CollisionBox(0, 7, 5, 27),
1251 new CollisionBox(4, 0, 6, 34),
1252 new CollisionBox(10, 4, 7, 14)
1256 type: 'CACTUS_LARGE',
1257 className: ' cactus cactus-large ',
1264 new CollisionBox(0, 12, 7, 38),
1265 new CollisionBox(8, 0, 7, 49),
1266 new CollisionBox(13, 10, 10, 38)
1272 //******************************************************************************
1274 * T-rex game character.
1275 * @param {HTMLCanvas} canvas
1276 * @param {HTMLImage} image Character image.
1279 function Trex(canvas, image) {
1280 this.canvas = canvas;
1281 this.canvasCtx = canvas.getContext('2d');
1285 // Position when on the ground.
1286 this.groundYPos = 0;
1287 this.currentFrame = 0;
1288 this.currentAnimFrames = [];
1289 this.blinkDelay = 0;
1290 this.animStartTime = 0;
1292 this.msPerFrame = 1000 / FPS;
1293 this.config = Trex.config;
1295 this.status = Trex.status.WAITING;
1297 this.jumping = false;
1298 this.jumpVelocity = 0;
1299 this.reachedMinHeight = false;
1300 this.speedDrop = false;
1309 * T-rex player config.
1316 INIITAL_JUMP_VELOCITY: -10,
1317 INTRO_DURATION: 1500,
1318 MAX_JUMP_HEIGHT: 30,
1319 MIN_JUMP_HEIGHT: 30,
1320 SPEED_DROP_COEFFICIENT: 3,
1328 * Used in collision detection.
1329 * @type {Array.<CollisionBox>}
1331 Trex.collisionBoxes = [
1332 new CollisionBox(1, -1, 30, 26),
1333 new CollisionBox(32, 0, 8, 16),
1334 new CollisionBox(10, 35, 14, 8),
1335 new CollisionBox(1, 24, 29, 5),
1336 new CollisionBox(5, 30, 21, 4),
1337 new CollisionBox(9, 34, 15, 4)
1353 * Blinking coefficient.
1356 Trex.BLINK_TIMING = 7000;
1360 * Animation config for different states.
1366 msPerFrame: 1000 / 3
1370 msPerFrame: 1000 / 12
1374 msPerFrame: 1000 / 60
1378 msPerFrame: 1000 / 60
1385 * T-rex player initaliser.
1386 * Sets the t-rex to blink at random intervals.
1389 this.blinkDelay = this.setBlinkDelay();
1390 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1391 Runner.config.BOTTOM_PAD;
1392 this.yPos = this.groundYPos;
1393 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1396 this.update(0, Trex.status.WAITING);
1400 * Setter for the jump velocity.
1401 * The approriate drop velocity is also set.
1403 setJumpVelocity: function(setting) {
1404 this.config.INIITAL_JUMP_VELOCITY = -setting;
1405 this.config.DROP_VELOCITY = -setting / 2;
1409 * Set the animation status.
1410 * @param {!number} deltaTime
1411 * @param {Trex.status} status Optional status to switch to.
1413 update: function(deltaTime, opt_status) {
1414 this.timer += deltaTime;
1416 // Update the status.
1418 this.status = opt_status;
1419 this.currentFrame = 0;
1420 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1421 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1423 if (opt_status == Trex.status.WAITING) {
1424 this.animStartTime = performance.now();
1425 this.setBlinkDelay();
1429 // Game intro animation, T-rex moves in from the left.
1430 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1431 this.xPos += Math.round((this.config.START_X_POS /
1432 this.config.INTRO_DURATION) * deltaTime);
1435 if (this.status == Trex.status.WAITING) {
1436 this.blink(performance.now());
1438 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1441 // Update the frame position.
1442 if (this.timer >= this.msPerFrame) {
1443 this.currentFrame = this.currentFrame ==
1444 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1450 * Draw the t-rex to a particular position.
1454 draw: function(x, y) {
1457 var sourceWidth = this.config.WIDTH;
1458 var sourceHeight = this.config.HEIGHT;
1467 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1468 sourceWidth, sourceHeight,
1469 this.xPos, this.yPos,
1470 this.config.WIDTH, this.config.HEIGHT);
1474 * Sets a random time for the blink to happen.
1476 setBlinkDelay: function() {
1477 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1481 * Make t-rex blink at random intervals.
1482 * @param {number} time Current time in milliseconds.
1484 blink: function(time) {
1485 var deltaTime = time - this.animStartTime;
1487 if (deltaTime >= this.blinkDelay) {
1488 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1490 if (this.currentFrame == 1) {
1491 // Set new random delay to blink.
1492 this.setBlinkDelay();
1493 this.animStartTime = time;
1499 * Initialise a jump.
1501 startJump: function() {
1502 if (!this.jumping) {
1503 this.update(0, Trex.status.JUMPING);
1504 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY;
1505 this.jumping = true;
1506 this.reachedMinHeight = false;
1507 this.speedDrop = false;
1512 * Jump is complete, falling down.
1514 endJump: function() {
1515 if (this.reachedMinHeight &&
1516 this.jumpVelocity < this.config.DROP_VELOCITY) {
1517 this.jumpVelocity = this.config.DROP_VELOCITY;
1522 * Update frame for a jump.
1523 * @param {number} deltaTime
1525 updateJump: function(deltaTime) {
1526 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1527 var framesElapsed = deltaTime / msPerFrame;
1529 // Speed drop makes Trex fall faster.
1530 if (this.speedDrop) {
1531 this.yPos += Math.round(this.jumpVelocity *
1532 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1534 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1537 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1539 // Minimum height has been reached.
1540 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1541 this.reachedMinHeight = true;
1544 // Reached max height
1545 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1549 // Back down at ground level. Jump completed.
1550 if (this.yPos > this.groundYPos) {
1555 this.update(deltaTime);
1559 * Set the speed drop. Immediately cancels the current jump.
1561 setSpeedDrop: function() {
1562 this.speedDrop = true;
1563 this.jumpVelocity = 1;
1567 * Reset the t-rex to running at start of game.
1570 this.yPos = this.groundYPos;
1571 this.jumpVelocity = 0;
1572 this.jumping = false;
1573 this.update(0, Trex.status.RUNNING);
1574 this.midair = false;
1575 this.speedDrop = false;
1581 //******************************************************************************
1584 * Handles displaying the distance meter.
1585 * @param {!HTMLCanvasElement} canvas
1586 * @param {!HTMLImage} spriteSheet Image sprite.
1587 * @param {number} canvasWidth
1590 function DistanceMeter(canvas, spriteSheet, canvasWidth) {
1591 this.canvas = canvas;
1592 this.canvasCtx = canvas.getContext('2d');
1593 this.image = spriteSheet;
1597 this.currentDistance = 0;
1600 this.container = null;
1603 this.acheivement = false;
1604 this.defaultString = '';
1605 this.flashTimer = 0;
1606 this.flashIterations = 0;
1608 this.config = DistanceMeter.config;
1609 this.init(canvasWidth);
1616 DistanceMeter.dimensions = {
1624 * Y positioning of the digits in the sprite sheet.
1625 * X position is always 0.
1626 * @type {array.<number>}
1628 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1632 * Distance meter config.
1635 DistanceMeter.config = {
1636 // Number of digits.
1637 MAX_DISTANCE_UNITS: 5,
1639 // Distance that causes achievement animation.
1640 ACHIEVEMENT_DISTANCE: 100,
1642 // Used for conversion from pixel distance to a scaled unit.
1645 // Flash duration in milliseconds.
1646 FLASH_DURATION: 1000 / 4,
1648 // Flash iterations for achievement animation.
1653 DistanceMeter.prototype = {
1655 * Initialise the distance meter to '00000'.
1656 * @param {number} width Canvas width in px.
1658 init: function(width) {
1659 var maxDistanceStr = '';
1661 this.calcXPos(width);
1662 this.maxScore = this.config.MAX_DISTANCE_UNITS;
1663 for (var i = 0; i < this.config.MAX_DISTANCE_UNITS; i++) {
1665 this.defaultString += '0';
1666 maxDistanceStr += '9';
1669 this.maxScore = parseInt(maxDistanceStr);
1673 * Calculate the xPos in the canvas.
1674 * @param {number} canvasWidth
1676 calcXPos: function(canvasWidth) {
1677 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1678 (this.config.MAX_DISTANCE_UNITS + 1));
1682 * Draw a digit to canvas.
1683 * @param {number} digitPos Position of the digit.
1684 * @param {number} value Digit value 0-9.
1685 * @param {boolean} opt_highScore Whether drawing the high score.
1687 draw: function(digitPos, value, opt_highScore) {
1688 var sourceWidth = DistanceMeter.dimensions.WIDTH;
1689 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1690 var sourceX = DistanceMeter.dimensions.WIDTH * value;
1692 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1693 var targetY = this.y;
1694 var targetWidth = DistanceMeter.dimensions.WIDTH;
1695 var targetHeight = DistanceMeter.dimensions.HEIGHT;
1697 // For high DPI we 2x source values.
1704 this.canvasCtx.save();
1706 if (opt_highScore) {
1707 // Left of the current score.
1708 var highScoreX = this.x - (this.config.MAX_DISTANCE_UNITS * 2) *
1709 DistanceMeter.dimensions.WIDTH;
1710 this.canvasCtx.translate(highScoreX, this.y);
1712 this.canvasCtx.translate(this.x, this.y);
1715 this.canvasCtx.drawImage(this.image, sourceX, 0,
1716 sourceWidth, sourceHeight,
1718 targetWidth, targetHeight
1721 this.canvasCtx.restore();
1725 * Covert pixel distance to a 'real' distance.
1726 * @param {number} distance Pixel distance ran.
1727 * @return {number} The 'real' distance ran.
1729 getActualDistance: function(distance) {
1731 Math.round(distance * this.config.COEFFICIENT) : 0;
1735 * Update the distance meter.
1736 * @param {number} deltaTime
1737 * @param {number} distance
1738 * @return {boolean} Whether the acheivement sound fx should be played.
1740 update: function(deltaTime, distance) {
1742 var playSound = false;
1744 if (!this.acheivement) {
1745 distance = this.getActualDistance(distance);
1748 // Acheivement unlocked
1749 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
1750 // Flash score and play sound.
1751 this.acheivement = true;
1752 this.flashTimer = 0;
1756 // Create a string representation of the distance with leading 0.
1757 var distanceStr = (this.defaultString +
1758 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1759 this.digits = distanceStr.split('');
1761 this.digits = this.defaultString.split('');
1764 // Control flashing of the score on reaching acheivement.
1765 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
1766 this.flashTimer += deltaTime;
1768 if (this.flashTimer < this.config.FLASH_DURATION) {
1770 } else if (this.flashTimer >
1771 this.config.FLASH_DURATION * 2) {
1772 this.flashTimer = 0;
1773 this.flashIterations++;
1776 this.acheivement = false;
1777 this.flashIterations = 0;
1778 this.flashTimer = 0;
1782 // Draw the digits if not flashing.
1784 for (var i = this.digits.length - 1; i >= 0; i--) {
1785 this.draw(i, parseInt(this.digits[i]));
1789 this.drawHighScore();
1795 * Draw the high score.
1797 drawHighScore: function() {
1798 this.canvasCtx.save();
1799 this.canvasCtx.globalAlpha = .8;
1800 for (var i = this.highScore.length - 1; i >= 0; i--) {
1801 this.draw(i, parseInt(this.highScore[i], 10), true);
1803 this.canvasCtx.restore();
1807 * Set the highscore as a array string.
1808 * Position of char in the sprite: H - 10, I - 11.
1809 * @param {number} distance Distance ran in pixels.
1811 setHighScore: function(distance) {
1812 distance = this.getActualDistance(distance);
1813 var highScoreStr = (this.defaultString +
1814 distance).substr(-this.config.MAX_DISTANCE_UNITS);
1816 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
1820 * Reset the distance meter back to '00000'.
1824 this.acheivement = false;
1829 //******************************************************************************
1832 * Cloud background item.
1833 * Similar to an obstacle object but without collision boxes.
1834 * @param {HTMLCanvasElement} canvas Canvas element.
1835 * @param {Image} cloudImg
1836 * @param {number} containerWidth
1838 function Cloud(canvas, cloudImg, containerWidth) {
1839 this.canvas = canvas;
1840 this.canvasCtx = this.canvas.getContext('2d');
1841 this.image = cloudImg;
1842 this.containerWidth = containerWidth;
1843 this.xPos = containerWidth;
1845 this.remove = false;
1846 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
1847 Cloud.config.MAX_CLOUD_GAP);
1854 * Cloud object config.
1869 * Initialise the cloud. Sets the Cloud height.
1872 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
1873 Cloud.config.MIN_SKY_LEVEL);
1881 this.canvasCtx.save();
1882 var sourceWidth = Cloud.config.WIDTH;
1883 var sourceHeight = Cloud.config.HEIGHT;
1886 sourceWidth = sourceWidth * 2;
1887 sourceHeight = sourceHeight * 2;
1890 this.canvasCtx.drawImage(this.image, 0, 0,
1891 sourceWidth, sourceHeight,
1892 this.xPos, this.yPos,
1893 Cloud.config.WIDTH, Cloud.config.HEIGHT);
1895 this.canvasCtx.restore();
1899 * Update the cloud position.
1900 * @param {number} speed
1902 update: function(speed) {
1904 this.xPos -= Math.ceil(speed);
1907 // Mark as removeable if no longer in the canvas.
1908 if (!this.isVisible()) {
1915 * Check if the cloud is visible on the stage.
1918 isVisible: function() {
1919 return this.xPos + Cloud.config.WIDTH > 0;
1924 //******************************************************************************
1928 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
1929 * @param {HTMLCanvasElement} canvas
1930 * @param {HTMLImage} bgImg Horizon line sprite.
1933 function HorizonLine(canvas, bgImg) {
1935 this.canvas = canvas;
1936 this.canvasCtx = canvas.getContext('2d');
1937 this.sourceDimensions = {};
1938 this.dimensions = HorizonLine.dimensions;
1939 this.sourceXPos = [0, this.dimensions.WIDTH];
1942 this.bumpThreshold = 0.5;
1944 this.setSourceDimensions();
1950 * Horizon line dimensions.
1953 HorizonLine.dimensions = {
1960 HorizonLine.prototype = {
1962 * Set the source dimensions of the horizon line.
1964 setSourceDimensions: function() {
1966 for (var dimension in HorizonLine.dimensions) {
1968 if (dimension != 'YPOS') {
1969 this.sourceDimensions[dimension] =
1970 HorizonLine.dimensions[dimension] * 2;
1973 this.sourceDimensions[dimension] =
1974 HorizonLine.dimensions[dimension];
1976 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
1979 this.xPos = [0, HorizonLine.dimensions.WIDTH];
1980 this.yPos = HorizonLine.dimensions.YPOS;
1984 * Return the crop x position of a type.
1986 getRandomType: function() {
1987 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
1991 * Draw the horizon line.
1994 this.canvasCtx.drawImage(this.image, this.sourceXPos[0], 0,
1995 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
1996 this.xPos[0], this.yPos,
1997 this.dimensions.WIDTH, this.dimensions.HEIGHT);
1999 this.canvasCtx.drawImage(this.image, this.sourceXPos[1], 0,
2000 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2001 this.xPos[1], this.yPos,
2002 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2006 * Update the x position of an indivdual piece of the line.
2007 * @param {number} pos Line position.
2008 * @param {number} increment
2010 updateXPos: function(pos, increment) {
2012 var line2 = pos == 0 ? 1 : 0;
2014 this.xPos[line1] -= increment;
2015 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2017 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2018 this.xPos[line1] += this.dimensions.WIDTH * 2;
2019 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2020 this.sourceXPos[line1] = this.getRandomType();
2025 * Update the horizon line.
2026 * @param {number} deltaTime
2027 * @param {number} speed
2029 update: function(deltaTime, speed) {
2030 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2032 if (this.xPos[0] <= 0) {
2033 this.updateXPos(0, increment);
2035 this.updateXPos(1, increment);
2041 * Reset horizon to the starting position.
2045 this.xPos[1] = HorizonLine.dimensions.WIDTH;
2050 //******************************************************************************
2053 * Horizon background class.
2054 * @param {HTMLCanvasElement} canvas
2055 * @param {Array.<HTMLImageElement>} images
2056 * @param {object} dimensions Canvas dimensions.
2057 * @param {number} gapCoefficient
2060 function Horizon(canvas, images, dimensions, gapCoefficient) {
2061 this.canvas = canvas;
2062 this.canvasCtx = this.canvas.getContext('2d');
2063 this.config = Horizon.config;
2064 this.dimensions = dimensions;
2065 this.gapCoefficient = gapCoefficient;
2066 this.obstacles = [];
2067 this.horizonOffsets = [0, 0];
2068 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2072 this.cloudImg = images.CLOUD;
2073 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2076 this.horizonImg = images.HORIZON;
2077 this.horizonLine = null;
2080 this.obstacleImgs = {
2081 CACTUS_SMALL: images.CACTUS_SMALL,
2082 CACTUS_LARGE: images.CACTUS_LARGE
2094 BG_CLOUD_SPEED: 0.2,
2095 BUMPY_THRESHOLD: .3,
2096 CLOUD_FREQUENCY: .5,
2102 Horizon.prototype = {
2104 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2108 this.horizonLine = new HorizonLine(this.canvas, this.horizonImg);
2112 * @param {number} deltaTime
2113 * @param {number} currentSpeed
2114 * @param {boolean} updateObstacles Used as an override to prevent
2115 * the obstacles from being updated / added. This happens in the
2118 update: function(deltaTime, currentSpeed, updateObstacles) {
2119 this.runningTime += deltaTime;
2120 this.horizonLine.update(deltaTime, currentSpeed);
2121 this.updateClouds(deltaTime, currentSpeed);
2123 if (updateObstacles) {
2124 this.updateObstacles(deltaTime, currentSpeed);
2129 * Update the cloud positions.
2130 * @param {number} deltaTime
2131 * @param {number} currentSpeed
2133 updateClouds: function(deltaTime, speed) {
2134 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2135 var numClouds = this.clouds.length;
2138 for (var i = numClouds - 1; i >= 0; i--) {
2139 this.clouds[i].update(cloudSpeed);
2142 var lastCloud = this.clouds[numClouds - 1];
2144 // Check for adding a new cloud.
2145 if (numClouds < this.config.MAX_CLOUDS &&
2146 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2147 this.cloudFrequency > Math.random()) {
2151 // Remove expired clouds.
2152 this.clouds = this.clouds.filter(function(obj) {
2159 * Update the obstacle positions.
2160 * @param {number} deltaTime
2161 * @param {number} currentSpeed
2163 updateObstacles: function(deltaTime, currentSpeed) {
2164 // Obstacles, move to Horizon layer.
2165 var updatedObstacles = this.obstacles.slice(0);
2167 for (var i = 0; i < this.obstacles.length; i++) {
2168 var obstacle = this.obstacles[i];
2169 obstacle.update(deltaTime, currentSpeed);
2171 // Clean up existing obstacles.
2172 if (obstacle.remove) {
2173 updatedObstacles.shift();
2176 this.obstacles = updatedObstacles;
2178 if (this.obstacles.length > 0) {
2179 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2181 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2182 lastObstacle.isVisible() &&
2183 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2184 this.dimensions.WIDTH) {
2185 this.addNewObstacle(currentSpeed);
2186 lastObstacle.followingObstacleCreated = true;
2189 // Create new obstacles.
2190 this.addNewObstacle(currentSpeed);
2195 * Add a new obstacle.
2196 * @param {number} currentSpeed
2198 addNewObstacle: function(currentSpeed) {
2199 var obstacleTypeIndex =
2200 getRandomNum(0, Obstacle.types.length - 1);
2201 var obstacleType = Obstacle.types[obstacleTypeIndex];
2202 var obstacleImg = this.obstacleImgs[obstacleType.type];
2204 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2205 obstacleImg, this.dimensions, this.gapCoefficient, currentSpeed));
2209 * Reset the horizon layer.
2210 * Remove existing obstacles and reposition the horizon line.
2213 this.obstacles = [];
2214 this.horizonLine.reset();
2218 * Update the canvas width and scaling.
2219 * @param {number} width Canvas width.
2220 * @param {number} height Canvas height.
2222 resize: function(width, height) {
2223 this.canvas.width = width;
2224 this.canvas.height = height;
2228 * Add a new cloud to the horizon.
2230 addCloud: function() {
2231 this.clouds.push(new Cloud(this.canvas, this.cloudImg,
2232 this.dimensions.WIDTH));