Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(221)

Side by Side Diff: chrome/renderer/resources/offline.js

Issue 1668963002: Componentize IDR_NET_ERROR_HTML for sharing it with iOS. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase & remove jar@chromium.org from components/resources/OWNERS Created 4 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/renderer/resources/neterror.js ('k') | chrome/renderer/resources/renderer_resources.grd » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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.
4 (function() {
5 'use strict';
6 /**
7 * T-Rex runner.
8 * @param {string} outerContainerId Outer containing element id.
9 * @param {Object} opt_config
10 * @constructor
11 * @export
12 */
13 function Runner(outerContainerId, opt_config) {
14 // Singleton
15 if (Runner.instance_) {
16 return Runner.instance_;
17 }
18 Runner.instance_ = this;
19
20 this.outerContainerEl = document.querySelector(outerContainerId);
21 this.containerEl = null;
22 this.snackbarEl = null;
23 this.detailsButton = this.outerContainerEl.querySelector('#details-button');
24
25 this.config = opt_config || Runner.config;
26
27 this.dimensions = Runner.defaultDimensions;
28
29 this.canvas = null;
30 this.canvasCtx = null;
31
32 this.tRex = null;
33
34 this.distanceMeter = null;
35 this.distanceRan = 0;
36
37 this.highestScore = 0;
38
39 this.time = 0;
40 this.runningTime = 0;
41 this.msPerFrame = 1000 / FPS;
42 this.currentSpeed = this.config.SPEED;
43
44 this.obstacles = [];
45
46 this.started = false;
47 this.activated = false;
48 this.crashed = false;
49 this.paused = false;
50 this.inverted = false;
51 this.invertTimer = 0;
52 this.resizeTimerId_ = null;
53
54 this.playCount = 0;
55
56 // Sound FX.
57 this.audioBuffer = null;
58 this.soundFx = {};
59
60 // Global web audio context for playing sounds.
61 this.audioContext = null;
62
63 // Images.
64 this.images = {};
65 this.imagesLoaded = 0;
66
67 if (this.isDisabled()) {
68 this.setupDisabledRunner();
69 } else {
70 this.loadImages();
71 }
72 }
73 window['Runner'] = Runner;
74
75
76 /**
77 * Default game width.
78 * @const
79 */
80 var DEFAULT_WIDTH = 600;
81
82 /**
83 * Frames per second.
84 * @const
85 */
86 var FPS = 60;
87
88 /** @const */
89 var IS_HIDPI = window.devicePixelRatio > 1;
90
91 /** @const */
92 var IS_IOS = window.navigator.userAgent.indexOf('CriOS') > -1 ||
93 window.navigator.userAgent == 'UIWebViewForStaticFileContent';
94
95 /** @const */
96 var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1 || IS_IOS;
97
98 /** @const */
99 var IS_TOUCH_ENABLED = 'ontouchstart' in window;
100
101 /**
102 * Default game configuration.
103 * @enum {number}
104 */
105 Runner.config = {
106 ACCELERATION: 0.001,
107 BG_CLOUD_SPEED: 0.2,
108 BOTTOM_PAD: 10,
109 CLEAR_TIME: 3000,
110 CLOUD_FREQUENCY: 0.5,
111 GAMEOVER_CLEAR_TIME: 750,
112 GAP_COEFFICIENT: 0.6,
113 GRAVITY: 0.6,
114 INITIAL_JUMP_VELOCITY: 12,
115 INVERT_FADE_DURATION: 12000,
116 INVERT_DISTANCE: 700,
117 MAX_CLOUDS: 6,
118 MAX_OBSTACLE_LENGTH: 3,
119 MAX_OBSTACLE_DUPLICATION: 2,
120 MAX_SPEED: 13,
121 MIN_JUMP_HEIGHT: 35,
122 MOBILE_SPEED_COEFFICIENT: 1.2,
123 RESOURCE_TEMPLATE_ID: 'audio-resources',
124 SPEED: 6,
125 SPEED_DROP_COEFFICIENT: 3
126 };
127
128
129 /**
130 * Default dimensions.
131 * @enum {string}
132 */
133 Runner.defaultDimensions = {
134 WIDTH: DEFAULT_WIDTH,
135 HEIGHT: 150
136 };
137
138
139 /**
140 * CSS class names.
141 * @enum {string}
142 */
143 Runner.classes = {
144 CANVAS: 'runner-canvas',
145 CONTAINER: 'runner-container',
146 CRASHED: 'crashed',
147 ICON: 'icon-offline',
148 INVERTED: 'inverted',
149 SNACKBAR: 'snackbar',
150 SNACKBAR_SHOW: 'snackbar-show',
151 TOUCH_CONTROLLER: 'controller'
152 };
153
154
155 /**
156 * Sprite definition layout of the spritesheet.
157 * @enum {Object}
158 */
159 Runner.spriteDefinition = {
160 LDPI: {
161 CACTUS_LARGE: {x: 332, y: 2},
162 CACTUS_SMALL: {x: 228, y: 2},
163 CLOUD: {x: 86, y: 2},
164 HORIZON: {x: 2, y: 54},
165 MOON: {x: 484, y: 2},
166 PTERODACTYL: {x: 134, y: 2},
167 RESTART: {x: 2, y: 2},
168 TEXT_SPRITE: {x: 655, y: 2},
169 TREX: {x: 848, y: 2},
170 STAR: {x: 645, y: 2}
171 },
172 HDPI: {
173 CACTUS_LARGE: {x: 652, y: 2},
174 CACTUS_SMALL: {x: 446, y: 2},
175 CLOUD: {x: 166, y: 2},
176 HORIZON: {x: 2, y: 104},
177 MOON: {x: 954, y: 2},
178 PTERODACTYL: {x: 260, y: 2},
179 RESTART: {x: 2, y: 2},
180 TEXT_SPRITE: {x: 1294, y: 2},
181 TREX: {x: 1678, y: 2},
182 STAR: {x: 1276, y: 2}
183 }
184 };
185
186
187 /**
188 * Sound FX. Reference to the ID of the audio tag on interstitial page.
189 * @enum {string}
190 */
191 Runner.sounds = {
192 BUTTON_PRESS: 'offline-sound-press',
193 HIT: 'offline-sound-hit',
194 SCORE: 'offline-sound-reached'
195 };
196
197
198 /**
199 * Key code mapping.
200 * @enum {Object}
201 */
202 Runner.keycodes = {
203 JUMP: {'38': 1, '32': 1}, // Up, spacebar
204 DUCK: {'40': 1}, // Down
205 RESTART: {'13': 1} // Enter
206 };
207
208
209 /**
210 * Runner event names.
211 * @enum {string}
212 */
213 Runner.events = {
214 ANIM_END: 'webkitAnimationEnd',
215 CLICK: 'click',
216 KEYDOWN: 'keydown',
217 KEYUP: 'keyup',
218 MOUSEDOWN: 'mousedown',
219 MOUSEUP: 'mouseup',
220 RESIZE: 'resize',
221 TOUCHEND: 'touchend',
222 TOUCHSTART: 'touchstart',
223 VISIBILITY: 'visibilitychange',
224 BLUR: 'blur',
225 FOCUS: 'focus',
226 LOAD: 'load'
227 };
228
229
230 Runner.prototype = {
231 /**
232 * Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
233 * @return {boolean}
234 */
235 isDisabled: function() {
236 return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
237 },
238
239 /**
240 * For disabled instances, set up a snackbar with the disabled message.
241 */
242 setupDisabledRunner: function() {
243 this.containerEl = document.createElement('div');
244 this.containerEl.className = Runner.classes.SNACKBAR;
245 this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
246 this.outerContainerEl.appendChild(this.containerEl);
247
248 // Show notification when the activation key is pressed.
249 document.addEventListener(Runner.events.KEYDOWN, function(e) {
250 if (Runner.keycodes.JUMP[e.keyCode]) {
251 this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
252 document.querySelector('.icon').classList.add('icon-disabled');
253 }
254 }.bind(this));
255 },
256
257 /**
258 * Setting individual settings for debugging.
259 * @param {string} setting
260 * @param {*} value
261 */
262 updateConfigSetting: function(setting, value) {
263 if (setting in this.config && value != undefined) {
264 this.config[setting] = value;
265
266 switch (setting) {
267 case 'GRAVITY':
268 case 'MIN_JUMP_HEIGHT':
269 case 'SPEED_DROP_COEFFICIENT':
270 this.tRex.config[setting] = value;
271 break;
272 case 'INITIAL_JUMP_VELOCITY':
273 this.tRex.setJumpVelocity(value);
274 break;
275 case 'SPEED':
276 this.setSpeed(value);
277 break;
278 }
279 }
280 },
281
282 /**
283 * Cache the appropriate image sprite from the page and get the sprite sheet
284 * definition.
285 */
286 loadImages: function() {
287 if (IS_HIDPI) {
288 Runner.imageSprite = document.getElementById('offline-resources-2x');
289 this.spriteDef = Runner.spriteDefinition.HDPI;
290 } else {
291 Runner.imageSprite = document.getElementById('offline-resources-1x');
292 this.spriteDef = Runner.spriteDefinition.LDPI;
293 }
294
295 this.init();
296 },
297
298 /**
299 * Load and decode base 64 encoded sounds.
300 */
301 loadSounds: function() {
302 if (!IS_IOS) {
303 this.audioContext = new AudioContext();
304
305 var resourceTemplate =
306 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
307
308 for (var sound in Runner.sounds) {
309 var soundSrc =
310 resourceTemplate.getElementById(Runner.sounds[sound]).src;
311 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
312 var buffer = decodeBase64ToArrayBuffer(soundSrc);
313
314 // Async, so no guarantee of order in array.
315 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
316 this.soundFx[index] = audioData;
317 }.bind(this, sound));
318 }
319 }
320 },
321
322 /**
323 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
324 * @param {number} opt_speed
325 */
326 setSpeed: function(opt_speed) {
327 var speed = opt_speed || this.currentSpeed;
328
329 // Reduce the speed on smaller mobile screens.
330 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
331 var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
332 this.config.MOBILE_SPEED_COEFFICIENT;
333 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
334 } else if (opt_speed) {
335 this.currentSpeed = opt_speed;
336 }
337 },
338
339 /**
340 * Game initialiser.
341 */
342 init: function() {
343 // Hide the static icon.
344 document.querySelector('.' + Runner.classes.ICON).style.visibility =
345 'hidden';
346
347 this.adjustDimensions();
348 this.setSpeed();
349
350 this.containerEl = document.createElement('div');
351 this.containerEl.className = Runner.classes.CONTAINER;
352
353 // Player canvas container.
354 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
355 this.dimensions.HEIGHT, Runner.classes.PLAYER);
356
357 this.canvasCtx = this.canvas.getContext('2d');
358 this.canvasCtx.fillStyle = '#f7f7f7';
359 this.canvasCtx.fill();
360 Runner.updateCanvasScaling(this.canvas);
361
362 // Horizon contains clouds, obstacles and the ground.
363 this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
364 this.config.GAP_COEFFICIENT);
365
366 // Distance meter
367 this.distanceMeter = new DistanceMeter(this.canvas,
368 this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
369
370 // Draw t-rex
371 this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
372
373 this.outerContainerEl.appendChild(this.containerEl);
374
375 if (IS_MOBILE) {
376 this.createTouchController();
377 }
378
379 this.startListening();
380 this.update();
381
382 window.addEventListener(Runner.events.RESIZE,
383 this.debounceResize.bind(this));
384 },
385
386 /**
387 * Create the touch controller. A div that covers whole screen.
388 */
389 createTouchController: function() {
390 this.touchController = document.createElement('div');
391 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
392 },
393
394 /**
395 * Debounce the resize event.
396 */
397 debounceResize: function() {
398 if (!this.resizeTimerId_) {
399 this.resizeTimerId_ =
400 setInterval(this.adjustDimensions.bind(this), 250);
401 }
402 },
403
404 /**
405 * Adjust game space dimensions on resize.
406 */
407 adjustDimensions: function() {
408 clearInterval(this.resizeTimerId_);
409 this.resizeTimerId_ = null;
410
411 var boxStyles = window.getComputedStyle(this.outerContainerEl);
412 var padding = Number(boxStyles.paddingLeft.substr(0,
413 boxStyles.paddingLeft.length - 2));
414
415 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
416
417 // Redraw the elements back onto the canvas.
418 if (this.canvas) {
419 this.canvas.width = this.dimensions.WIDTH;
420 this.canvas.height = this.dimensions.HEIGHT;
421
422 Runner.updateCanvasScaling(this.canvas);
423
424 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
425 this.clearCanvas();
426 this.horizon.update(0, 0, true);
427 this.tRex.update(0);
428
429 // Outer container and distance meter.
430 if (this.activated || this.crashed || this.paused) {
431 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
432 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
433 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
434 this.stop();
435 } else {
436 this.tRex.draw(0, 0);
437 }
438
439 // Game over panel.
440 if (this.crashed && this.gameOverPanel) {
441 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
442 this.gameOverPanel.draw();
443 }
444 }
445 },
446
447 /**
448 * Play the game intro.
449 * Canvas container width expands out to the full width.
450 */
451 playIntro: function() {
452 if (!this.started && !this.crashed) {
453 this.playingIntro = true;
454 this.tRex.playingIntro = true;
455
456 // CSS animation definition.
457 var keyframes = '@-webkit-keyframes intro { ' +
458 'from { width:' + Trex.config.WIDTH + 'px }' +
459 'to { width: ' + this.dimensions.WIDTH + 'px }' +
460 '}';
461 document.styleSheets[0].insertRule(keyframes, 0);
462
463 this.containerEl.addEventListener(Runner.events.ANIM_END,
464 this.startGame.bind(this));
465
466 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
467 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
468
469 if (this.touchController) {
470 this.outerContainerEl.appendChild(this.touchController);
471 }
472 this.activated = true;
473 this.started = true;
474 } else if (this.crashed) {
475 this.restart();
476 }
477 },
478
479
480 /**
481 * Update the game status to started.
482 */
483 startGame: function() {
484 this.runningTime = 0;
485 this.playingIntro = false;
486 this.tRex.playingIntro = false;
487 this.containerEl.style.webkitAnimation = '';
488 this.playCount++;
489
490 // Handle tabbing off the page. Pause the current game.
491 document.addEventListener(Runner.events.VISIBILITY,
492 this.onVisibilityChange.bind(this));
493
494 window.addEventListener(Runner.events.BLUR,
495 this.onVisibilityChange.bind(this));
496
497 window.addEventListener(Runner.events.FOCUS,
498 this.onVisibilityChange.bind(this));
499 },
500
501 clearCanvas: function() {
502 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
503 this.dimensions.HEIGHT);
504 },
505
506 /**
507 * Update the game frame.
508 */
509 update: function() {
510 this.drawPending = false;
511
512 var now = getTimeStamp();
513 var deltaTime = now - (this.time || now);
514 this.time = now;
515
516 if (this.activated) {
517 this.clearCanvas();
518
519 if (this.tRex.jumping) {
520 this.tRex.updateJump(deltaTime);
521 }
522
523 this.runningTime += deltaTime;
524 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
525
526 // First jump triggers the intro.
527 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
528 this.playIntro();
529 }
530
531 // The horizon doesn't move until the intro is over.
532 if (this.playingIntro) {
533 this.horizon.update(0, this.currentSpeed, hasObstacles);
534 } else {
535 deltaTime = !this.started ? 0 : deltaTime;
536 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles,
537 this.inverted);
538 }
539
540 // Check for collisions.
541 var collision = hasObstacles &&
542 checkForCollision(this.horizon.obstacles[0], this.tRex);
543
544 if (!collision) {
545 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
546
547 if (this.currentSpeed < this.config.MAX_SPEED) {
548 this.currentSpeed += this.config.ACCELERATION;
549 }
550 } else {
551 this.gameOver();
552 }
553
554 var playAchievementSound = this.distanceMeter.update(deltaTime,
555 Math.ceil(this.distanceRan));
556
557 if (playAchievementSound) {
558 this.playSound(this.soundFx.SCORE);
559 }
560
561 // Night mode.
562 if (this.invertTimer > this.config.INVERT_FADE_DURATION) {
563 this.invertTimer = 0;
564 this.invertTrigger = false;
565 this.invert();
566 } else if (this.invertTimer) {
567 this.invertTimer += deltaTime;
568 } else {
569 var actualDistance =
570 this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));
571
572 if (actualDistance > 0) {
573 this.invertTrigger = !(actualDistance %
574 this.config.INVERT_DISTANCE);
575
576 if (this.invertTrigger && this.invertTimer === 0) {
577 this.invertTimer += deltaTime;
578 this.invert();
579 }
580 }
581 }
582 }
583
584 if (!this.crashed) {
585 this.tRex.update(deltaTime);
586 this.raq();
587 }
588 },
589
590 /**
591 * Event handler.
592 */
593 handleEvent: function(e) {
594 return (function(evtType, events) {
595 switch (evtType) {
596 case events.KEYDOWN:
597 case events.TOUCHSTART:
598 case events.MOUSEDOWN:
599 this.onKeyDown(e);
600 break;
601 case events.KEYUP:
602 case events.TOUCHEND:
603 case events.MOUSEUP:
604 this.onKeyUp(e);
605 break;
606 }
607 }.bind(this))(e.type, Runner.events);
608 },
609
610 /**
611 * Bind relevant key / mouse / touch listeners.
612 */
613 startListening: function() {
614 // Keys.
615 document.addEventListener(Runner.events.KEYDOWN, this);
616 document.addEventListener(Runner.events.KEYUP, this);
617
618 if (IS_MOBILE) {
619 // Mobile only touch devices.
620 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
621 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
622 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
623 } else {
624 // Mouse.
625 document.addEventListener(Runner.events.MOUSEDOWN, this);
626 document.addEventListener(Runner.events.MOUSEUP, this);
627 }
628 },
629
630 /**
631 * Remove all listeners.
632 */
633 stopListening: function() {
634 document.removeEventListener(Runner.events.KEYDOWN, this);
635 document.removeEventListener(Runner.events.KEYUP, this);
636
637 if (IS_MOBILE) {
638 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
639 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
640 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
641 } else {
642 document.removeEventListener(Runner.events.MOUSEDOWN, this);
643 document.removeEventListener(Runner.events.MOUSEUP, this);
644 }
645 },
646
647 /**
648 * Process keydown.
649 * @param {Event} e
650 */
651 onKeyDown: function(e) {
652 // Prevent native page scrolling whilst tapping on mobile.
653 if (IS_MOBILE) {
654 e.preventDefault();
655 }
656
657 if (e.target != this.detailsButton) {
658 if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] ||
659 e.type == Runner.events.TOUCHSTART)) {
660 if (!this.activated) {
661 this.loadSounds();
662 this.activated = true;
663 errorPageController.trackEasterEgg();
664 }
665
666 if (!this.tRex.jumping && !this.tRex.ducking) {
667 this.playSound(this.soundFx.BUTTON_PRESS);
668 this.tRex.startJump(this.currentSpeed);
669 }
670 }
671
672 if (this.crashed && e.type == Runner.events.TOUCHSTART &&
673 e.currentTarget == this.containerEl) {
674 this.restart();
675 }
676 }
677
678 if (this.activated && !this.crashed && Runner.keycodes.DUCK[e.keyCode]) {
679 e.preventDefault();
680 if (this.tRex.jumping) {
681 // Speed drop, activated only when jump key is not pressed.
682 this.tRex.setSpeedDrop();
683 } else if (!this.tRex.jumping && !this.tRex.ducking) {
684 // Duck.
685 this.tRex.setDuck(true);
686 }
687 }
688 },
689
690
691 /**
692 * Process key up.
693 * @param {Event} e
694 */
695 onKeyUp: function(e) {
696 var keyCode = String(e.keyCode);
697 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
698 e.type == Runner.events.TOUCHEND ||
699 e.type == Runner.events.MOUSEDOWN;
700
701 if (this.isRunning() && isjumpKey) {
702 this.tRex.endJump();
703 } else if (Runner.keycodes.DUCK[keyCode]) {
704 this.tRex.speedDrop = false;
705 this.tRex.setDuck(false);
706 } else if (this.crashed) {
707 // Check that enough time has elapsed before allowing jump key to restart.
708 var deltaTime = getTimeStamp() - this.time;
709
710 if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
711 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
712 Runner.keycodes.JUMP[keyCode])) {
713 this.restart();
714 }
715 } else if (this.paused && isjumpKey) {
716 // Reset the jump state
717 this.tRex.reset();
718 this.play();
719 }
720 },
721
722 /**
723 * Returns whether the event was a left click on canvas.
724 * On Windows right click is registered as a click.
725 * @param {Event} e
726 * @return {boolean}
727 */
728 isLeftClickOnCanvas: function(e) {
729 return e.button != null && e.button < 2 &&
730 e.type == Runner.events.MOUSEUP && e.target == this.canvas;
731 },
732
733 /**
734 * RequestAnimationFrame wrapper.
735 */
736 raq: function() {
737 if (!this.drawPending) {
738 this.drawPending = true;
739 this.raqId = requestAnimationFrame(this.update.bind(this));
740 }
741 },
742
743 /**
744 * Whether the game is running.
745 * @return {boolean}
746 */
747 isRunning: function() {
748 return !!this.raqId;
749 },
750
751 /**
752 * Game over state.
753 */
754 gameOver: function() {
755 this.playSound(this.soundFx.HIT);
756 vibrate(200);
757
758 this.stop();
759 this.crashed = true;
760 this.distanceMeter.acheivement = false;
761
762 this.tRex.update(100, Trex.status.CRASHED);
763
764 // Game over panel.
765 if (!this.gameOverPanel) {
766 this.gameOverPanel = new GameOverPanel(this.canvas,
767 this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
768 this.dimensions);
769 } else {
770 this.gameOverPanel.draw();
771 }
772
773 // Update the high score.
774 if (this.distanceRan > this.highestScore) {
775 this.highestScore = Math.ceil(this.distanceRan);
776 this.distanceMeter.setHighScore(this.highestScore);
777 }
778
779 // Reset the time clock.
780 this.time = getTimeStamp();
781 },
782
783 stop: function() {
784 this.activated = false;
785 this.paused = true;
786 cancelAnimationFrame(this.raqId);
787 this.raqId = 0;
788 },
789
790 play: function() {
791 if (!this.crashed) {
792 this.activated = true;
793 this.paused = false;
794 this.tRex.update(0, Trex.status.RUNNING);
795 this.time = getTimeStamp();
796 this.update();
797 }
798 },
799
800 restart: function() {
801 if (!this.raqId) {
802 this.playCount++;
803 this.runningTime = 0;
804 this.activated = true;
805 this.crashed = false;
806 this.distanceRan = 0;
807 this.setSpeed(this.config.SPEED);
808 this.time = getTimeStamp();
809 this.containerEl.classList.remove(Runner.classes.CRASHED);
810 this.clearCanvas();
811 this.distanceMeter.reset(this.highestScore);
812 this.horizon.reset();
813 this.tRex.reset();
814 this.playSound(this.soundFx.BUTTON_PRESS);
815 this.invert(true);
816 this.update();
817 }
818 },
819
820 /**
821 * Pause the game if the tab is not in focus.
822 */
823 onVisibilityChange: function(e) {
824 if (document.hidden || document.webkitHidden || e.type == 'blur' ||
825 document.visibilityState != 'visible') {
826 this.stop();
827 } else if (!this.crashed) {
828 this.tRex.reset();
829 this.play();
830 }
831 },
832
833 /**
834 * Play a sound.
835 * @param {SoundBuffer} soundBuffer
836 */
837 playSound: function(soundBuffer) {
838 if (soundBuffer) {
839 var sourceNode = this.audioContext.createBufferSource();
840 sourceNode.buffer = soundBuffer;
841 sourceNode.connect(this.audioContext.destination);
842 sourceNode.start(0);
843 }
844 },
845
846 /**
847 * Inverts the current page / canvas colors.
848 * @param {boolean} Whether to reset colors.
849 */
850 invert: function(reset) {
851 if (reset) {
852 document.body.classList.toggle(Runner.classes.INVERTED, false);
853 this.invertTimer = 0;
854 this.inverted = false;
855 } else {
856 this.inverted = document.body.classList.toggle(Runner.classes.INVERTED,
857 this.invertTrigger);
858 }
859 }
860 };
861
862
863 /**
864 * Updates the canvas size taking into
865 * account the backing store pixel ratio and
866 * the device pixel ratio.
867 *
868 * See article by Paul Lewis:
869 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
870 *
871 * @param {HTMLCanvasElement} canvas
872 * @param {number} opt_width
873 * @param {number} opt_height
874 * @return {boolean} Whether the canvas was scaled.
875 */
876 Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
877 var context = canvas.getContext('2d');
878
879 // Query the various pixel ratios
880 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
881 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
882 var ratio = devicePixelRatio / backingStoreRatio;
883
884 // Upscale the canvas if the two ratios don't match
885 if (devicePixelRatio !== backingStoreRatio) {
886 var oldWidth = opt_width || canvas.width;
887 var oldHeight = opt_height || canvas.height;
888
889 canvas.width = oldWidth * ratio;
890 canvas.height = oldHeight * ratio;
891
892 canvas.style.width = oldWidth + 'px';
893 canvas.style.height = oldHeight + 'px';
894
895 // Scale the context to counter the fact that we've manually scaled
896 // our canvas element.
897 context.scale(ratio, ratio);
898 return true;
899 } else if (devicePixelRatio == 1) {
900 // Reset the canvas width / height. Fixes scaling bug when the page is
901 // zoomed and the devicePixelRatio changes accordingly.
902 canvas.style.width = canvas.width + 'px';
903 canvas.style.height = canvas.height + 'px';
904 }
905 return false;
906 };
907
908
909 /**
910 * Get random number.
911 * @param {number} min
912 * @param {number} max
913 * @param {number}
914 */
915 function getRandomNum(min, max) {
916 return Math.floor(Math.random() * (max - min + 1)) + min;
917 }
918
919
920 /**
921 * Vibrate on mobile devices.
922 * @param {number} duration Duration of the vibration in milliseconds.
923 */
924 function vibrate(duration) {
925 if (IS_MOBILE && window.navigator.vibrate) {
926 window.navigator.vibrate(duration);
927 }
928 }
929
930
931 /**
932 * Create canvas element.
933 * @param {HTMLElement} container Element to append canvas to.
934 * @param {number} width
935 * @param {number} height
936 * @param {string} opt_classname
937 * @return {HTMLCanvasElement}
938 */
939 function createCanvas(container, width, height, opt_classname) {
940 var canvas = document.createElement('canvas');
941 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
942 opt_classname : Runner.classes.CANVAS;
943 canvas.width = width;
944 canvas.height = height;
945 container.appendChild(canvas);
946
947 return canvas;
948 }
949
950
951 /**
952 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
953 * @param {string} base64String
954 */
955 function decodeBase64ToArrayBuffer(base64String) {
956 var len = (base64String.length / 4) * 3;
957 var str = atob(base64String);
958 var arrayBuffer = new ArrayBuffer(len);
959 var bytes = new Uint8Array(arrayBuffer);
960
961 for (var i = 0; i < len; i++) {
962 bytes[i] = str.charCodeAt(i);
963 }
964 return bytes.buffer;
965 }
966
967
968 /**
969 * Return the current timestamp.
970 * @return {number}
971 */
972 function getTimeStamp() {
973 return IS_IOS ? new Date().getTime() : performance.now();
974 }
975
976
977 //******************************************************************************
978
979
980 /**
981 * Game over panel.
982 * @param {!HTMLCanvasElement} canvas
983 * @param {Object} textImgPos
984 * @param {Object} restartImgPos
985 * @param {!Object} dimensions Canvas dimensions.
986 * @constructor
987 */
988 function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
989 this.canvas = canvas;
990 this.canvasCtx = canvas.getContext('2d');
991 this.canvasDimensions = dimensions;
992 this.textImgPos = textImgPos;
993 this.restartImgPos = restartImgPos;
994 this.draw();
995 };
996
997
998 /**
999 * Dimensions used in the panel.
1000 * @enum {number}
1001 */
1002 GameOverPanel.dimensions = {
1003 TEXT_X: 0,
1004 TEXT_Y: 13,
1005 TEXT_WIDTH: 191,
1006 TEXT_HEIGHT: 11,
1007 RESTART_WIDTH: 36,
1008 RESTART_HEIGHT: 32
1009 };
1010
1011
1012 GameOverPanel.prototype = {
1013 /**
1014 * Update the panel dimensions.
1015 * @param {number} width New canvas width.
1016 * @param {number} opt_height Optional new canvas height.
1017 */
1018 updateDimensions: function(width, opt_height) {
1019 this.canvasDimensions.WIDTH = width;
1020 if (opt_height) {
1021 this.canvasDimensions.HEIGHT = opt_height;
1022 }
1023 },
1024
1025 /**
1026 * Draw the panel.
1027 */
1028 draw: function() {
1029 var dimensions = GameOverPanel.dimensions;
1030
1031 var centerX = this.canvasDimensions.WIDTH / 2;
1032
1033 // Game over text.
1034 var textSourceX = dimensions.TEXT_X;
1035 var textSourceY = dimensions.TEXT_Y;
1036 var textSourceWidth = dimensions.TEXT_WIDTH;
1037 var textSourceHeight = dimensions.TEXT_HEIGHT;
1038
1039 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
1040 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
1041 var textTargetWidth = dimensions.TEXT_WIDTH;
1042 var textTargetHeight = dimensions.TEXT_HEIGHT;
1043
1044 var restartSourceWidth = dimensions.RESTART_WIDTH;
1045 var restartSourceHeight = dimensions.RESTART_HEIGHT;
1046 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
1047 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
1048
1049 if (IS_HIDPI) {
1050 textSourceY *= 2;
1051 textSourceX *= 2;
1052 textSourceWidth *= 2;
1053 textSourceHeight *= 2;
1054 restartSourceWidth *= 2;
1055 restartSourceHeight *= 2;
1056 }
1057
1058 textSourceX += this.textImgPos.x;
1059 textSourceY += this.textImgPos.y;
1060
1061 // Game over text from sprite.
1062 this.canvasCtx.drawImage(Runner.imageSprite,
1063 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
1064 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
1065
1066 // Restart button.
1067 this.canvasCtx.drawImage(Runner.imageSprite,
1068 this.restartImgPos.x, this.restartImgPos.y,
1069 restartSourceWidth, restartSourceHeight,
1070 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
1071 dimensions.RESTART_HEIGHT);
1072 }
1073 };
1074
1075
1076 //******************************************************************************
1077
1078 /**
1079 * Check for a collision.
1080 * @param {!Obstacle} obstacle
1081 * @param {!Trex} tRex T-rex object.
1082 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
1083 * collision boxes.
1084 * @return {Array<CollisionBox>}
1085 */
1086 function checkForCollision(obstacle, tRex, opt_canvasCtx) {
1087 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
1088
1089 // Adjustments are made to the bounding box as there is a 1 pixel white
1090 // border around the t-rex and obstacles.
1091 var tRexBox = new CollisionBox(
1092 tRex.xPos + 1,
1093 tRex.yPos + 1,
1094 tRex.config.WIDTH - 2,
1095 tRex.config.HEIGHT - 2);
1096
1097 var obstacleBox = new CollisionBox(
1098 obstacle.xPos + 1,
1099 obstacle.yPos + 1,
1100 obstacle.typeConfig.width * obstacle.size - 2,
1101 obstacle.typeConfig.height - 2);
1102
1103 // Debug outer box
1104 if (opt_canvasCtx) {
1105 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
1106 }
1107
1108 // Simple outer bounds check.
1109 if (boxCompare(tRexBox, obstacleBox)) {
1110 var collisionBoxes = obstacle.collisionBoxes;
1111 var tRexCollisionBoxes = tRex.ducking ?
1112 Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
1113
1114 // Detailed axis aligned box check.
1115 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
1116 for (var i = 0; i < collisionBoxes.length; i++) {
1117 // Adjust the box to actual positions.
1118 var adjTrexBox =
1119 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
1120 var adjObstacleBox =
1121 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
1122 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
1123
1124 // Draw boxes for debug.
1125 if (opt_canvasCtx) {
1126 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1127 }
1128
1129 if (crashed) {
1130 return [adjTrexBox, adjObstacleBox];
1131 }
1132 }
1133 }
1134 }
1135 return false;
1136 };
1137
1138
1139 /**
1140 * Adjust the collision box.
1141 * @param {!CollisionBox} box The original box.
1142 * @param {!CollisionBox} adjustment Adjustment box.
1143 * @return {CollisionBox} The adjusted collision box object.
1144 */
1145 function createAdjustedCollisionBox(box, adjustment) {
1146 return new CollisionBox(
1147 box.x + adjustment.x,
1148 box.y + adjustment.y,
1149 box.width,
1150 box.height);
1151 };
1152
1153
1154 /**
1155 * Draw the collision boxes for debug.
1156 */
1157 function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1158 canvasCtx.save();
1159 canvasCtx.strokeStyle = '#f00';
1160 canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
1161
1162 canvasCtx.strokeStyle = '#0f0';
1163 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1164 obstacleBox.width, obstacleBox.height);
1165 canvasCtx.restore();
1166 };
1167
1168
1169 /**
1170 * Compare two collision boxes for a collision.
1171 * @param {CollisionBox} tRexBox
1172 * @param {CollisionBox} obstacleBox
1173 * @return {boolean} Whether the boxes intersected.
1174 */
1175 function boxCompare(tRexBox, obstacleBox) {
1176 var crashed = false;
1177 var tRexBoxX = tRexBox.x;
1178 var tRexBoxY = tRexBox.y;
1179
1180 var obstacleBoxX = obstacleBox.x;
1181 var obstacleBoxY = obstacleBox.y;
1182
1183 // Axis-Aligned Bounding Box method.
1184 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1185 tRexBox.x + tRexBox.width > obstacleBoxX &&
1186 tRexBox.y < obstacleBox.y + obstacleBox.height &&
1187 tRexBox.height + tRexBox.y > obstacleBox.y) {
1188 crashed = true;
1189 }
1190
1191 return crashed;
1192 };
1193
1194
1195 //******************************************************************************
1196
1197 /**
1198 * Collision box object.
1199 * @param {number} x X position.
1200 * @param {number} y Y Position.
1201 * @param {number} w Width.
1202 * @param {number} h Height.
1203 */
1204 function CollisionBox(x, y, w, h) {
1205 this.x = x;
1206 this.y = y;
1207 this.width = w;
1208 this.height = h;
1209 };
1210
1211
1212 //******************************************************************************
1213
1214 /**
1215 * Obstacle.
1216 * @param {HTMLCanvasCtx} canvasCtx
1217 * @param {Obstacle.type} type
1218 * @param {Object} spritePos Obstacle position in sprite.
1219 * @param {Object} dimensions
1220 * @param {number} gapCoefficient Mutipler in determining the gap.
1221 * @param {number} speed
1222 * @param {number} opt_xOffset
1223 */
1224 function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
1225 gapCoefficient, speed, opt_xOffset) {
1226
1227 this.canvasCtx = canvasCtx;
1228 this.spritePos = spriteImgPos;
1229 this.typeConfig = type;
1230 this.gapCoefficient = gapCoefficient;
1231 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1232 this.dimensions = dimensions;
1233 this.remove = false;
1234 this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
1235 this.yPos = 0;
1236 this.width = 0;
1237 this.collisionBoxes = [];
1238 this.gap = 0;
1239 this.speedOffset = 0;
1240
1241 // For animated obstacles.
1242 this.currentFrame = 0;
1243 this.timer = 0;
1244
1245 this.init(speed);
1246 };
1247
1248 /**
1249 * Coefficient for calculating the maximum gap.
1250 * @const
1251 */
1252 Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1253
1254 /**
1255 * Maximum obstacle grouping count.
1256 * @const
1257 */
1258 Obstacle.MAX_OBSTACLE_LENGTH = 3,
1259
1260
1261 Obstacle.prototype = {
1262 /**
1263 * Initialise the DOM for the obstacle.
1264 * @param {number} speed
1265 */
1266 init: function(speed) {
1267 this.cloneCollisionBoxes();
1268
1269 // Only allow sizing if we're at the right speed.
1270 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1271 this.size = 1;
1272 }
1273
1274 this.width = this.typeConfig.width * this.size;
1275
1276 // Check if obstacle can be positioned at various heights.
1277 if (Array.isArray(this.typeConfig.yPos)) {
1278 var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
1279 this.typeConfig.yPos;
1280 this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
1281 } else {
1282 this.yPos = this.typeConfig.yPos;
1283 }
1284
1285 this.draw();
1286
1287 // Make collision box adjustments,
1288 // Central box is adjusted to the size as one box.
1289 // ____ ______ ________
1290 // _| |-| _| |-| _| |-|
1291 // | |<->| | | |<--->| | | |<----->| |
1292 // | | 1 | | | | 2 | | | | 3 | |
1293 // |_|___|_| |_|_____|_| |_|_______|_|
1294 //
1295 if (this.size > 1) {
1296 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1297 this.collisionBoxes[2].width;
1298 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1299 }
1300
1301 // For obstacles that go at a different speed from the horizon.
1302 if (this.typeConfig.speedOffset) {
1303 this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
1304 -this.typeConfig.speedOffset;
1305 }
1306
1307 this.gap = this.getGap(this.gapCoefficient, speed);
1308 },
1309
1310 /**
1311 * Draw and crop based on size.
1312 */
1313 draw: function() {
1314 var sourceWidth = this.typeConfig.width;
1315 var sourceHeight = this.typeConfig.height;
1316
1317 if (IS_HIDPI) {
1318 sourceWidth = sourceWidth * 2;
1319 sourceHeight = sourceHeight * 2;
1320 }
1321
1322 // X position in sprite.
1323 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
1324 this.spritePos.x;
1325
1326 // Animation frames.
1327 if (this.currentFrame > 0) {
1328 sourceX += sourceWidth * this.currentFrame;
1329 }
1330
1331 this.canvasCtx.drawImage(Runner.imageSprite,
1332 sourceX, this.spritePos.y,
1333 sourceWidth * this.size, sourceHeight,
1334 this.xPos, this.yPos,
1335 this.typeConfig.width * this.size, this.typeConfig.height);
1336 },
1337
1338 /**
1339 * Obstacle frame update.
1340 * @param {number} deltaTime
1341 * @param {number} speed
1342 */
1343 update: function(deltaTime, speed) {
1344 if (!this.remove) {
1345 if (this.typeConfig.speedOffset) {
1346 speed += this.speedOffset;
1347 }
1348 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1349
1350 // Update frame
1351 if (this.typeConfig.numFrames) {
1352 this.timer += deltaTime;
1353 if (this.timer >= this.typeConfig.frameRate) {
1354 this.currentFrame =
1355 this.currentFrame == this.typeConfig.numFrames - 1 ?
1356 0 : this.currentFrame + 1;
1357 this.timer = 0;
1358 }
1359 }
1360 this.draw();
1361
1362 if (!this.isVisible()) {
1363 this.remove = true;
1364 }
1365 }
1366 },
1367
1368 /**
1369 * Calculate a random gap size.
1370 * - Minimum gap gets wider as speed increses
1371 * @param {number} gapCoefficient
1372 * @param {number} speed
1373 * @return {number} The gap size.
1374 */
1375 getGap: function(gapCoefficient, speed) {
1376 var minGap = Math.round(this.width * speed +
1377 this.typeConfig.minGap * gapCoefficient);
1378 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1379 return getRandomNum(minGap, maxGap);
1380 },
1381
1382 /**
1383 * Check if obstacle is visible.
1384 * @return {boolean} Whether the obstacle is in the game area.
1385 */
1386 isVisible: function() {
1387 return this.xPos + this.width > 0;
1388 },
1389
1390 /**
1391 * Make a copy of the collision boxes, since these will change based on
1392 * obstacle type and size.
1393 */
1394 cloneCollisionBoxes: function() {
1395 var collisionBoxes = this.typeConfig.collisionBoxes;
1396
1397 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1398 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1399 collisionBoxes[i].y, collisionBoxes[i].width,
1400 collisionBoxes[i].height);
1401 }
1402 }
1403 };
1404
1405
1406 /**
1407 * Obstacle definitions.
1408 * minGap: minimum pixel space betweeen obstacles.
1409 * multipleSpeed: Speed at which multiples are allowed.
1410 * speedOffset: speed faster / slower than the horizon.
1411 * minSpeed: Minimum speed which the obstacle can make an appearance.
1412 */
1413 Obstacle.types = [
1414 {
1415 type: 'CACTUS_SMALL',
1416 width: 17,
1417 height: 35,
1418 yPos: 105,
1419 multipleSpeed: 4,
1420 minGap: 120,
1421 minSpeed: 0,
1422 collisionBoxes: [
1423 new CollisionBox(0, 7, 5, 27),
1424 new CollisionBox(4, 0, 6, 34),
1425 new CollisionBox(10, 4, 7, 14)
1426 ]
1427 },
1428 {
1429 type: 'CACTUS_LARGE',
1430 width: 25,
1431 height: 50,
1432 yPos: 90,
1433 multipleSpeed: 7,
1434 minGap: 120,
1435 minSpeed: 0,
1436 collisionBoxes: [
1437 new CollisionBox(0, 12, 7, 38),
1438 new CollisionBox(8, 0, 7, 49),
1439 new CollisionBox(13, 10, 10, 38)
1440 ]
1441 },
1442 {
1443 type: 'PTERODACTYL',
1444 width: 46,
1445 height: 40,
1446 yPos: [ 100, 75, 50 ], // Variable height.
1447 yPosMobile: [ 100, 50 ], // Variable height mobile.
1448 multipleSpeed: 999,
1449 minSpeed: 8.5,
1450 minGap: 150,
1451 collisionBoxes: [
1452 new CollisionBox(15, 15, 16, 5),
1453 new CollisionBox(18, 21, 24, 6),
1454 new CollisionBox(2, 14, 4, 3),
1455 new CollisionBox(6, 10, 4, 7),
1456 new CollisionBox(10, 8, 6, 9)
1457 ],
1458 numFrames: 2,
1459 frameRate: 1000/6,
1460 speedOffset: .8
1461 }
1462 ];
1463
1464
1465 //******************************************************************************
1466 /**
1467 * T-rex game character.
1468 * @param {HTMLCanvas} canvas
1469 * @param {Object} spritePos Positioning within image sprite.
1470 * @constructor
1471 */
1472 function Trex(canvas, spritePos) {
1473 this.canvas = canvas;
1474 this.canvasCtx = canvas.getContext('2d');
1475 this.spritePos = spritePos;
1476 this.xPos = 0;
1477 this.yPos = 0;
1478 // Position when on the ground.
1479 this.groundYPos = 0;
1480 this.currentFrame = 0;
1481 this.currentAnimFrames = [];
1482 this.blinkDelay = 0;
1483 this.animStartTime = 0;
1484 this.timer = 0;
1485 this.msPerFrame = 1000 / FPS;
1486 this.config = Trex.config;
1487 // Current status.
1488 this.status = Trex.status.WAITING;
1489
1490 this.jumping = false;
1491 this.ducking = false;
1492 this.jumpVelocity = 0;
1493 this.reachedMinHeight = false;
1494 this.speedDrop = false;
1495 this.jumpCount = 0;
1496 this.jumpspotX = 0;
1497
1498 this.init();
1499 };
1500
1501
1502 /**
1503 * T-rex player config.
1504 * @enum {number}
1505 */
1506 Trex.config = {
1507 DROP_VELOCITY: -5,
1508 GRAVITY: 0.6,
1509 HEIGHT: 47,
1510 HEIGHT_DUCK: 25,
1511 INIITAL_JUMP_VELOCITY: -10,
1512 INTRO_DURATION: 1500,
1513 MAX_JUMP_HEIGHT: 30,
1514 MIN_JUMP_HEIGHT: 30,
1515 SPEED_DROP_COEFFICIENT: 3,
1516 SPRITE_WIDTH: 262,
1517 START_X_POS: 50,
1518 WIDTH: 44,
1519 WIDTH_DUCK: 59
1520 };
1521
1522
1523 /**
1524 * Used in collision detection.
1525 * @type {Array<CollisionBox>}
1526 */
1527 Trex.collisionBoxes = {
1528 DUCKING: [
1529 new CollisionBox(1, 18, 55, 25)
1530 ],
1531 RUNNING: [
1532 new CollisionBox(22, 0, 17, 16),
1533 new CollisionBox(1, 18, 30, 9),
1534 new CollisionBox(10, 35, 14, 8),
1535 new CollisionBox(1, 24, 29, 5),
1536 new CollisionBox(5, 30, 21, 4),
1537 new CollisionBox(9, 34, 15, 4)
1538 ]
1539 };
1540
1541
1542 /**
1543 * Animation states.
1544 * @enum {string}
1545 */
1546 Trex.status = {
1547 CRASHED: 'CRASHED',
1548 DUCKING: 'DUCKING',
1549 JUMPING: 'JUMPING',
1550 RUNNING: 'RUNNING',
1551 WAITING: 'WAITING'
1552 };
1553
1554 /**
1555 * Blinking coefficient.
1556 * @const
1557 */
1558 Trex.BLINK_TIMING = 7000;
1559
1560
1561 /**
1562 * Animation config for different states.
1563 * @enum {Object}
1564 */
1565 Trex.animFrames = {
1566 WAITING: {
1567 frames: [44, 0],
1568 msPerFrame: 1000 / 3
1569 },
1570 RUNNING: {
1571 frames: [88, 132],
1572 msPerFrame: 1000 / 12
1573 },
1574 CRASHED: {
1575 frames: [220],
1576 msPerFrame: 1000 / 60
1577 },
1578 JUMPING: {
1579 frames: [0],
1580 msPerFrame: 1000 / 60
1581 },
1582 DUCKING: {
1583 frames: [262, 321],
1584 msPerFrame: 1000 / 8
1585 }
1586 };
1587
1588
1589 Trex.prototype = {
1590 /**
1591 * T-rex player initaliser.
1592 * Sets the t-rex to blink at random intervals.
1593 */
1594 init: function() {
1595 this.blinkDelay = this.setBlinkDelay();
1596 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1597 Runner.config.BOTTOM_PAD;
1598 this.yPos = this.groundYPos;
1599 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1600
1601 this.draw(0, 0);
1602 this.update(0, Trex.status.WAITING);
1603 },
1604
1605 /**
1606 * Setter for the jump velocity.
1607 * The approriate drop velocity is also set.
1608 */
1609 setJumpVelocity: function(setting) {
1610 this.config.INIITAL_JUMP_VELOCITY = -setting;
1611 this.config.DROP_VELOCITY = -setting / 2;
1612 },
1613
1614 /**
1615 * Set the animation status.
1616 * @param {!number} deltaTime
1617 * @param {Trex.status} status Optional status to switch to.
1618 */
1619 update: function(deltaTime, opt_status) {
1620 this.timer += deltaTime;
1621
1622 // Update the status.
1623 if (opt_status) {
1624 this.status = opt_status;
1625 this.currentFrame = 0;
1626 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1627 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1628
1629 if (opt_status == Trex.status.WAITING) {
1630 this.animStartTime = getTimeStamp();
1631 this.setBlinkDelay();
1632 }
1633 }
1634
1635 // Game intro animation, T-rex moves in from the left.
1636 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1637 this.xPos += Math.round((this.config.START_X_POS /
1638 this.config.INTRO_DURATION) * deltaTime);
1639 }
1640
1641 if (this.status == Trex.status.WAITING) {
1642 this.blink(getTimeStamp());
1643 } else {
1644 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1645 }
1646
1647 // Update the frame position.
1648 if (this.timer >= this.msPerFrame) {
1649 this.currentFrame = this.currentFrame ==
1650 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1651 this.timer = 0;
1652 }
1653
1654 // Speed drop becomes duck if the down key is still being pressed.
1655 if (this.speedDrop && this.yPos == this.groundYPos) {
1656 this.speedDrop = false;
1657 this.setDuck(true);
1658 }
1659 },
1660
1661 /**
1662 * Draw the t-rex to a particular position.
1663 * @param {number} x
1664 * @param {number} y
1665 */
1666 draw: function(x, y) {
1667 var sourceX = x;
1668 var sourceY = y;
1669 var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
1670 this.config.WIDTH_DUCK : this.config.WIDTH;
1671 var sourceHeight = this.config.HEIGHT;
1672
1673 if (IS_HIDPI) {
1674 sourceX *= 2;
1675 sourceY *= 2;
1676 sourceWidth *= 2;
1677 sourceHeight *= 2;
1678 }
1679
1680 // Adjustments for sprite sheet position.
1681 sourceX += this.spritePos.x;
1682 sourceY += this.spritePos.y;
1683
1684 // Ducking.
1685 if (this.ducking && this.status != Trex.status.CRASHED) {
1686 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1687 sourceWidth, sourceHeight,
1688 this.xPos, this.yPos,
1689 this.config.WIDTH_DUCK, this.config.HEIGHT);
1690 } else {
1691 // Crashed whilst ducking. Trex is standing up so needs adjustment.
1692 if (this.ducking && this.status == Trex.status.CRASHED) {
1693 this.xPos++;
1694 }
1695 // Standing / running
1696 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1697 sourceWidth, sourceHeight,
1698 this.xPos, this.yPos,
1699 this.config.WIDTH, this.config.HEIGHT);
1700 }
1701 },
1702
1703 /**
1704 * Sets a random time for the blink to happen.
1705 */
1706 setBlinkDelay: function() {
1707 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1708 },
1709
1710 /**
1711 * Make t-rex blink at random intervals.
1712 * @param {number} time Current time in milliseconds.
1713 */
1714 blink: function(time) {
1715 var deltaTime = time - this.animStartTime;
1716
1717 if (deltaTime >= this.blinkDelay) {
1718 this.draw(this.currentAnimFrames[this.currentFrame], 0);
1719
1720 if (this.currentFrame == 1) {
1721 // Set new random delay to blink.
1722 this.setBlinkDelay();
1723 this.animStartTime = time;
1724 }
1725 }
1726 },
1727
1728 /**
1729 * Initialise a jump.
1730 * @param {number} speed
1731 */
1732 startJump: function(speed) {
1733 if (!this.jumping) {
1734 this.update(0, Trex.status.JUMPING);
1735 // Tweak the jump velocity based on the speed.
1736 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
1737 this.jumping = true;
1738 this.reachedMinHeight = false;
1739 this.speedDrop = false;
1740 }
1741 },
1742
1743 /**
1744 * Jump is complete, falling down.
1745 */
1746 endJump: function() {
1747 if (this.reachedMinHeight &&
1748 this.jumpVelocity < this.config.DROP_VELOCITY) {
1749 this.jumpVelocity = this.config.DROP_VELOCITY;
1750 }
1751 },
1752
1753 /**
1754 * Update frame for a jump.
1755 * @param {number} deltaTime
1756 * @param {number} speed
1757 */
1758 updateJump: function(deltaTime, speed) {
1759 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1760 var framesElapsed = deltaTime / msPerFrame;
1761
1762 // Speed drop makes Trex fall faster.
1763 if (this.speedDrop) {
1764 this.yPos += Math.round(this.jumpVelocity *
1765 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1766 } else {
1767 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1768 }
1769
1770 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1771
1772 // Minimum height has been reached.
1773 if (this.yPos < this.minJumpHeight || this.speedDrop) {
1774 this.reachedMinHeight = true;
1775 }
1776
1777 // Reached max height
1778 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1779 this.endJump();
1780 }
1781
1782 // Back down at ground level. Jump completed.
1783 if (this.yPos > this.groundYPos) {
1784 this.reset();
1785 this.jumpCount++;
1786 }
1787
1788 this.update(deltaTime);
1789 },
1790
1791 /**
1792 * Set the speed drop. Immediately cancels the current jump.
1793 */
1794 setSpeedDrop: function() {
1795 this.speedDrop = true;
1796 this.jumpVelocity = 1;
1797 },
1798
1799 /**
1800 * @param {boolean} isDucking.
1801 */
1802 setDuck: function(isDucking) {
1803 if (isDucking && this.status != Trex.status.DUCKING) {
1804 this.update(0, Trex.status.DUCKING);
1805 this.ducking = true;
1806 } else if (this.status == Trex.status.DUCKING) {
1807 this.update(0, Trex.status.RUNNING);
1808 this.ducking = false;
1809 }
1810 },
1811
1812 /**
1813 * Reset the t-rex to running at start of game.
1814 */
1815 reset: function() {
1816 this.yPos = this.groundYPos;
1817 this.jumpVelocity = 0;
1818 this.jumping = false;
1819 this.ducking = false;
1820 this.update(0, Trex.status.RUNNING);
1821 this.midair = false;
1822 this.speedDrop = false;
1823 this.jumpCount = 0;
1824 }
1825 };
1826
1827
1828 //******************************************************************************
1829
1830 /**
1831 * Handles displaying the distance meter.
1832 * @param {!HTMLCanvasElement} canvas
1833 * @param {Object} spritePos Image position in sprite.
1834 * @param {number} canvasWidth
1835 * @constructor
1836 */
1837 function DistanceMeter(canvas, spritePos, canvasWidth) {
1838 this.canvas = canvas;
1839 this.canvasCtx = canvas.getContext('2d');
1840 this.image = Runner.imageSprite;
1841 this.spritePos = spritePos;
1842 this.x = 0;
1843 this.y = 5;
1844
1845 this.currentDistance = 0;
1846 this.maxScore = 0;
1847 this.highScore = 0;
1848 this.container = null;
1849
1850 this.digits = [];
1851 this.acheivement = false;
1852 this.defaultString = '';
1853 this.flashTimer = 0;
1854 this.flashIterations = 0;
1855 this.invertTrigger = false;
1856
1857 this.config = DistanceMeter.config;
1858 this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
1859 this.init(canvasWidth);
1860 };
1861
1862
1863 /**
1864 * @enum {number}
1865 */
1866 DistanceMeter.dimensions = {
1867 WIDTH: 10,
1868 HEIGHT: 13,
1869 DEST_WIDTH: 11
1870 };
1871
1872
1873 /**
1874 * Y positioning of the digits in the sprite sheet.
1875 * X position is always 0.
1876 * @type {Array<number>}
1877 */
1878 DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1879
1880
1881 /**
1882 * Distance meter config.
1883 * @enum {number}
1884 */
1885 DistanceMeter.config = {
1886 // Number of digits.
1887 MAX_DISTANCE_UNITS: 5,
1888
1889 // Distance that causes achievement animation.
1890 ACHIEVEMENT_DISTANCE: 100,
1891
1892 // Used for conversion from pixel distance to a scaled unit.
1893 COEFFICIENT: 0.025,
1894
1895 // Flash duration in milliseconds.
1896 FLASH_DURATION: 1000 / 4,
1897
1898 // Flash iterations for achievement animation.
1899 FLASH_ITERATIONS: 3
1900 };
1901
1902
1903 DistanceMeter.prototype = {
1904 /**
1905 * Initialise the distance meter to '00000'.
1906 * @param {number} width Canvas width in px.
1907 */
1908 init: function(width) {
1909 var maxDistanceStr = '';
1910
1911 this.calcXPos(width);
1912 this.maxScore = this.maxScoreUnits;
1913 for (var i = 0; i < this.maxScoreUnits; i++) {
1914 this.draw(i, 0);
1915 this.defaultString += '0';
1916 maxDistanceStr += '9';
1917 }
1918
1919 this.maxScore = parseInt(maxDistanceStr);
1920 },
1921
1922 /**
1923 * Calculate the xPos in the canvas.
1924 * @param {number} canvasWidth
1925 */
1926 calcXPos: function(canvasWidth) {
1927 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1928 (this.maxScoreUnits + 1));
1929 },
1930
1931 /**
1932 * Draw a digit to canvas.
1933 * @param {number} digitPos Position of the digit.
1934 * @param {number} value Digit value 0-9.
1935 * @param {boolean} opt_highScore Whether drawing the high score.
1936 */
1937 draw: function(digitPos, value, opt_highScore) {
1938 var sourceWidth = DistanceMeter.dimensions.WIDTH;
1939 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1940 var sourceX = DistanceMeter.dimensions.WIDTH * value;
1941 var sourceY = 0;
1942
1943 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1944 var targetY = this.y;
1945 var targetWidth = DistanceMeter.dimensions.WIDTH;
1946 var targetHeight = DistanceMeter.dimensions.HEIGHT;
1947
1948 // For high DPI we 2x source values.
1949 if (IS_HIDPI) {
1950 sourceWidth *= 2;
1951 sourceHeight *= 2;
1952 sourceX *= 2;
1953 }
1954
1955 sourceX += this.spritePos.x;
1956 sourceY += this.spritePos.y;
1957
1958 this.canvasCtx.save();
1959
1960 if (opt_highScore) {
1961 // Left of the current score.
1962 var highScoreX = this.x - (this.maxScoreUnits * 2) *
1963 DistanceMeter.dimensions.WIDTH;
1964 this.canvasCtx.translate(highScoreX, this.y);
1965 } else {
1966 this.canvasCtx.translate(this.x, this.y);
1967 }
1968
1969 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1970 sourceWidth, sourceHeight,
1971 targetX, targetY,
1972 targetWidth, targetHeight
1973 );
1974
1975 this.canvasCtx.restore();
1976 },
1977
1978 /**
1979 * Covert pixel distance to a 'real' distance.
1980 * @param {number} distance Pixel distance ran.
1981 * @return {number} The 'real' distance ran.
1982 */
1983 getActualDistance: function(distance) {
1984 return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
1985 },
1986
1987 /**
1988 * Update the distance meter.
1989 * @param {number} distance
1990 * @param {number} deltaTime
1991 * @return {boolean} Whether the acheivement sound fx should be played.
1992 */
1993 update: function(deltaTime, distance) {
1994 var paint = true;
1995 var playSound = false;
1996
1997 if (!this.acheivement) {
1998 distance = this.getActualDistance(distance);
1999 // Score has gone beyond the initial digit count.
2000 if (distance > this.maxScore && this.maxScoreUnits ==
2001 this.config.MAX_DISTANCE_UNITS) {
2002 this.maxScoreUnits++;
2003 this.maxScore = parseInt(this.maxScore + '9');
2004 } else {
2005 this.distance = 0;
2006 }
2007
2008 if (distance > 0) {
2009 // Acheivement unlocked
2010 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
2011 // Flash score and play sound.
2012 this.acheivement = true;
2013 this.flashTimer = 0;
2014 playSound = true;
2015 }
2016
2017 // Create a string representation of the distance with leading 0.
2018 var distanceStr = (this.defaultString +
2019 distance).substr(-this.maxScoreUnits);
2020 this.digits = distanceStr.split('');
2021 } else {
2022 this.digits = this.defaultString.split('');
2023 }
2024 } else {
2025 // Control flashing of the score on reaching acheivement.
2026 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
2027 this.flashTimer += deltaTime;
2028
2029 if (this.flashTimer < this.config.FLASH_DURATION) {
2030 paint = false;
2031 } else if (this.flashTimer >
2032 this.config.FLASH_DURATION * 2) {
2033 this.flashTimer = 0;
2034 this.flashIterations++;
2035 }
2036 } else {
2037 this.acheivement = false;
2038 this.flashIterations = 0;
2039 this.flashTimer = 0;
2040 }
2041 }
2042
2043 // Draw the digits if not flashing.
2044 if (paint) {
2045 for (var i = this.digits.length - 1; i >= 0; i--) {
2046 this.draw(i, parseInt(this.digits[i]));
2047 }
2048 }
2049
2050 this.drawHighScore();
2051 return playSound;
2052 },
2053
2054 /**
2055 * Draw the high score.
2056 */
2057 drawHighScore: function() {
2058 this.canvasCtx.save();
2059 this.canvasCtx.globalAlpha = .8;
2060 for (var i = this.highScore.length - 1; i >= 0; i--) {
2061 this.draw(i, parseInt(this.highScore[i], 10), true);
2062 }
2063 this.canvasCtx.restore();
2064 },
2065
2066 /**
2067 * Set the highscore as a array string.
2068 * Position of char in the sprite: H - 10, I - 11.
2069 * @param {number} distance Distance ran in pixels.
2070 */
2071 setHighScore: function(distance) {
2072 distance = this.getActualDistance(distance);
2073 var highScoreStr = (this.defaultString +
2074 distance).substr(-this.maxScoreUnits);
2075
2076 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
2077 },
2078
2079 /**
2080 * Reset the distance meter back to '00000'.
2081 */
2082 reset: function() {
2083 this.update(0);
2084 this.acheivement = false;
2085 }
2086 };
2087
2088
2089 //******************************************************************************
2090
2091 /**
2092 * Cloud background item.
2093 * Similar to an obstacle object but without collision boxes.
2094 * @param {HTMLCanvasElement} canvas Canvas element.
2095 * @param {Object} spritePos Position of image in sprite.
2096 * @param {number} containerWidth
2097 */
2098 function Cloud(canvas, spritePos, containerWidth) {
2099 this.canvas = canvas;
2100 this.canvasCtx = this.canvas.getContext('2d');
2101 this.spritePos = spritePos;
2102 this.containerWidth = containerWidth;
2103 this.xPos = containerWidth;
2104 this.yPos = 0;
2105 this.remove = false;
2106 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
2107 Cloud.config.MAX_CLOUD_GAP);
2108
2109 this.init();
2110 };
2111
2112
2113 /**
2114 * Cloud object config.
2115 * @enum {number}
2116 */
2117 Cloud.config = {
2118 HEIGHT: 14,
2119 MAX_CLOUD_GAP: 400,
2120 MAX_SKY_LEVEL: 30,
2121 MIN_CLOUD_GAP: 100,
2122 MIN_SKY_LEVEL: 71,
2123 WIDTH: 46
2124 };
2125
2126
2127 Cloud.prototype = {
2128 /**
2129 * Initialise the cloud. Sets the Cloud height.
2130 */
2131 init: function() {
2132 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
2133 Cloud.config.MIN_SKY_LEVEL);
2134 this.draw();
2135 },
2136
2137 /**
2138 * Draw the cloud.
2139 */
2140 draw: function() {
2141 this.canvasCtx.save();
2142 var sourceWidth = Cloud.config.WIDTH;
2143 var sourceHeight = Cloud.config.HEIGHT;
2144
2145 if (IS_HIDPI) {
2146 sourceWidth = sourceWidth * 2;
2147 sourceHeight = sourceHeight * 2;
2148 }
2149
2150 this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
2151 this.spritePos.y,
2152 sourceWidth, sourceHeight,
2153 this.xPos, this.yPos,
2154 Cloud.config.WIDTH, Cloud.config.HEIGHT);
2155
2156 this.canvasCtx.restore();
2157 },
2158
2159 /**
2160 * Update the cloud position.
2161 * @param {number} speed
2162 */
2163 update: function(speed) {
2164 if (!this.remove) {
2165 this.xPos -= Math.ceil(speed);
2166 this.draw();
2167
2168 // Mark as removeable if no longer in the canvas.
2169 if (!this.isVisible()) {
2170 this.remove = true;
2171 }
2172 }
2173 },
2174
2175 /**
2176 * Check if the cloud is visible on the stage.
2177 * @return {boolean}
2178 */
2179 isVisible: function() {
2180 return this.xPos + Cloud.config.WIDTH > 0;
2181 }
2182 };
2183
2184
2185 //******************************************************************************
2186
2187 /**
2188 * Nightmode shows a moon and stars on the horizon.
2189 */
2190 function NightMode(canvas, spritePos, containerWidth) {
2191 this.spritePos = spritePos;
2192 this.canvas = canvas;
2193 this.canvasCtx = canvas.getContext('2d');
2194 this.xPos = containerWidth - 50;
2195 this.yPos = 30;
2196 this.currentPhase = 0;
2197 this.opacity = 0;
2198 this.containerWidth = containerWidth;
2199 this.stars = [];
2200 this.drawStars = false;
2201 this.placeStars();
2202 };
2203
2204 /**
2205 * @enum {number}
2206 */
2207 NightMode.config = {
2208 FADE_SPEED: 0.035,
2209 HEIGHT: 40,
2210 MOON_SPEED: 0.25,
2211 NUM_STARS: 2,
2212 STAR_SIZE: 9,
2213 STAR_SPEED: 0.3,
2214 STAR_MAX_Y: 70,
2215 WIDTH: 20
2216 };
2217
2218 NightMode.phases = [140, 120, 100, 60, 40, 20, 0];
2219
2220 NightMode.prototype = {
2221 /**
2222 * Update moving moon, changing phases.
2223 * @param {boolean} activated Whether night mode is activated.
2224 * @param {number} delta
2225 */
2226 update: function(activated, delta) {
2227 // Moon phase.
2228 if (activated && this.opacity == 0) {
2229 this.currentPhase++;
2230
2231 if (this.currentPhase >= NightMode.phases.length) {
2232 this.currentPhase = 0;
2233 }
2234 }
2235
2236 // Fade in / out.
2237 if (activated && (this.opacity < 1 || this.opacity == 0)) {
2238 this.opacity += NightMode.config.FADE_SPEED;
2239 } else if (this.opacity > 0) {
2240 this.opacity -= NightMode.config.FADE_SPEED;
2241 }
2242
2243 // Set moon positioning.
2244 if (this.opacity > 0) {
2245 this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);
2246
2247 // Update stars.
2248 if (this.drawStars) {
2249 for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
2250 this.stars[i].x = this.updateXPos(this.stars[i].x,
2251 NightMode.config.STAR_SPEED);
2252 }
2253 }
2254 this.draw();
2255 } else {
2256 this.opacity = 0;
2257 this.placeStars();
2258 }
2259 this.drawStars = true;
2260 },
2261
2262 updateXPos: function(currentPos, speed) {
2263 if (currentPos < -NightMode.config.WIDTH) {
2264 currentPos = this.containerWidth;
2265 } else {
2266 currentPos -= speed;
2267 }
2268 return currentPos;
2269 },
2270
2271 draw: function() {
2272 var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :
2273 NightMode.config.WIDTH;
2274 var moonSourceHeight = NightMode.config.HEIGHT;
2275 var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];
2276 var moonOutputWidth = moonSourceWidth;
2277 var starSize = NightMode.config.STAR_SIZE;
2278 var starSourceX = Runner.spriteDefinition.LDPI.STAR.x;
2279
2280 if (IS_HIDPI) {
2281 moonSourceWidth *= 2;
2282 moonSourceHeight *= 2;
2283 moonSourceX = this.spritePos.x +
2284 (NightMode.phases[this.currentPhase] * 2);
2285 starSize *= 2;
2286 starSourceX = Runner.spriteDefinition.HDPI.STAR.x;
2287 }
2288
2289 this.canvasCtx.save();
2290 this.canvasCtx.globalAlpha = this.opacity;
2291
2292 // Stars.
2293 if (this.drawStars) {
2294 for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
2295 this.canvasCtx.drawImage(Runner.imageSprite,
2296 starSourceX, this.stars[i].sourceY, starSize, starSize,
2297 Math.round(this.stars[i].x), this.stars[i].y,
2298 NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE);
2299 }
2300 }
2301
2302 // Moon.
2303 this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX,
2304 this.spritePos.y, moonSourceWidth, moonSourceHeight,
2305 Math.round(this.xPos), this.yPos,
2306 moonOutputWidth, NightMode.config.HEIGHT);
2307
2308 this.canvasCtx.globalAlpha = 1;
2309 this.canvasCtx.restore();
2310 },
2311
2312 // Do star placement.
2313 placeStars: function() {
2314 var segmentSize = Math.round(this.containerWidth /
2315 NightMode.config.NUM_STARS);
2316
2317 for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
2318 this.stars[i] = {};
2319 this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));
2320 this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);
2321
2322 if (IS_HIDPI) {
2323 this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y +
2324 NightMode.config.STAR_SIZE * 2 * i;
2325 } else {
2326 this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +
2327 NightMode.config.STAR_SIZE * i;
2328 }
2329 }
2330 },
2331
2332 reset: function() {
2333 this.currentPhase = 0;
2334 this.opacity = 0;
2335 this.update(false);
2336 }
2337
2338 };
2339
2340
2341 //******************************************************************************
2342
2343 /**
2344 * Horizon Line.
2345 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
2346 * @param {HTMLCanvasElement} canvas
2347 * @param {Object} spritePos Horizon position in sprite.
2348 * @constructor
2349 */
2350 function HorizonLine(canvas, spritePos) {
2351 this.spritePos = spritePos;
2352 this.canvas = canvas;
2353 this.canvasCtx = canvas.getContext('2d');
2354 this.sourceDimensions = {};
2355 this.dimensions = HorizonLine.dimensions;
2356 this.sourceXPos = [this.spritePos.x, this.spritePos.x +
2357 this.dimensions.WIDTH];
2358 this.xPos = [];
2359 this.yPos = 0;
2360 this.bumpThreshold = 0.5;
2361
2362 this.setSourceDimensions();
2363 this.draw();
2364 };
2365
2366
2367 /**
2368 * Horizon line dimensions.
2369 * @enum {number}
2370 */
2371 HorizonLine.dimensions = {
2372 WIDTH: 600,
2373 HEIGHT: 12,
2374 YPOS: 127
2375 };
2376
2377
2378 HorizonLine.prototype = {
2379 /**
2380 * Set the source dimensions of the horizon line.
2381 */
2382 setSourceDimensions: function() {
2383
2384 for (var dimension in HorizonLine.dimensions) {
2385 if (IS_HIDPI) {
2386 if (dimension != 'YPOS') {
2387 this.sourceDimensions[dimension] =
2388 HorizonLine.dimensions[dimension] * 2;
2389 }
2390 } else {
2391 this.sourceDimensions[dimension] =
2392 HorizonLine.dimensions[dimension];
2393 }
2394 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
2395 }
2396
2397 this.xPos = [0, HorizonLine.dimensions.WIDTH];
2398 this.yPos = HorizonLine.dimensions.YPOS;
2399 },
2400
2401 /**
2402 * Return the crop x position of a type.
2403 */
2404 getRandomType: function() {
2405 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
2406 },
2407
2408 /**
2409 * Draw the horizon line.
2410 */
2411 draw: function() {
2412 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
2413 this.spritePos.y,
2414 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2415 this.xPos[0], this.yPos,
2416 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2417
2418 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
2419 this.spritePos.y,
2420 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2421 this.xPos[1], this.yPos,
2422 this.dimensions.WIDTH, this.dimensions.HEIGHT);
2423 },
2424
2425 /**
2426 * Update the x position of an indivdual piece of the line.
2427 * @param {number} pos Line position.
2428 * @param {number} increment
2429 */
2430 updateXPos: function(pos, increment) {
2431 var line1 = pos;
2432 var line2 = pos == 0 ? 1 : 0;
2433
2434 this.xPos[line1] -= increment;
2435 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2436
2437 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2438 this.xPos[line1] += this.dimensions.WIDTH * 2;
2439 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2440 this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
2441 }
2442 },
2443
2444 /**
2445 * Update the horizon line.
2446 * @param {number} deltaTime
2447 * @param {number} speed
2448 */
2449 update: function(deltaTime, speed) {
2450 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2451
2452 if (this.xPos[0] <= 0) {
2453 this.updateXPos(0, increment);
2454 } else {
2455 this.updateXPos(1, increment);
2456 }
2457 this.draw();
2458 },
2459
2460 /**
2461 * Reset horizon to the starting position.
2462 */
2463 reset: function() {
2464 this.xPos[0] = 0;
2465 this.xPos[1] = HorizonLine.dimensions.WIDTH;
2466 }
2467 };
2468
2469
2470 //******************************************************************************
2471
2472 /**
2473 * Horizon background class.
2474 * @param {HTMLCanvasElement} canvas
2475 * @param {Object} spritePos Sprite positioning.
2476 * @param {Object} dimensions Canvas dimensions.
2477 * @param {number} gapCoefficient
2478 * @constructor
2479 */
2480 function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
2481 this.canvas = canvas;
2482 this.canvasCtx = this.canvas.getContext('2d');
2483 this.config = Horizon.config;
2484 this.dimensions = dimensions;
2485 this.gapCoefficient = gapCoefficient;
2486 this.obstacles = [];
2487 this.obstacleHistory = [];
2488 this.horizonOffsets = [0, 0];
2489 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2490 this.spritePos = spritePos;
2491 this.nightMode = null;
2492
2493 // Cloud
2494 this.clouds = [];
2495 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2496
2497 // Horizon
2498 this.horizonLine = null;
2499 this.init();
2500 };
2501
2502
2503 /**
2504 * Horizon config.
2505 * @enum {number}
2506 */
2507 Horizon.config = {
2508 BG_CLOUD_SPEED: 0.2,
2509 BUMPY_THRESHOLD: .3,
2510 CLOUD_FREQUENCY: .5,
2511 HORIZON_HEIGHT: 16,
2512 MAX_CLOUDS: 6
2513 };
2514
2515
2516 Horizon.prototype = {
2517 /**
2518 * Initialise the horizon. Just add the line and a cloud. No obstacles.
2519 */
2520 init: function() {
2521 this.addCloud();
2522 this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
2523 this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
2524 this.dimensions.WIDTH);
2525 },
2526
2527 /**
2528 * @param {number} deltaTime
2529 * @param {number} currentSpeed
2530 * @param {boolean} updateObstacles Used as an override to prevent
2531 * the obstacles from being updated / added. This happens in the
2532 * ease in section.
2533 * @param {boolean} showNightMode Night mode activated.
2534 */
2535 update: function(deltaTime, currentSpeed, updateObstacles, showNightMode) {
2536 this.runningTime += deltaTime;
2537 this.horizonLine.update(deltaTime, currentSpeed);
2538 this.nightMode.update(showNightMode);
2539 this.updateClouds(deltaTime, currentSpeed);
2540
2541 if (updateObstacles) {
2542 this.updateObstacles(deltaTime, currentSpeed);
2543 }
2544 },
2545
2546 /**
2547 * Update the cloud positions.
2548 * @param {number} deltaTime
2549 * @param {number} currentSpeed
2550 */
2551 updateClouds: function(deltaTime, speed) {
2552 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2553 var numClouds = this.clouds.length;
2554
2555 if (numClouds) {
2556 for (var i = numClouds - 1; i >= 0; i--) {
2557 this.clouds[i].update(cloudSpeed);
2558 }
2559
2560 var lastCloud = this.clouds[numClouds - 1];
2561
2562 // Check for adding a new cloud.
2563 if (numClouds < this.config.MAX_CLOUDS &&
2564 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2565 this.cloudFrequency > Math.random()) {
2566 this.addCloud();
2567 }
2568
2569 // Remove expired clouds.
2570 this.clouds = this.clouds.filter(function(obj) {
2571 return !obj.remove;
2572 });
2573 } else {
2574 this.addCloud();
2575 }
2576 },
2577
2578 /**
2579 * Update the obstacle positions.
2580 * @param {number} deltaTime
2581 * @param {number} currentSpeed
2582 */
2583 updateObstacles: function(deltaTime, currentSpeed) {
2584 // Obstacles, move to Horizon layer.
2585 var updatedObstacles = this.obstacles.slice(0);
2586
2587 for (var i = 0; i < this.obstacles.length; i++) {
2588 var obstacle = this.obstacles[i];
2589 obstacle.update(deltaTime, currentSpeed);
2590
2591 // Clean up existing obstacles.
2592 if (obstacle.remove) {
2593 updatedObstacles.shift();
2594 }
2595 }
2596 this.obstacles = updatedObstacles;
2597
2598 if (this.obstacles.length > 0) {
2599 var lastObstacle = this.obstacles[this.obstacles.length - 1];
2600
2601 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2602 lastObstacle.isVisible() &&
2603 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2604 this.dimensions.WIDTH) {
2605 this.addNewObstacle(currentSpeed);
2606 lastObstacle.followingObstacleCreated = true;
2607 }
2608 } else {
2609 // Create new obstacles.
2610 this.addNewObstacle(currentSpeed);
2611 }
2612 },
2613
2614 removeFirstObstacle: function() {
2615 this.obstacles.shift();
2616 },
2617
2618 /**
2619 * Add a new obstacle.
2620 * @param {number} currentSpeed
2621 */
2622 addNewObstacle: function(currentSpeed) {
2623 var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
2624 var obstacleType = Obstacle.types[obstacleTypeIndex];
2625
2626 // Check for multiples of the same type of obstacle.
2627 // Also check obstacle is available at current speed.
2628 if (this.duplicateObstacleCheck(obstacleType.type) ||
2629 currentSpeed < obstacleType.minSpeed) {
2630 this.addNewObstacle(currentSpeed);
2631 } else {
2632 var obstacleSpritePos = this.spritePos[obstacleType.type];
2633
2634 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2635 obstacleSpritePos, this.dimensions,
2636 this.gapCoefficient, currentSpeed, obstacleType.width));
2637
2638 this.obstacleHistory.unshift(obstacleType.type);
2639
2640 if (this.obstacleHistory.length > 1) {
2641 this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
2642 }
2643 }
2644 },
2645
2646 /**
2647 * Returns whether the previous two obstacles are the same as the next one.
2648 * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
2649 * @return {boolean}
2650 */
2651 duplicateObstacleCheck: function(nextObstacleType) {
2652 var duplicateCount = 0;
2653
2654 for (var i = 0; i < this.obstacleHistory.length; i++) {
2655 duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
2656 duplicateCount + 1 : 0;
2657 }
2658 return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
2659 },
2660
2661 /**
2662 * Reset the horizon layer.
2663 * Remove existing obstacles and reposition the horizon line.
2664 */
2665 reset: function() {
2666 this.obstacles = [];
2667 this.horizonLine.reset();
2668 this.nightMode.reset();
2669 },
2670
2671 /**
2672 * Update the canvas width and scaling.
2673 * @param {number} width Canvas width.
2674 * @param {number} height Canvas height.
2675 */
2676 resize: function(width, height) {
2677 this.canvas.width = width;
2678 this.canvas.height = height;
2679 },
2680
2681 /**
2682 * Add a new cloud to the horizon.
2683 */
2684 addCloud: function() {
2685 this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
2686 this.dimensions.WIDTH));
2687 }
2688 };
2689 })();
OLDNEW
« no previous file with comments | « chrome/renderer/resources/neterror.js ('k') | chrome/renderer/resources/renderer_resources.grd » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698