· 8 years ago · Mar 05, 2018, 06:36 PM
1<html dir="ltr" lang="en" i18n-processed=""><head>
2 <meta charset="utf-8">
3 <meta name="viewport" content="width=device-width, initial-scale=1.0,
4 maximum-scale=1.0, user-scalable=no">
5 <title>chrome-extension://flejfacjooompmliegamfbpjjdlhokhj/devices.html</title>
6 <style>/* Copyright 2017 The Chromium Authors. All rights reserved.
7 * Use of this source code is governed by a BSD-style license that can be
8 * found in the LICENSE file. */
9
10a {
11 color: rgb(88, 88, 88);
12}
13
14body {
15 background-color: rgb(247, 247, 247);
16 color: rgb(100, 100, 100);
17}
18
19#details-button {
20 background: inherit;
21 border: 0;
22 float: none;
23 margin: 0;
24 padding: 10px 0;
25 text-transform: uppercase;
26}
27
28.hidden {
29 display: none;
30}
31
32html {
33 -webkit-text-size-adjust: 100%;
34 font-size: 125%;
35}
36
37.icon {
38 background-repeat: no-repeat;
39 background-size: 100%;
40}</style>
41 <style>/* Copyright 2014 The Chromium Authors. All rights reserved.
42 Use of this source code is governed by a BSD-style license that can be
43 found in the LICENSE file. */
44
45button {
46 border: 0;
47 border-radius: 2px;
48 box-sizing: border-box;
49 color: #fff;
50 cursor: pointer;
51 float: right;
52 font-size: .875em;
53 margin: 0;
54 padding: 10px 24px;
55 transition: box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1);
56 user-select: none;
57}
58
59[dir='rtl'] button {
60 float: left;
61}
62
63.bad-clock button,
64.captive-portal button,
65.main-frame-blocked button,
66.neterror button,
67.offline button,
68.ssl button {
69 background: rgb(66, 133, 244);
70}
71
72button:active {
73 background: rgb(50, 102, 213);
74 outline: 0;
75}
76
77button:hover {
78 box-shadow: 0 1px 3px rgba(0, 0, 0, .50);
79}
80
81#debugging {
82 display: inline;
83 overflow: auto;
84}
85
86.debugging-content {
87 line-height: 1em;
88 margin-bottom: 0;
89 margin-top: 1em;
90}
91
92.debugging-content-fixed-width {
93 display: block;
94 font-family: monospace;
95 font-size: 1.2em;
96 margin-top: 0.5em;
97}
98
99.debugging-title {
100 font-weight: bold;
101}
102
103#details {
104 color: #696969;
105 margin: 0 0 50px;
106}
107
108#details p:not(:first-of-type) {
109 margin-top: 20px;
110}
111
112#details-button:hover {
113 box-shadow: inherit;
114 text-decoration: underline;
115}
116
117.error-code {
118 color: #646464;
119 font-size: .86667em;
120 text-transform: uppercase;
121}
122
123#error-debugging-info {
124 font-size: 0.8em;
125}
126
127h1 {
128 color: #333;
129 font-size: 1.6em;
130 font-weight: normal;
131 line-height: 1.25em;
132 margin-bottom: 16px;
133}
134
135h2 {
136 font-size: 1.2em;
137 font-weight: normal;
138}
139
140.icon {
141 height: 72px;
142 margin: 0 0 40px;
143 width: 72px;
144}
145
146input[type=checkbox] {
147 opacity: 0;
148}
149
150input[type=checkbox]:focus ~ .checkbox {
151 outline: -webkit-focus-ring-color auto 5px;
152}
153
154.interstitial-wrapper {
155 box-sizing: border-box;
156 font-size: 1em;
157 line-height: 1.6em;
158 margin: 14vh auto 0;
159 max-width: 600px;
160 width: 100%;
161}
162
163#main-message > p {
164 display: inline;
165}
166
167#extended-reporting-opt-in {
168 font-size: .875em;
169 margin-top: 39px;
170}
171
172#extended-reporting-opt-in label {
173 position: relative;
174 display: flex;
175 align-items: flex-start;
176}
177
178.nav-wrapper {
179 margin-top: 51px;
180}
181
182.nav-wrapper::after {
183 clear: both;
184 content: '';
185 display: table;
186 width: 100%;
187}
188
189.small-link {
190 color: #696969;
191 font-size: .875em;
192}
193
194.checkboxes {
195 flex: 0 0 24px;
196}
197
198.checkbox {
199 background: transparent;
200 border: 1px solid white;
201 border-radius: 2px;
202 display: block;
203 height: 14px;
204 left: 0;
205 position: absolute;
206 right: 0;
207 top: 3px;
208 width: 14px;
209}
210
211.checkbox::before {
212 background: transparent;
213 border: 2px solid white;
214 border-right-width: 0;
215 border-top-width: 0;
216 content: '';
217 height: 4px;
218 left: 2px;
219 opacity: 0;
220 position: absolute;
221 top: 3px;
222 transform: rotate(-45deg);
223 width: 9px;
224}
225
226input[type=checkbox]:checked ~ .checkbox::before {
227 opacity: 1;
228}
229
230@media (max-width: 700px) {
231 .interstitial-wrapper {
232 padding: 0 10%;
233 }
234
235 #error-debugging-info {
236 overflow: auto;
237 }
238}
239
240@media (max-height: 600px) {
241 .error-code {
242 margin-top: 10px;
243 }
244}
245
246@media (max-width: 420px) {
247 button,
248 [dir='rtl'] button,
249 .small-link {
250 float: none;
251 font-size: .825em;
252 font-weight: 400;
253 margin: 0;
254 text-transform: uppercase;
255 width: 100%;
256 }
257
258 #details {
259 margin: 20px 0 20px 0;
260 }
261
262 #details p:not(:first-of-type) {
263 margin-top: 10px;
264 }
265
266 #details-button {
267 display: block;
268 margin-top: 20px;
269 text-align: center;
270 width: 100%;
271 }
272
273 .interstitial-wrapper {
274 padding: 0 5%;
275 }
276
277 #extended-reporting-opt-in {
278 margin-top: 24px;
279 }
280
281 .nav-wrapper {
282 margin-top: 30px;
283 }
284}
285
286/**
287 * Mobile specific styling.
288 * Navigation buttons are anchored to the bottom of the screen.
289 * Details message replaces the top content in its own scrollable area.
290 */
291
292@media (max-width: 420px) {
293 #details-button {
294 border: 0;
295 margin: 8px 0 0;
296 }
297
298 .secondary-button {
299 -webkit-margin-end: 0;
300 margin-top: 16px;
301 }
302}
303
304/* Fixed nav. */
305@media (min-width: 240px) and (max-width: 420px) and
306 (min-height: 401px),
307 (min-width: 421px) and (min-height: 240px) and
308 (max-height: 560px) {
309 body .nav-wrapper {
310 background: #f7f7f7;
311 bottom: 0;
312 box-shadow: 0 -22px 40px rgb(247, 247, 247);
313 left: 0;
314 margin: 0 auto;
315 max-width: 736px;
316 padding-left: 24px;
317 padding-right: 24px;
318 position: fixed;
319 right: 0;
320 width: 100%;
321 z-index: 2;
322 }
323
324 .interstitial-wrapper {
325 max-width: 736px;
326 }
327
328 #details,
329 #main-content {
330 padding-bottom: 40px;
331 }
332
333 #details {
334 padding-top: 5.5vh;
335 }
336
337 #details-button:hover {
338 box-shadow: none;
339 }
340}
341
342@media (max-width: 420px) and (orientation: portrait),
343 (max-height: 560px) {
344 body {
345 margin: 0 auto;
346 }
347
348 button,
349 [dir='rtl'] button,
350 button.small-link {
351 font-family: Roboto-Regular,Helvetica;
352 font-size: .933em;
353 font-weight: 600;
354 margin: 6px 0;
355 text-transform: uppercase;
356 transform: translatez(0);
357 }
358
359 .nav-wrapper {
360 box-sizing: border-box;
361 padding-bottom: 8px;
362 width: 100%;
363 }
364
365 .error-code {
366 margin-top: 0;
367 }
368
369 #details {
370 box-sizing: border-box;
371 height: auto;
372 margin: 0;
373 opacity: 1;
374 transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
375 }
376
377 #details.hidden,
378 #main-content.hidden {
379 display: block;
380 height: 0;
381 opacity: 0;
382 overflow: hidden;
383 padding-bottom: 0;
384 transition: none;
385 }
386
387 #details-button {
388 padding-bottom: 16px;
389 padding-top: 16px;
390 }
391
392 h1 {
393 font-size: 1.5em;
394 margin-bottom: 8px;
395 }
396
397 .icon {
398 margin-bottom: 5.69vh;
399 }
400
401 .interstitial-wrapper {
402 box-sizing: border-box;
403 margin: 7vh auto 12px;
404 padding: 0 24px;
405 position: relative;
406 }
407
408 .interstitial-wrapper p {
409 font-size: .95em;
410 line-height: 1.61em;
411 margin-top: 8px;
412 }
413
414 #main-content {
415 margin: 0;
416 transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1);
417 }
418
419 .small-link {
420 border: 0;
421 }
422
423 .suggested-left > #control-buttons,
424 .suggested-right > #control-buttons {
425 float: none;
426 margin: 0;
427 }
428}
429
430@media (min-width: 421px) and (min-height: 500px) and (max-height: 560px) {
431 .interstitial-wrapper {
432 margin-top: 10vh;
433 }
434}
435
436@media (min-height: 400px) and (orientation:portrait) {
437 .interstitial-wrapper {
438 margin-bottom: 145px;
439 }
440}
441
442@media (min-height: 299px) {
443 .nav-wrapper {
444 padding-bottom: 16px;
445 }
446}
447
448@media (min-height: 500px) and (max-height: 650px) and (max-width: 414px) and
449 (orientation: portrait) {
450 .interstitial-wrapper {
451 margin-top: 7vh;
452 }
453}
454
455@media (min-height: 650px) and (max-width: 414px) and (orientation: portrait) {
456 .interstitial-wrapper {
457 margin-top: 10vh;
458 }
459}
460
461/* Small mobile screens. No fixed nav. */
462@media (max-height: 400px) and (orientation: portrait),
463 (max-height: 239px) and (orientation: landscape),
464 (max-width: 419px) and (max-height: 399px) {
465 .interstitial-wrapper {
466 display: flex;
467 flex-direction: column;
468 margin-bottom: 0;
469 }
470
471 #details {
472 flex: 1 1 auto;
473 order: 0;
474 }
475
476 #main-content {
477 flex: 1 1 auto;
478 order: 0;
479 }
480
481 .nav-wrapper {
482 flex: 0 1 auto;
483 margin-top: 8px;
484 order: 1;
485 padding-left: 0;
486 padding-right: 0;
487 position: relative;
488 width: 100%;
489 }
490}
491
492@media (max-width: 239px) and (orientation: portrait) {
493 .nav-wrapper {
494 padding-left: 0;
495 padding-right: 0;
496 }
497}
498</style>
499 <style>/* Copyright 2013 The Chromium Authors. All rights reserved.
500 * Use of this source code is governed by a BSD-style license that can be
501 * found in the LICENSE file. */
502
503/* Don't use the main frame div when the error is in a subframe. */
504html[subframe] #main-frame-error {
505 display: none;
506}
507
508/* Don't use the subframe error div when the error is in a main frame. */
509html:not([subframe]) #sub-frame-error {
510 display: none;
511}
512
513#diagnose-button {
514 -webkit-margin-start: 0;
515 float: none;
516 margin-bottom: 10px;
517 margin-top: 20px;
518}
519
520h1 {
521 margin-top: 0;
522 word-wrap: break-word;
523}
524
525h1 span {
526 font-weight: 500;
527}
528
529h2 {
530 color: #666;
531 font-size: 1.2em;
532 font-weight: normal;
533 margin: 10px 0;
534}
535
536a {
537 color: rgb(17, 85, 204);
538 text-decoration: none;
539}
540
541.icon {
542 -webkit-user-select: none;
543 display: inline-block;
544}
545
546.icon-generic {
547 /**
548 * Can't access chrome://theme/IDR_ERROR_NETWORK_GENERIC from an untrusted
549 * renderer process, so embed the resource manually.
550 */
551 content: -webkit-image-set(
552 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAENJREFUeF7tzbEJACEQRNGBLeAasBCza2lLEGx0CxFGG9hBMDDxRy/72O9FMnIFapGylsu1fgoBdkXfUHLrQgdfrlJN1BdYBjQQm3UAAAAASUVORK5CYII=) 1x,
553 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQAQMAAADdiHD7AAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAFJJREFUeF7t0cENgDAMQ9FwYgxG6WjpaIzCCAxQxVggFuDiCvlLOeRdHR9yzjncHVoq3npu+wQUrUuJHylSTmBaespJyJQoObUeyxDQb3bEm5Au81c0pSCD8HYAAAAASUVORK5CYII=) 2x);
554}
555
556.icon-offline {
557 content: -webkit-image-set(
558 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAGxJREFUeF7tyMEJwkAQRuFf5ipMKxYQiJ3Z2nSwrWwBA0+DQZcdxEOueaePp9+dQZFB7GpUcURSVU66yVNFj6LFICatThZB6r/ko/pbRpUgilY0Cbw5sNmb9txGXUKyuH7eV25x39DtJXUNPQGJtWFV+BT/QAAAAABJRU5ErkJggg==) 1x,
559 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQBAMAAAAVaP+LAAAAGFBMVEUAAABTU1NNTU1TU1NPT09SUlJSUlJTU1O8B7DEAAAAB3RSTlMAoArVKvVgBuEdKgAAAJ1JREFUeF7t1TEOwyAMQNG0Q6/UE+RMXD9d/tC6womIFSL9P+MnAYOXeTIzMzMzMzMzaz8J9Ri6HoITmuHXhISE8nEh9yxDh55aCEUoTGbbQwjqHwIkRAEiIaG0+0AA9VBMaE89Rogeoww936MQrWdBr4GN/z0IAdQ6nQ/FIpRXDwHcA+JIJcQowQAlFUA0MfQpXLlVQfkzR4igS6ENjknm/wiaGhsAAAAASUVORK5CYII=) 2x);
560 position: relative;
561}
562
563.icon-disabled {
564 content: -webkit-image-set(
565 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAABICAMAAAAZF4G5AAAABlBMVEVMaXFTU1OXUj8tAAAAAXRSTlMAQObYZgAAASZJREFUeAHd11Fq7jAMRGGf/W/6PoWB67YMqv5DybwG/CFjRuR8JBw3+ByiRjgV9W/TJ31P0tBfC6+cj1haUFXKHmVJo5wP98WwQ0ZCbfUc6LQ6VuUBz31ikADkLMkDrfUC4rR6QGW+gF6rx7NaHWCj1Y/W6lf4L7utvgBSt3rBFSS/XBMPUILcJINHCBWYUfpWn4NBi1ZfudIc3rf6/NGEvEA+AsYTJozmXemjXeLZAov+mnkN2HfzXpMSVQDnGw++57qNJ4D1xitA2sJ+VAWMygSEaYf2mYPTjZfk2K8wmP7HLIH5Mg4/pP+PEcDzUvDMvYbs/2NWwPO5vBdMZE4EE5UTQLiBFDaUlTDPBRoJ9HdAYIkIo06og3BNXtCzy7zA1aXk5x+tJARq63eAygAAAABJRU5ErkJggg==) 1x,
566 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOAAAACQAQMAAAArwfVjAAAABlBMVEVMaXFTU1OXUj8tAAAAAXRSTlMAQObYZgAAAYdJREFUeF7F1EFqwzAUBNARAmVj0FZe5QoBH6BX+dn4GlY2PYNzGx/A0CvkCIJuvIraKJKbgBvzf2g62weDGD7CYggpfFReis4J0ey9EGFIiEQQojFSlA9kSIiqd0KkFjKsewgRbStEN19mxUPTtmW9HQ/h6tyqNQ8NlSMZdzyE6qkoE0trVYGFm0n1WYeBhduzwbwBC7voS+vIxfeMjeaiLxsMMtQNwMPtuew+DjzcTHk8YMfDknEcIUOtf2lVfgVH3K4Xv5PRYAXRVMtItIJ3rfaCIVn9DsTH2NxisAVRex2Hh3hX+/mRUR08bAwPEYsI51ZxWH4Q0SpicQRXeyEaIug48FEdegARfMz/tADVsRciwTAxW308ehmC2gLraC+YCbV3QoTZexa+zegAEW5PhhgYfmbvJgcRqngGByOSXdFJcLk2JeDPEN0kxe1JhIt5FiFA+w+ItMELsUyPF2IaJ4aILqb4FbxPwhImwj6JauKgDUCYaxmYIsd4KXdMjIC9ItB5Bn4BNRwsG0XM2nwAAAAASUVORK5CYII=) 2x);
567 width: 112px;
568}
569
570.error-code {
571 display: block;
572 font-size: .8em;
573}
574
575#content-top {
576 margin: 20px;
577}
578
579#help-box-inner {
580 background-color: #f9f9f9;
581 border-top: 1px solid #EEE;
582 color: #444;
583 padding: 20px;
584 text-align: start;
585}
586
587.hidden {
588 display: none;
589}
590
591#suggestion {
592 margin-top: 15px;
593}
594
595#suggestions-list p {
596 -webkit-margin-after: 0;
597}
598
599#suggestions-list ul {
600 margin-top: 0;
601}
602
603.single-suggestion {
604 list-style-type: none;
605 padding-left: 0;
606}
607
608#short-suggestion {
609 margin-top: 5px;
610}
611
612#sub-frame-error-details {
613
614 color: #8F8F8F;
615
616 /* Not done on mobile for performance reasons. */
617 text-shadow: 0 1px 0 rgba(255,255,255,0.3);
618
619}
620
621[jscontent=hostName],
622[jscontent=failedUrl] {
623 overflow-wrap: break-word;
624}
625
626#search-container {
627 /* Prevents a space between controls. */
628 display: flex;
629 margin-top: 20px;
630}
631
632#search-box {
633 border: 1px solid #cdcdcd;
634 flex-grow: 1;
635 font-size: 1em;
636 height: 26px;
637 margin-right: 0;
638 padding: 1px 9px;
639}
640
641#search-box:focus {
642 border: 1px solid rgb(93, 154, 255);
643 outline: none;
644}
645
646#search-button {
647 border: none;
648 border-bottom-left-radius: 0;
649 border-top-left-radius: 0;
650 box-shadow: none;
651 display: flex;
652 height: 30px;
653 margin: 0;
654 padding: 0;
655 width: 60px;
656}
657
658#search-image {
659 content:
660 -webkit-image-set(
661 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAPCAQAAAB+HTb/AAAArElEQVR4Xn3NsUoCUBzG0XvB3U0chR4geo5qihpt6gkCx0bXFsMERWj2KWqIanAvmlUUoQapwU6g4l8H5bd9Z/iSPS0hu/RqZqrncBuzLl7U3Rn4cSpQFTeroejJl1Lgs7f4ceDPdeBMXYp86gaONYJkY83AnqHiGk9wHnjk16PKgo5N9BUCkzPf5j6M0PfuVg5MymoetFwoaKAlB26WdXAvJ7u5mezitqtkT//7Sv/u96CaLQAAAABJRU5ErkJggg==) 1x,
662 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAeCAQAAACVzLYUAAABYElEQVR4Xr3VMUuVURzH8XO98jgkGikENkRD0KRGDUVDQy0h2SiC4IuIiktL4AvQt1CDBJUJwo1KXXS6cWdHw7tcjWwoC5Hrx+UZgnNO5CXiO/75jD/+QZf9MzjskVU7DrU1zRv9G9ir5hsA4Nii83+GA9ZI1nI1D6tWAE1TRlQMuuuFDthzMQefgo4nKr+f3dIGDdUUHPYD1ISoMQdgJgUfgqaKEOcxWE/BVTArJBvwC0cGY7gNLgiZNsD1GP4EPVn4EtyLYRuczcJ34HYMP4E7GdajDS7FcB48z8AJ8FmI4TjouBkzZ2yBuRQMlsButIZ+dfDVUBqOaIHvavpLVHXfFmAqv45r9gEHNr3y3hcAfLSgSMPgiiZR+6Z9AMuKNAwqpjUcA2h55pxgAfBWkYRlQ254YMJloaxPHbCkiGCymL5RlLA7GnRDXyuC7uhicLoKdRyaDE5Pl00K//93nABqPgBDK8sfWgAAAABJRU5ErkJggg==) 2x);
663 margin: auto;
664}
665
666.secondary-button {
667 -webkit-margin-end: 16px;
668 background: #d9d9d9;
669 color: #696969;
670}
671
672.snackbar {
673 background: #323232;
674 border-radius: 2px;
675 bottom: 24px;
676 box-sizing: border-box;
677 color: #fff;
678 font-size: .87em;
679 left: 24px;
680 max-width: 568px;
681 min-width: 288px;
682 opacity: 0;
683 padding: 16px 24px 12px;
684 position: fixed;
685 transform: translateY(90px);
686 will-change: opacity, transform;
687 z-index: 999;
688}
689
690.snackbar-show {
691 -webkit-animation:
692 show-snackbar .25s cubic-bezier(0.0, 0.0, 0.2, 1) forwards,
693 hide-snackbar .25s cubic-bezier(0.4, 0.0, 1, 1) forwards 5s;
694}
695
696@-webkit-keyframes show-snackbar {
697 100% {
698 opacity: 1;
699 transform: translateY(0);
700 }
701}
702
703@-webkit-keyframes hide-snackbar {
704 0% {
705 opacity: 1;
706 transform: translateY(0);
707 }
708 100% {
709 opacity: 0;
710 transform: translateY(90px);
711 }
712}
713
714.suggestions {
715 margin-top: 18px;
716}
717
718.suggestion-header {
719 font-weight: bold;
720 margin-bottom: 4px;
721}
722
723.suggestion-body {
724 color: #777;
725}
726
727/* Increase line height at higher resolutions. */
728@media (min-width: 641px) and (min-height: 641px) {
729 #help-box-inner {
730 line-height: 18px;
731 }
732}
733
734/* Decrease padding at low sizes. */
735@media (max-width: 640px), (max-height: 640px) {
736 h1 {
737 margin: 0 0 15px;
738 }
739 #content-top {
740 margin: 15px;
741 }
742 #help-box-inner {
743 padding: 20px;
744 }
745 .suggestions {
746 margin-top: 10px;
747 }
748 .suggestion-header {
749 margin-bottom: 0;
750 }
751}
752
753/* Don't allow overflow when in a subframe. */
754html[subframe] body {
755 overflow: hidden;
756}
757
758#sub-frame-error {
759 -webkit-align-items: center;
760 background-color: #DDD;
761 display: -webkit-flex;
762 -webkit-flex-flow: column;
763 height: 100%;
764 -webkit-justify-content: center;
765 left: 0;
766 position: absolute;
767 text-align: center;
768 top: 0;
769 transition: background-color .2s ease-in-out;
770 width: 100%;
771}
772
773#sub-frame-error:hover {
774 background-color: #EEE;
775}
776
777#sub-frame-error .icon-generic {
778 margin: 0 0 16px;
779}
780
781#sub-frame-error-details {
782 margin: 0 10px;
783 text-align: center;
784 visibility: hidden;
785}
786
787/* Show details only when hovering. */
788#sub-frame-error:hover #sub-frame-error-details {
789 visibility: visible;
790}
791
792/* If the iframe is too small, always hide the error code. */
793/* TODO(mmenke): See if overflow: no-display works better, once supported. */
794@media (max-width: 200px), (max-height: 95px) {
795 #sub-frame-error-details {
796 display: none;
797 }
798}
799
800/* Adjust icon for small embedded frames in apps. */
801@media (max-height: 100px) {
802 #sub-frame-error .icon-generic {
803 height: auto;
804 margin: 0;
805 padding-top: 0;
806 width: 25px;
807 }
808}
809
810/* details-button is special; it's a <button> element that looks like a link. */
811#details-button {
812 box-shadow: none;
813 min-width: 0;
814}
815
816/* Styles for platform dependent separation of controls and details button. */
817.suggested-left > #control-buttons,
818.suggested-left #stale-load-button,
819.suggested-right > #details-button {
820 float: left;
821}
822
823.suggested-right > #control-buttons,
824.suggested-right #stale-load-button,
825.suggested-left > #details-button {
826 float: right;
827}
828
829.suggested-left .secondary-button {
830 -webkit-margin-end: 0px;
831 -webkit-margin-start: 16px;
832}
833
834#details-button.singular {
835 float: none;
836}
837
838/* download-button shows both icon and text. */
839#download-button {
840 box-shadow: none;
841 position: relative;
842}
843
844#download-button:before {
845 -webkit-margin-end: 4px;
846 background: -webkit-image-set(
847 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAAO0lEQVQ4y2NgGArgPxIY1YChsOE/LtBAmpYG0mxpIOSDBpKUo2lpIDZxNJCkHKqlYZAla3RAHQ1DFgAARRroHyLNTwwAAAAASUVORK5CYII=) 1x,
848 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAAAZElEQVRYw+3Ruw3AMAwDUY3OzZUmRRD4E9iim9wNwAdbEURHyk4AAAAATiCVK8lLyPsKeT9K3lsownnunfkPxO78hKiYHxBV8x2icr5BVM+/CMf8g3DN34Rzns6ViwHUAUQ/6wIAd5Km7l6c8AAAAABJRU5ErkJggg==) 2x)
849 no-repeat;
850 content: '';
851 display: inline-block;
852 width: 24px;
853 height: 24px;
854 vertical-align: middle;
855}
856
857#download-button:disabled {
858 background: rgb(180, 206, 249);
859 color: rgb(255, 255, 255);
860}
861
862#buttons::after {
863 clear: both;
864 content: '';
865 display: block;
866 width: 100%;
867}
868
869/* Offline page */
870.offline {
871 transition: -webkit-filter 1.5s cubic-bezier(0.65, 0.05, 0.36, 1),
872 background-color 1.5s cubic-bezier(0.65, 0.05, 0.36, 1);
873 will-change: -webkit-filter, background-color;
874}
875
876.offline #main-message > p {
877 display: none;
878}
879
880.offline.inverted {
881 -webkit-filter: invert(100%);
882 background-color: #000;
883}
884
885.offline .interstitial-wrapper {
886 color: #2b2b2b;
887 font-size: 1em;
888 line-height: 1.55;
889 margin: 0 auto;
890 max-width: 600px;
891 padding-top: 100px;
892 width: 100%;
893}
894
895.offline .runner-container {
896 direction: ltr;
897 height: 150px;
898 max-width: 600px;
899 overflow: hidden;
900 position: absolute;
901 top: 35px;
902 width: 44px;
903}
904
905.offline .runner-canvas {
906 height: 150px;
907 max-width: 600px;
908 opacity: 1;
909 overflow: hidden;
910 position: absolute;
911 top: 0;
912 z-index: 2;
913}
914
915.offline .controller {
916 background: rgba(247,247,247, .1);
917 height: 100vh;
918 left: 0;
919 position: absolute;
920 top: 0;
921 width: 100vw;
922 z-index: 1;
923}
924
925#offline-resources {
926 display: none;
927}
928
929@media (max-width: 420px) {
930 .suggested-left > #control-buttons,
931 .suggested-right > #control-buttons {
932 float: none;
933 }
934
935 .snackbar {
936 left: 0;
937 bottom: 0;
938 width: 100%;
939 border-radius: 0;
940 }
941}
942
943@media (max-height: 350px) {
944 h1 {
945 margin: 0 0 15px;
946 }
947
948 .icon-offline {
949 margin: 0 0 10px;
950 }
951
952 .interstitial-wrapper {
953 margin-top: 5%;
954 }
955
956 .nav-wrapper {
957 margin-top: 30px;
958 }
959}
960
961@media (min-width: 420px) and (max-width: 736px) and
962 (min-height: 240px) and (max-height: 420px) and
963 (orientation:landscape) {
964 .interstitial-wrapper {
965 margin-bottom: 100px;
966 }
967}
968
969@media (max-width: 360px) and (max-height: 480px) {
970 .offline .interstitial-wrapper {
971 padding-top: 60px;
972 }
973
974 .offline .runner-container {
975 top: 8px;
976 }
977}
978
979@media (min-height: 240px) and (orientation: landscape) {
980 .offline .interstitial-wrapper {
981 margin-bottom: 90px;
982 }
983
984 .icon-offline {
985 margin-bottom: 20px;
986 }
987}
988
989@media (max-height: 320px) and (orientation: landscape) {
990 .icon-offline {
991 margin-bottom: 0;
992 }
993
994 .offline .runner-container {
995 top: 10px;
996 }
997}
998
999@media (max-width: 240px) {
1000 button {
1001 padding-left: 12px;
1002 padding-right: 12px;
1003 }
1004
1005 .interstitial-wrapper {
1006 overflow: inherit;
1007 padding: 0 8px;
1008 }
1009}
1010
1011@media (max-width: 120px) {
1012 button {
1013 width: auto;
1014 }
1015}
1016
1017.arcade-mode,
1018.arcade-mode .runner-container,
1019.arcade-mode .runner-canvas {
1020 image-rendering: pixelated;
1021 max-width: 100%;
1022 overflow: hidden;
1023}
1024
1025.arcade-mode #buttons,
1026.arcade-mode #main-content {
1027 opacity: 0;
1028 overflow: hidden;
1029}
1030
1031.arcade-mode .interstitial-wrapper {
1032 height: 100vh;
1033 max-width: 100%;
1034 overflow: hidden;
1035}
1036
1037.arcade-mode .runner-container {
1038 left: 0;
1039 margin: auto;
1040 right: 0;
1041 transform-origin: top center;
1042 transition: transform 250ms cubic-bezier(0.4, 0.0, 1, 1) .4s;
1043 z-index: 2;
1044}
1045</style>
1046 <script>// Copyright 2017 The Chromium Authors. All rights reserved.
1047// Use of this source code is governed by a BSD-style license that can be
1048// found in the LICENSE file.
1049
1050// This is the shared code for security interstitials. It is used for both SSL
1051// interstitials and Safe Browsing interstitials.
1052
1053// Should match security_interstitials::SecurityInterstitialCommand
1054/** @enum| {string} */
1055var SecurityInterstitialCommandId = {
1056 CMD_DONT_PROCEED: 0,
1057 CMD_PROCEED: 1,
1058 // Ways for user to get more information
1059 CMD_SHOW_MORE_SECTION: 2,
1060 CMD_OPEN_HELP_CENTER: 3,
1061 CMD_OPEN_DIAGNOSTIC: 4,
1062 // Primary button actions
1063 CMD_RELOAD: 5,
1064 CMD_OPEN_DATE_SETTINGS: 6,
1065 CMD_OPEN_LOGIN: 7,
1066 // Safe Browsing Extended Reporting
1067 CMD_DO_REPORT: 8,
1068 CMD_DONT_REPORT: 9,
1069 CMD_OPEN_REPORTING_PRIVACY: 10,
1070 CMD_OPEN_WHITEPAPER: 11,
1071 // Report a phishing error.
1072 CMD_REPORT_PHISHING_ERROR: 12
1073};
1074
1075var HIDDEN_CLASS = 'hidden';
1076
1077/**
1078 * A convenience method for sending commands to the parent page.
1079 * @param {string} cmd The command to send.
1080 */
1081function sendCommand(cmd) {
1082 if (window.certificateErrorPageController) {
1083 switch (cmd) {
1084 case SecurityInterstitialCommandId.CMD_DONT_PROCEED:
1085 certificateErrorPageController.dontProceed();
1086 break;
1087 case SecurityInterstitialCommandId.CMD_PROCEED:
1088 certificateErrorPageController.proceed();
1089 break;
1090 case SecurityInterstitialCommandId.CMD_SHOW_MORE_SECTION:
1091 certificateErrorPageController.showMoreSection();
1092 break;
1093 case SecurityInterstitialCommandId.CMD_OPEN_HELP_CENTER:
1094 certificateErrorPageController.openHelpCenter();
1095 break;
1096 case SecurityInterstitialCommandId.CMD_OPEN_DIAGNOSTIC:
1097 certificateErrorPageController.openDiagnostic();
1098 break;
1099 case SecurityInterstitialCommandId.CMD_RELOAD:
1100 certificateErrorPageController.reload();
1101 break;
1102 case SecurityInterstitialCommandId.CMD_OPEN_DATE_SETTINGS:
1103 certificateErrorPageController.openDateSettings();
1104 break;
1105 case SecurityInterstitialCommandId.CMD_OPEN_LOGIN:
1106 certificateErrorPageController.openLogin();
1107 break;
1108 case SecurityInterstitialCommandId.CMD_DO_REPORT:
1109 certificateErrorPageController.doReport();
1110 break;
1111 case SecurityInterstitialCommandId.CMD_DONT_REPORT:
1112 certificateErrorPageController.dontReport();
1113 break;
1114 case SecurityInterstitialCommandId.CMD_OPEN_REPORTING_PRIVACY:
1115 certificateErrorPageController.openReportingPrivacy();
1116 break;
1117 case SecurityInterstitialCommandId.CMD_OPEN_WHITEPAPER:
1118 certificateErrorPageController.openWhitepaper();
1119 break;
1120 case SecurityInterstitialCommandId.CMD_REPORT_PHISHING_ERROR:
1121 certificateErrorPageController.reportPhishingError();
1122 break;
1123 }
1124 return;
1125 }
1126//
1127 window.domAutomationController.send(cmd);
1128//
1129//
1130}
1131
1132/**
1133 * Call this to stop clicks on <a href="#"> links from scrolling to the top of
1134 * the page (and possibly showing a # in the link).
1135 */
1136function preventDefaultOnPoundLinkClicks() {
1137 document.addEventListener('click', function(e) {
1138 var anchor = findAncestor(/** @type {Node} */ (e.target), function(el) {
1139 return el.tagName == 'A';
1140 });
1141 // Use getAttribute() to prevent URL normalization.
1142 if (anchor && anchor.getAttribute('href') == '#')
1143 e.preventDefault();
1144 });
1145}
1146</script>
1147 <script>// Copyright 2015 The Chromium Authors. All rights reserved.
1148// Use of this source code is governed by a BSD-style license that can be
1149// found in the LICENSE file.
1150
1151var mobileNav = false;
1152
1153/**
1154 * For small screen mobile the navigation buttons are moved
1155 * below the advanced text.
1156 */
1157function onResize() {
1158 var helpOuterBox = document.querySelector('#details');
1159 var mainContent = document.querySelector('#main-content');
1160 var mediaQuery = '(min-width: 240px) and (max-width: 420px) and ' +
1161 '(min-height: 401px), ' +
1162 '(max-height: 560px) and (min-height: 240px) and ' +
1163 '(min-width: 421px)';
1164
1165 var detailsHidden = helpOuterBox.classList.contains(HIDDEN_CLASS);
1166 var runnerContainer = document.querySelector('.runner-container');
1167
1168 // Check for change in nav status.
1169 if (mobileNav != window.matchMedia(mediaQuery).matches) {
1170 mobileNav = !mobileNav;
1171
1172 // Handle showing the top content / details sections according to state.
1173 if (mobileNav) {
1174 mainContent.classList.toggle(HIDDEN_CLASS, !detailsHidden);
1175 helpOuterBox.classList.toggle(HIDDEN_CLASS, detailsHidden);
1176 if (runnerContainer) {
1177 runnerContainer.classList.toggle(HIDDEN_CLASS, !detailsHidden);
1178 }
1179 } else if (!detailsHidden) {
1180 // Non mobile nav with visible details.
1181 mainContent.classList.remove(HIDDEN_CLASS);
1182 helpOuterBox.classList.remove(HIDDEN_CLASS);
1183 if (runnerContainer) {
1184 runnerContainer.classList.remove(HIDDEN_CLASS);
1185 }
1186 }
1187 }
1188}
1189
1190function setupMobileNav() {
1191 window.addEventListener('resize', onResize);
1192 onResize();
1193}
1194
1195document.addEventListener('DOMContentLoaded', setupMobileNav);
1196</script>
1197 <script>// Copyright 2013 The Chromium Authors. All rights reserved.
1198// Use of this source code is governed by a BSD-style license that can be
1199// found in the LICENSE file.
1200
1201function toggleHelpBox() {
1202 var helpBoxOuter = document.getElementById('details');
1203 helpBoxOuter.classList.toggle(HIDDEN_CLASS);
1204 var detailsButton = document.getElementById('details-button');
1205 if (helpBoxOuter.classList.contains(HIDDEN_CLASS))
1206 detailsButton.innerText = detailsButton.detailsText;
1207 else
1208 detailsButton.innerText = detailsButton.hideDetailsText;
1209
1210 // Details appears over the main content on small screens.
1211 if (mobileNav) {
1212 document.getElementById('main-content').classList.toggle(HIDDEN_CLASS);
1213 var runnerContainer = document.querySelector('.runner-container');
1214 if (runnerContainer) {
1215 runnerContainer.classList.toggle(HIDDEN_CLASS);
1216 }
1217 }
1218}
1219
1220function diagnoseErrors() {
1221//
1222 if (window.errorPageController)
1223 errorPageController.diagnoseErrorsButtonClick();
1224//
1225//
1226}
1227
1228// Subframes use a different layout but the same html file. This is to make it
1229// easier to support platforms that load the error page via different
1230// mechanisms (Currently just iOS).
1231if (window.top.location != window.location)
1232 document.documentElement.setAttribute('subframe', '');
1233
1234// Re-renders the error page using |strings| as the dictionary of values.
1235// Used by NetErrorTabHelper to update DNS error pages with probe results.
1236function updateForDnsProbe(strings) {
1237 var context = new JsEvalContext(strings);
1238 jstProcess(context, document.getElementById('t'));
1239}
1240
1241// Given the classList property of an element, adds an icon class to the list
1242// and removes the previously-
1243function updateIconClass(classList, newClass) {
1244 var oldClass;
1245
1246 if (classList.hasOwnProperty('last_icon_class')) {
1247 oldClass = classList['last_icon_class'];
1248 if (oldClass == newClass)
1249 return;
1250 }
1251
1252 classList.add(newClass);
1253 if (oldClass !== undefined)
1254 classList.remove(oldClass);
1255
1256 classList['last_icon_class'] = newClass;
1257
1258 if (newClass == 'icon-offline') {
1259 document.body.classList.add('offline');
1260 new Runner('.interstitial-wrapper');
1261 } else {
1262 document.body.classList.add('neterror');
1263 }
1264}
1265
1266// Does a search using |baseSearchUrl| and the text in the search box.
1267function search(baseSearchUrl) {
1268 var searchTextNode = document.getElementById('search-box');
1269 document.location = baseSearchUrl + searchTextNode.value;
1270 return false;
1271}
1272
1273// Use to track clicks on elements generated by the navigation correction
1274// service. If |trackingId| is negative, the element does not come from the
1275// correction service.
1276function trackClick(trackingId) {
1277 // This can't be done with XHRs because XHRs are cancelled on navigation
1278 // start, and because these are cross-site requests.
1279 if (trackingId >= 0 && errorPageController)
1280 errorPageController.trackClick(trackingId);
1281}
1282
1283// Called when an <a> tag generated by the navigation correction service is
1284// clicked. Separate function from trackClick so the resources don't have to
1285// be updated if new data is added to jstdata.
1286function linkClicked(jstdata) {
1287 trackClick(jstdata.trackingId);
1288}
1289
1290// Implements button clicks. This function is needed during the transition
1291// between implementing these in trunk chromium and implementing them in
1292// iOS.
1293function reloadButtonClick(url) {
1294 if (window.errorPageController) {
1295 errorPageController.reloadButtonClick();
1296 } else {
1297 location = url;
1298 }
1299}
1300
1301function showSavedCopyButtonClick() {
1302 if (window.errorPageController) {
1303 errorPageController.showSavedCopyButtonClick();
1304 }
1305}
1306
1307function downloadButtonClick() {
1308 if (window.errorPageController) {
1309 errorPageController.downloadButtonClick();
1310 var downloadButton = document.getElementById('download-button');
1311 downloadButton.disabled = true;
1312 downloadButton.textContent = downloadButton.disabledText;
1313 }
1314}
1315
1316function detailsButtonClick() {
1317 if (window.errorPageController)
1318 errorPageController.detailsButtonClick();
1319}
1320
1321/**
1322 * Replace the reload button with the Google cached copy suggestion.
1323 */
1324function setUpCachedButton(buttonStrings) {
1325 var reloadButton = document.getElementById('reload-button');
1326
1327 reloadButton.textContent = buttonStrings.msg;
1328 var url = buttonStrings.cacheUrl;
1329 var trackingId = buttonStrings.trackingId;
1330 reloadButton.onclick = function(e) {
1331 e.preventDefault();
1332 trackClick(trackingId);
1333 if (window.errorPageController) {
1334 errorPageController.trackCachedCopyButtonClick();
1335 }
1336 location = url;
1337 };
1338 reloadButton.style.display = '';
1339 document.getElementById('control-buttons').hidden = false;
1340}
1341
1342var primaryControlOnLeft = true;
1343//
1344primaryControlOnLeft = false;
1345//
1346
1347function onDocumentLoad() {
1348 var controlButtonDiv = document.getElementById('control-buttons');
1349 var reloadButton = document.getElementById('reload-button');
1350 var detailsButton = document.getElementById('details-button');
1351 var showSavedCopyButton = document.getElementById('show-saved-copy-button');
1352 var downloadButton = document.getElementById('download-button');
1353
1354 var reloadButtonVisible = loadTimeData.valueExists('reloadButton') &&
1355 loadTimeData.getValue('reloadButton').msg;
1356 var showSavedCopyButtonVisible =
1357 loadTimeData.valueExists('showSavedCopyButton') &&
1358 loadTimeData.getValue('showSavedCopyButton').msg;
1359 var downloadButtonVisible =
1360 loadTimeData.valueExists('downloadButton') &&
1361 loadTimeData.getValue('downloadButton').msg;
1362
1363 var primaryButton, secondaryButton;
1364 if (showSavedCopyButton.primary) {
1365 primaryButton = showSavedCopyButton;
1366 secondaryButton = reloadButton;
1367 } else {
1368 primaryButton = reloadButton;
1369 secondaryButton = showSavedCopyButton;
1370 }
1371
1372 // Sets up the proper button layout for the current platform.
1373 if (primaryControlOnLeft) {
1374 buttons.classList.add('suggested-left');
1375 controlButtonDiv.insertBefore(secondaryButton, primaryButton);
1376 } else {
1377 buttons.classList.add('suggested-right');
1378 controlButtonDiv.insertBefore(primaryButton, secondaryButton);
1379 }
1380
1381 // Check for Google cached copy suggestion.
1382 if (loadTimeData.valueExists('cacheButton')) {
1383 setUpCachedButton(loadTimeData.getValue('cacheButton'));
1384 }
1385
1386 if (reloadButton.style.display == 'none' &&
1387 showSavedCopyButton.style.display == 'none' &&
1388 downloadButton.style.display == 'none') {
1389 detailsButton.classList.add('singular');
1390 }
1391
1392 // Show control buttons.
1393 if (reloadButtonVisible || showSavedCopyButtonVisible ||
1394 downloadButtonVisible) {
1395 controlButtonDiv.hidden = false;
1396
1397 // Set the secondary button state in the cases of two call to actions.
1398 if ((reloadButtonVisible || downloadButtonVisible) &&
1399 showSavedCopyButtonVisible) {
1400 secondaryButton.classList.add('secondary-button');
1401 }
1402 }
1403}
1404
1405document.addEventListener('DOMContentLoaded', onDocumentLoad);
1406</script>
1407 <script>// Copyright (c) 2014 The Chromium Authors. All rights reserved.
1408// Use of this source code is governed by a BSD-style license that can be
1409// found in the LICENSE file.
1410(function() {
1411'use strict';
1412/**
1413 * T-Rex runner.
1414 * @param {string} outerContainerId Outer containing element id.
1415 * @param {Object} opt_config
1416 * @constructor
1417 * @export
1418 */
1419function Runner(outerContainerId, opt_config) {
1420 // Singleton
1421 if (Runner.instance_) {
1422 return Runner.instance_;
1423 }
1424 Runner.instance_ = this;
1425
1426 this.outerContainerEl = document.querySelector(outerContainerId);
1427 this.containerEl = null;
1428 this.snackbarEl = null;
1429
1430 this.config = opt_config || Runner.config;
1431 // Logical dimensions of the container.
1432 this.dimensions = Runner.defaultDimensions;
1433
1434 this.canvas = null;
1435 this.canvasCtx = null;
1436
1437 this.tRex = null;
1438
1439 this.distanceMeter = null;
1440 this.distanceRan = 0;
1441
1442 this.highestScore = 0;
1443
1444 this.time = 0;
1445 this.runningTime = 0;
1446 this.msPerFrame = 1000 / FPS;
1447 this.currentSpeed = this.config.SPEED;
1448
1449 this.obstacles = [];
1450
1451 this.activated = false; // Whether the easter egg has been activated.
1452 this.playing = false; // Whether the game is currently in play state.
1453 this.crashed = false;
1454 this.paused = false;
1455 this.inverted = false;
1456 this.invertTimer = 0;
1457 this.resizeTimerId_ = null;
1458
1459 this.playCount = 0;
1460
1461 // Sound FX.
1462 this.audioBuffer = null;
1463 this.soundFx = {};
1464
1465 // Global web audio context for playing sounds.
1466 this.audioContext = null;
1467
1468 // Images.
1469 this.images = {};
1470 this.imagesLoaded = 0;
1471
1472 if (this.isDisabled()) {
1473 this.setupDisabledRunner();
1474 } else {
1475 this.loadImages();
1476 }
1477}
1478window['Runner'] = Runner;
1479
1480
1481/**
1482 * Default game width.
1483 * @const
1484 */
1485var DEFAULT_WIDTH = 600;
1486
1487/**
1488 * Frames per second.
1489 * @const
1490 */
1491var FPS = 60;
1492
1493/** @const */
1494var IS_HIDPI = window.devicePixelRatio > 1;
1495
1496/** @const */
1497var IS_IOS = /iPad|iPhone|iPod/.test(window.navigator.platform);
1498
1499/** @const */
1500var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS;
1501
1502/** @const */
1503var IS_TOUCH_ENABLED = 'ontouchstart' in window;
1504
1505/** @const */
1506var ARCADE_MODE_URL = 'chrome://dino/';
1507
1508/**
1509 * Default game configuration.
1510 * @enum {number}
1511 */
1512Runner.config = {
1513 ACCELERATION: 0.001,
1514 BG_CLOUD_SPEED: 0.2,
1515 BOTTOM_PAD: 10,
1516 CLEAR_TIME: 3000,
1517 CLOUD_FREQUENCY: 0.5,
1518 GAMEOVER_CLEAR_TIME: 750,
1519 GAP_COEFFICIENT: 0.6,
1520 GRAVITY: 0.6,
1521 INITIAL_JUMP_VELOCITY: 12,
1522 INVERT_FADE_DURATION: 12000,
1523 INVERT_DISTANCE: 700,
1524 MAX_BLINK_COUNT: 3,
1525 MAX_CLOUDS: 6,
1526 MAX_OBSTACLE_LENGTH: 3,
1527 MAX_OBSTACLE_DUPLICATION: 2,
1528 MAX_SPEED: 13,
1529 MIN_JUMP_HEIGHT: 35,
1530 MOBILE_SPEED_COEFFICIENT: 1.2,
1531 RESOURCE_TEMPLATE_ID: 'audio-resources',
1532 SPEED: 6,
1533 SPEED_DROP_COEFFICIENT: 3,
1534 ARCADE_MODE_INITIAL_TOP_POSITION: 35,
1535 ARCADE_MODE_TOP_POSITION_PERCENT: 0.1
1536};
1537
1538
1539/**
1540 * Default dimensions.
1541 * @enum {string}
1542 */
1543Runner.defaultDimensions = {
1544 WIDTH: DEFAULT_WIDTH,
1545 HEIGHT: 150
1546};
1547
1548
1549/**
1550 * CSS class names.
1551 * @enum {string}
1552 */
1553Runner.classes = {
1554 ARCADE_MODE: 'arcade-mode',
1555 CANVAS: 'runner-canvas',
1556 CONTAINER: 'runner-container',
1557 CRASHED: 'crashed',
1558 ICON: 'icon-offline',
1559 INVERTED: 'inverted',
1560 SNACKBAR: 'snackbar',
1561 SNACKBAR_SHOW: 'snackbar-show',
1562 TOUCH_CONTROLLER: 'controller'
1563};
1564
1565
1566/**
1567 * Sprite definition layout of the spritesheet.
1568 * @enum {Object}
1569 */
1570Runner.spriteDefinition = {
1571 LDPI: {
1572 CACTUS_LARGE: {x: 332, y: 2},
1573 CACTUS_SMALL: {x: 228, y: 2},
1574 CLOUD: {x: 86, y: 2},
1575 HORIZON: {x: 2, y: 54},
1576 MOON: {x: 484, y: 2},
1577 PTERODACTYL: {x: 134, y: 2},
1578 RESTART: {x: 2, y: 2},
1579 TEXT_SPRITE: {x: 655, y: 2},
1580 TREX: {x: 848, y: 2},
1581 STAR: {x: 645, y: 2}
1582 },
1583 HDPI: {
1584 CACTUS_LARGE: {x: 652, y: 2},
1585 CACTUS_SMALL: {x: 446, y: 2},
1586 CLOUD: {x: 166, y: 2},
1587 HORIZON: {x: 2, y: 104},
1588 MOON: {x: 954, y: 2},
1589 PTERODACTYL: {x: 260, y: 2},
1590 RESTART: {x: 2, y: 2},
1591 TEXT_SPRITE: {x: 1294, y: 2},
1592 TREX: {x: 1678, y: 2},
1593 STAR: {x: 1276, y: 2}
1594 }
1595};
1596
1597
1598/**
1599 * Sound FX. Reference to the ID of the audio tag on interstitial page.
1600 * @enum {string}
1601 */
1602Runner.sounds = {
1603 BUTTON_PRESS: 'offline-sound-press',
1604 HIT: 'offline-sound-hit',
1605 SCORE: 'offline-sound-reached'
1606};
1607
1608
1609/**
1610 * Key code mapping.
1611 * @enum {Object}
1612 */
1613Runner.keycodes = {
1614 JUMP: {'38': 1, '32': 1}, // Up, spacebar
1615 DUCK: {'40': 1}, // Down
1616 RESTART: {'13': 1} // Enter
1617};
1618
1619
1620/**
1621 * Runner event names.
1622 * @enum {string}
1623 */
1624Runner.events = {
1625 ANIM_END: 'webkitAnimationEnd',
1626 CLICK: 'click',
1627 KEYDOWN: 'keydown',
1628 KEYUP: 'keyup',
1629 MOUSEDOWN: 'mousedown',
1630 MOUSEUP: 'mouseup',
1631 RESIZE: 'resize',
1632 TOUCHEND: 'touchend',
1633 TOUCHSTART: 'touchstart',
1634 VISIBILITY: 'visibilitychange',
1635 BLUR: 'blur',
1636 FOCUS: 'focus',
1637 LOAD: 'load'
1638};
1639
1640Runner.prototype = {
1641 /**
1642 * Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
1643 * @return {boolean}
1644 */
1645 isDisabled: function() {
1646 return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
1647 },
1648
1649 /**
1650 * For disabled instances, set up a snackbar with the disabled message.
1651 */
1652 setupDisabledRunner: function() {
1653 this.containerEl = document.createElement('div');
1654 this.containerEl.className = Runner.classes.SNACKBAR;
1655 this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
1656 this.outerContainerEl.appendChild(this.containerEl);
1657
1658 // Show notification when the activation key is pressed.
1659 document.addEventListener(Runner.events.KEYDOWN, function(e) {
1660 if (Runner.keycodes.JUMP[e.keyCode]) {
1661 this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
1662 document.querySelector('.icon').classList.add('icon-disabled');
1663 }
1664 }.bind(this));
1665 },
1666
1667 /**
1668 * Setting individual settings for debugging.
1669 * @param {string} setting
1670 * @param {*} value
1671 */
1672 updateConfigSetting: function(setting, value) {
1673 if (setting in this.config && value != undefined) {
1674 this.config[setting] = value;
1675
1676 switch (setting) {
1677 case 'GRAVITY':
1678 case 'MIN_JUMP_HEIGHT':
1679 case 'SPEED_DROP_COEFFICIENT':
1680 this.tRex.config[setting] = value;
1681 break;
1682 case 'INITIAL_JUMP_VELOCITY':
1683 this.tRex.setJumpVelocity(value);
1684 break;
1685 case 'SPEED':
1686 this.setSpeed(value);
1687 break;
1688 }
1689 }
1690 },
1691
1692 /**
1693 * Cache the appropriate image sprite from the page and get the sprite sheet
1694 * definition.
1695 */
1696 loadImages: function() {
1697 if (IS_HIDPI) {
1698 Runner.imageSprite = document.getElementById('offline-resources-2x');
1699 this.spriteDef = Runner.spriteDefinition.HDPI;
1700 } else {
1701 Runner.imageSprite = document.getElementById('offline-resources-1x');
1702 this.spriteDef = Runner.spriteDefinition.LDPI;
1703 }
1704
1705 if (Runner.imageSprite.complete) {
1706 this.init();
1707 } else {
1708 // If the images are not yet loaded, add a listener.
1709 Runner.imageSprite.addEventListener(Runner.events.LOAD,
1710 this.init.bind(this));
1711 }
1712 },
1713
1714 /**
1715 * Load and decode base 64 encoded sounds.
1716 */
1717 loadSounds: function() {
1718 if (!IS_IOS) {
1719 this.audioContext = new AudioContext();
1720
1721 var resourceTemplate =
1722 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
1723
1724 for (var sound in Runner.sounds) {
1725 var soundSrc =
1726 resourceTemplate.getElementById(Runner.sounds[sound]).src;
1727 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
1728 var buffer = decodeBase64ToArrayBuffer(soundSrc);
1729
1730 // Async, so no guarantee of order in array.
1731 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
1732 this.soundFx[index] = audioData;
1733 }.bind(this, sound));
1734 }
1735 }
1736 },
1737
1738 /**
1739 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
1740 * @param {number} opt_speed
1741 */
1742 setSpeed: function(opt_speed) {
1743 var speed = opt_speed || this.currentSpeed;
1744
1745 // Reduce the speed on smaller mobile screens.
1746 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
1747 var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
1748 this.config.MOBILE_SPEED_COEFFICIENT;
1749 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
1750 } else if (opt_speed) {
1751 this.currentSpeed = opt_speed;
1752 }
1753 },
1754
1755 /**
1756 * Game initialiser.
1757 */
1758 init: function() {
1759 // Hide the static icon.
1760 document.querySelector('.' + Runner.classes.ICON).style.visibility =
1761 'hidden';
1762
1763 this.adjustDimensions();
1764 this.setSpeed();
1765
1766 this.containerEl = document.createElement('div');
1767 this.containerEl.className = Runner.classes.CONTAINER;
1768
1769 // Player canvas container.
1770 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
1771 this.dimensions.HEIGHT, Runner.classes.PLAYER);
1772
1773 this.canvasCtx = this.canvas.getContext('2d');
1774 this.canvasCtx.fillStyle = '#f7f7f7';
1775 this.canvasCtx.fill();
1776 Runner.updateCanvasScaling(this.canvas);
1777
1778 // Horizon contains clouds, obstacles and the ground.
1779 this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
1780 this.config.GAP_COEFFICIENT);
1781
1782 // Distance meter
1783 this.distanceMeter = new DistanceMeter(this.canvas,
1784 this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
1785
1786 // Draw t-rex
1787 this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
1788
1789 this.outerContainerEl.appendChild(this.containerEl);
1790
1791 if (IS_MOBILE) {
1792 this.createTouchController();
1793 }
1794
1795 this.startListening();
1796 this.update();
1797
1798 window.addEventListener(Runner.events.RESIZE,
1799 this.debounceResize.bind(this));
1800 },
1801
1802 /**
1803 * Create the touch controller. A div that covers whole screen.
1804 */
1805 createTouchController: function() {
1806 this.touchController = document.createElement('div');
1807 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
1808 },
1809
1810 /**
1811 * Debounce the resize event.
1812 */
1813 debounceResize: function() {
1814 if (!this.resizeTimerId_) {
1815 this.resizeTimerId_ =
1816 setInterval(this.adjustDimensions.bind(this), 250);
1817 }
1818 },
1819
1820 /**
1821 * Adjust game space dimensions on resize.
1822 */
1823 adjustDimensions: function() {
1824 clearInterval(this.resizeTimerId_);
1825 this.resizeTimerId_ = null;
1826
1827 var boxStyles = window.getComputedStyle(this.outerContainerEl);
1828 var padding = Number(boxStyles.paddingLeft.substr(0,
1829 boxStyles.paddingLeft.length - 2));
1830
1831 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
1832 if (this.isArcadeMode()) {
1833 this.dimensions.WIDTH = Math.min(DEFAULT_WIDTH, this.dimensions.WIDTH);
1834 if (this.activated) {
1835 this.setArcadeModeContainerScale();
1836 }
1837 }
1838
1839 // Redraw the elements back onto the canvas.
1840 if (this.canvas) {
1841 this.canvas.width = this.dimensions.WIDTH;
1842 this.canvas.height = this.dimensions.HEIGHT;
1843
1844 Runner.updateCanvasScaling(this.canvas);
1845
1846 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
1847 this.clearCanvas();
1848 this.horizon.update(0, 0, true);
1849 this.tRex.update(0);
1850
1851 // Outer container and distance meter.
1852 if (this.playing || this.crashed || this.paused) {
1853 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
1854 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
1855 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
1856 this.stop();
1857 } else {
1858 this.tRex.draw(0, 0);
1859 }
1860
1861 // Game over panel.
1862 if (this.crashed && this.gameOverPanel) {
1863 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
1864 this.gameOverPanel.draw();
1865 }
1866 }
1867 },
1868
1869 /**
1870 * Play the game intro.
1871 * Canvas container width expands out to the full width.
1872 */
1873 playIntro: function() {
1874 if (!this.activated && !this.crashed) {
1875 this.playingIntro = true;
1876 this.tRex.playingIntro = true;
1877
1878 // CSS animation definition.
1879 var keyframes = '@-webkit-keyframes intro { ' +
1880 'from { width:' + Trex.config.WIDTH + 'px }' +
1881 'to { width: ' + this.dimensions.WIDTH + 'px }' +
1882 '}';
1883 document.styleSheets[0].insertRule(keyframes, 0);
1884
1885 this.containerEl.addEventListener(Runner.events.ANIM_END,
1886 this.startGame.bind(this));
1887
1888 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
1889 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
1890
1891 if (this.touchController) {
1892 this.outerContainerEl.appendChild(this.touchController);
1893 }
1894 this.playing = true;
1895 this.activated = true;
1896 } else if (this.crashed) {
1897 this.restart();
1898 }
1899 },
1900
1901
1902 /**
1903 * Update the game status to started.
1904 */
1905 startGame: function() {
1906 if (this.isArcadeMode()) {
1907 this.setArcadeMode();
1908 }
1909 this.runningTime = 0;
1910 this.playingIntro = false;
1911 this.tRex.playingIntro = false;
1912 this.containerEl.style.webkitAnimation = '';
1913 this.playCount++;
1914
1915 // Handle tabbing off the page. Pause the current game.
1916 document.addEventListener(Runner.events.VISIBILITY,
1917 this.onVisibilityChange.bind(this));
1918
1919 window.addEventListener(Runner.events.BLUR,
1920 this.onVisibilityChange.bind(this));
1921
1922 window.addEventListener(Runner.events.FOCUS,
1923 this.onVisibilityChange.bind(this));
1924 },
1925
1926 clearCanvas: function() {
1927 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
1928 this.dimensions.HEIGHT);
1929 },
1930
1931 /**
1932 * Update the game frame and schedules the next one.
1933 */
1934 update: function() {
1935 this.updatePending = false;
1936
1937 var now = getTimeStamp();
1938 var deltaTime = now - (this.time || now);
1939 this.time = now;
1940
1941 if (this.playing) {
1942 this.clearCanvas();
1943
1944 if (this.tRex.jumping) {
1945 this.tRex.updateJump(deltaTime);
1946 }
1947
1948 this.runningTime += deltaTime;
1949 var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
1950
1951 // First jump triggers the intro.
1952 if (this.tRex.jumpCount == 1 && !this.playingIntro) {
1953 this.playIntro();
1954 }
1955
1956 // The horizon doesn't move until the intro is over.
1957 if (this.playingIntro) {
1958 this.horizon.update(0, this.currentSpeed, hasObstacles);
1959 } else {
1960 deltaTime = !this.activated ? 0 : deltaTime;
1961 this.horizon.update(deltaTime, this.currentSpeed, hasObstacles,
1962 this.inverted);
1963 }
1964
1965 // Check for collisions.
1966 var collision = hasObstacles &&
1967 checkForCollision(this.horizon.obstacles[0], this.tRex);
1968
1969 if (!collision) {
1970 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
1971
1972 if (this.currentSpeed < this.config.MAX_SPEED) {
1973 this.currentSpeed += this.config.ACCELERATION;
1974 }
1975 } else {
1976 this.gameOver();
1977 }
1978
1979 var playAchievementSound = this.distanceMeter.update(deltaTime,
1980 Math.ceil(this.distanceRan));
1981
1982 if (playAchievementSound) {
1983 this.playSound(this.soundFx.SCORE);
1984 }
1985
1986 // Night mode.
1987 if (this.invertTimer > this.config.INVERT_FADE_DURATION) {
1988 this.invertTimer = 0;
1989 this.invertTrigger = false;
1990 this.invert();
1991 } else if (this.invertTimer) {
1992 this.invertTimer += deltaTime;
1993 } else {
1994 var actualDistance =
1995 this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));
1996
1997 if (actualDistance > 0) {
1998 this.invertTrigger = !(actualDistance %
1999 this.config.INVERT_DISTANCE);
2000
2001 if (this.invertTrigger && this.invertTimer === 0) {
2002 this.invertTimer += deltaTime;
2003 this.invert();
2004 }
2005 }
2006 }
2007 }
2008
2009 if (this.playing || (!this.activated &&
2010 this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
2011 this.tRex.update(deltaTime);
2012 this.scheduleNextUpdate();
2013 }
2014 },
2015
2016 /**
2017 * Event handler.
2018 */
2019 handleEvent: function(e) {
2020 return (function(evtType, events) {
2021 switch (evtType) {
2022 case events.KEYDOWN:
2023 case events.TOUCHSTART:
2024 case events.MOUSEDOWN:
2025 this.onKeyDown(e);
2026 break;
2027 case events.KEYUP:
2028 case events.TOUCHEND:
2029 case events.MOUSEUP:
2030 this.onKeyUp(e);
2031 break;
2032 }
2033 }.bind(this))(e.type, Runner.events);
2034 },
2035
2036 /**
2037 * Bind relevant key / mouse / touch listeners.
2038 */
2039 startListening: function() {
2040 // Keys.
2041 document.addEventListener(Runner.events.KEYDOWN, this);
2042 document.addEventListener(Runner.events.KEYUP, this);
2043
2044 if (IS_MOBILE) {
2045 // Mobile only touch devices.
2046 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
2047 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
2048 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
2049 } else {
2050 // Mouse.
2051 document.addEventListener(Runner.events.MOUSEDOWN, this);
2052 document.addEventListener(Runner.events.MOUSEUP, this);
2053 }
2054 },
2055
2056 /**
2057 * Remove all listeners.
2058 */
2059 stopListening: function() {
2060 document.removeEventListener(Runner.events.KEYDOWN, this);
2061 document.removeEventListener(Runner.events.KEYUP, this);
2062
2063 if (IS_MOBILE) {
2064 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
2065 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
2066 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
2067 } else {
2068 document.removeEventListener(Runner.events.MOUSEDOWN, this);
2069 document.removeEventListener(Runner.events.MOUSEUP, this);
2070 }
2071 },
2072
2073 /**
2074 * Process keydown.
2075 * @param {Event} e
2076 */
2077 onKeyDown: function(e) {
2078 // Prevent native page scrolling whilst tapping on mobile.
2079 if (IS_MOBILE && this.playing) {
2080 e.preventDefault();
2081 }
2082
2083 if (!this.crashed && !this.paused) {
2084 if (Runner.keycodes.JUMP[e.keyCode] ||
2085 e.type == Runner.events.TOUCHSTART) {
2086 e.preventDefault();
2087 // Starting the game for the first time.
2088 if (!this.playing) {
2089 this.loadSounds();
2090 this.playing = true;
2091 this.update();
2092 if (window.errorPageController) {
2093 errorPageController.trackEasterEgg();
2094 }
2095 }
2096 // Start jump.
2097 if (!this.tRex.jumping && !this.tRex.ducking) {
2098 this.playSound(this.soundFx.BUTTON_PRESS);
2099 this.tRex.startJump(this.currentSpeed);
2100 }
2101 } else if (this.playing && Runner.keycodes.DUCK[e.keyCode]) {
2102 e.preventDefault();
2103 if (this.tRex.jumping) {
2104 // Speed drop, activated only when jump key is not pressed.
2105 this.tRex.setSpeedDrop();
2106 } else if (!this.tRex.jumping && !this.tRex.ducking) {
2107 // Duck.
2108 this.tRex.setDuck(true);
2109 }
2110 }
2111 } else if (this.crashed && e.type == Runner.events.TOUCHSTART &&
2112 e.currentTarget == this.containerEl) {
2113 this.restart();
2114 }
2115 },
2116
2117
2118 /**
2119 * Process key up.
2120 * @param {Event} e
2121 */
2122 onKeyUp: function(e) {
2123 var keyCode = String(e.keyCode);
2124 var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
2125 e.type == Runner.events.TOUCHEND ||
2126 e.type == Runner.events.MOUSEDOWN;
2127
2128 if (this.isRunning() && isjumpKey) {
2129 this.tRex.endJump();
2130 } else if (Runner.keycodes.DUCK[keyCode]) {
2131 this.tRex.speedDrop = false;
2132 this.tRex.setDuck(false);
2133 } else if (this.crashed) {
2134 // Check that enough time has elapsed before allowing jump key to restart.
2135 var deltaTime = getTimeStamp() - this.time;
2136
2137 if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
2138 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
2139 Runner.keycodes.JUMP[keyCode])) {
2140 this.restart();
2141 }
2142 } else if (this.paused && isjumpKey) {
2143 // Reset the jump state
2144 this.tRex.reset();
2145 this.play();
2146 }
2147 },
2148
2149 /**
2150 * Returns whether the event was a left click on canvas.
2151 * On Windows right click is registered as a click.
2152 * @param {Event} e
2153 * @return {boolean}
2154 */
2155 isLeftClickOnCanvas: function(e) {
2156 return e.button != null && e.button < 2 &&
2157 e.type == Runner.events.MOUSEUP && e.target == this.canvas;
2158 },
2159
2160 /**
2161 * RequestAnimationFrame wrapper.
2162 */
2163 scheduleNextUpdate: function() {
2164 if (!this.updatePending) {
2165 this.updatePending = true;
2166 this.raqId = requestAnimationFrame(this.update.bind(this));
2167 }
2168 },
2169
2170 /**
2171 * Whether the game is running.
2172 * @return {boolean}
2173 */
2174 isRunning: function() {
2175 return !!this.raqId;
2176 },
2177
2178 /**
2179 * Game over state.
2180 */
2181 gameOver: function() {
2182 this.playSound(this.soundFx.HIT);
2183 vibrate(200);
2184
2185 this.stop();
2186 this.crashed = true;
2187 this.distanceMeter.achievement = false;
2188
2189 this.tRex.update(100, Trex.status.CRASHED);
2190
2191 // Game over panel.
2192 if (!this.gameOverPanel) {
2193 this.gameOverPanel = new GameOverPanel(this.canvas,
2194 this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
2195 this.dimensions);
2196 } else {
2197 this.gameOverPanel.draw();
2198 }
2199
2200 // Update the high score.
2201 if (this.distanceRan > this.highestScore) {
2202 this.highestScore = Math.ceil(this.distanceRan);
2203 this.distanceMeter.setHighScore(this.highestScore);
2204 }
2205
2206 // Reset the time clock.
2207 this.time = getTimeStamp();
2208 },
2209
2210 stop: function() {
2211 this.playing = false;
2212 this.paused = true;
2213 cancelAnimationFrame(this.raqId);
2214 this.raqId = 0;
2215 },
2216
2217 play: function() {
2218 if (!this.crashed) {
2219 this.playing = true;
2220 this.paused = false;
2221 this.tRex.update(0, Trex.status.RUNNING);
2222 this.time = getTimeStamp();
2223 this.update();
2224 }
2225 },
2226
2227 restart: function() {
2228 if (!this.raqId) {
2229 this.playCount++;
2230 this.runningTime = 0;
2231 this.playing = true;
2232 this.paused = false;
2233 this.crashed = false;
2234 this.distanceRan = 0;
2235 this.setSpeed(this.config.SPEED);
2236 this.time = getTimeStamp();
2237 this.containerEl.classList.remove(Runner.classes.CRASHED);
2238 this.clearCanvas();
2239 this.distanceMeter.reset(this.highestScore);
2240 this.horizon.reset();
2241 this.tRex.reset();
2242 this.playSound(this.soundFx.BUTTON_PRESS);
2243 this.invert(true);
2244 this.update();
2245 }
2246 },
2247
2248 /**
2249 * Whether the game should go into arcade mode.
2250 * @return {boolean}
2251 */
2252 isArcadeMode: function() {
2253 return document.title == ARCADE_MODE_URL;
2254 },
2255
2256 /**
2257 * Hides offline messaging for a fullscreen game only experience.
2258 */
2259 setArcadeMode: function() {
2260 document.body.classList.add(Runner.classes.ARCADE_MODE);
2261 this.setArcadeModeContainerScale();
2262 },
2263
2264 /**
2265 * Sets the scaling for arcade mode.
2266 */
2267 setArcadeModeContainerScale: function() {
2268 var windowHeight = window.innerHeight;
2269 var scaleHeight = windowHeight / this.dimensions.HEIGHT;
2270 var scaleWidth = window.innerWidth / this.dimensions.WIDTH;
2271 var scale = Math.max(1, Math.min(scaleHeight, scaleWidth));
2272 var scaledCanvasHeight = this.dimensions.HEIGHT * scale;
2273 // Positions the game container at 10% of the available vertical window
2274 // height minus the game container height.
2275 var translateY = Math.ceil(Math.max(0, (windowHeight - scaledCanvasHeight -
2276 Runner.config.ARCADE_MODE_INITIAL_TOP_POSITION) *
2277 Runner.config.ARCADE_MODE_TOP_POSITION_PERCENT)) *
2278 window.devicePixelRatio;
2279 this.containerEl.style.transform = 'scale(' + scale + ') translateY(' +
2280 translateY + 'px)';
2281 },
2282
2283 /**
2284 * Pause the game if the tab is not in focus.
2285 */
2286 onVisibilityChange: function(e) {
2287 if (document.hidden || document.webkitHidden || e.type == 'blur' ||
2288 document.visibilityState != 'visible') {
2289 this.stop();
2290 } else if (!this.crashed) {
2291 this.tRex.reset();
2292 this.play();
2293 }
2294 },
2295
2296 /**
2297 * Play a sound.
2298 * @param {SoundBuffer} soundBuffer
2299 */
2300 playSound: function(soundBuffer) {
2301 if (soundBuffer) {
2302 var sourceNode = this.audioContext.createBufferSource();
2303 sourceNode.buffer = soundBuffer;
2304 sourceNode.connect(this.audioContext.destination);
2305 sourceNode.start(0);
2306 }
2307 },
2308
2309 /**
2310 * Inverts the current page / canvas colors.
2311 * @param {boolean} Whether to reset colors.
2312 */
2313 invert: function(reset) {
2314 if (reset) {
2315 document.body.classList.toggle(Runner.classes.INVERTED, false);
2316 this.invertTimer = 0;
2317 this.inverted = false;
2318 } else {
2319 this.inverted = document.body.classList.toggle(Runner.classes.INVERTED,
2320 this.invertTrigger);
2321 }
2322 }
2323};
2324
2325
2326/**
2327 * Updates the canvas size taking into
2328 * account the backing store pixel ratio and
2329 * the device pixel ratio.
2330 *
2331 * See article by Paul Lewis:
2332 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
2333 *
2334 * @param {HTMLCanvasElement} canvas
2335 * @param {number} opt_width
2336 * @param {number} opt_height
2337 * @return {boolean} Whether the canvas was scaled.
2338 */
2339Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
2340 var context = canvas.getContext('2d');
2341
2342 // Query the various pixel ratios
2343 var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
2344 var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
2345 var ratio = devicePixelRatio / backingStoreRatio;
2346
2347 // Upscale the canvas if the two ratios don't match
2348 if (devicePixelRatio !== backingStoreRatio) {
2349 var oldWidth = opt_width || canvas.width;
2350 var oldHeight = opt_height || canvas.height;
2351
2352 canvas.width = oldWidth * ratio;
2353 canvas.height = oldHeight * ratio;
2354
2355 canvas.style.width = oldWidth + 'px';
2356 canvas.style.height = oldHeight + 'px';
2357
2358 // Scale the context to counter the fact that we've manually scaled
2359 // our canvas element.
2360 context.scale(ratio, ratio);
2361 return true;
2362 } else if (devicePixelRatio == 1) {
2363 // Reset the canvas width / height. Fixes scaling bug when the page is
2364 // zoomed and the devicePixelRatio changes accordingly.
2365 canvas.style.width = canvas.width + 'px';
2366 canvas.style.height = canvas.height + 'px';
2367 }
2368 return false;
2369};
2370
2371
2372/**
2373 * Get random number.
2374 * @param {number} min
2375 * @param {number} max
2376 * @param {number}
2377 */
2378function getRandomNum(min, max) {
2379 return Math.floor(Math.random() * (max - min + 1)) + min;
2380}
2381
2382
2383/**
2384 * Vibrate on mobile devices.
2385 * @param {number} duration Duration of the vibration in milliseconds.
2386 */
2387function vibrate(duration) {
2388 if (IS_MOBILE && window.navigator.vibrate) {
2389 window.navigator.vibrate(duration);
2390 }
2391}
2392
2393
2394/**
2395 * Create canvas element.
2396 * @param {HTMLElement} container Element to append canvas to.
2397 * @param {number} width
2398 * @param {number} height
2399 * @param {string} opt_classname
2400 * @return {HTMLCanvasElement}
2401 */
2402function createCanvas(container, width, height, opt_classname) {
2403 var canvas = document.createElement('canvas');
2404 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
2405 opt_classname : Runner.classes.CANVAS;
2406 canvas.width = width;
2407 canvas.height = height;
2408 container.appendChild(canvas);
2409
2410 return canvas;
2411}
2412
2413
2414/**
2415 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
2416 * @param {string} base64String
2417 */
2418function decodeBase64ToArrayBuffer(base64String) {
2419 var len = (base64String.length / 4) * 3;
2420 var str = atob(base64String);
2421 var arrayBuffer = new ArrayBuffer(len);
2422 var bytes = new Uint8Array(arrayBuffer);
2423
2424 for (var i = 0; i < len; i++) {
2425 bytes[i] = str.charCodeAt(i);
2426 }
2427 return bytes.buffer;
2428}
2429
2430
2431/**
2432 * Return the current timestamp.
2433 * @return {number}
2434 */
2435function getTimeStamp() {
2436 return IS_IOS ? new Date().getTime() : performance.now();
2437}
2438
2439
2440//******************************************************************************
2441
2442
2443/**
2444 * Game over panel.
2445 * @param {!HTMLCanvasElement} canvas
2446 * @param {Object} textImgPos
2447 * @param {Object} restartImgPos
2448 * @param {!Object} dimensions Canvas dimensions.
2449 * @constructor
2450 */
2451function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
2452 this.canvas = canvas;
2453 this.canvasCtx = canvas.getContext('2d');
2454 this.canvasDimensions = dimensions;
2455 this.textImgPos = textImgPos;
2456 this.restartImgPos = restartImgPos;
2457 this.draw();
2458};
2459
2460
2461/**
2462 * Dimensions used in the panel.
2463 * @enum {number}
2464 */
2465GameOverPanel.dimensions = {
2466 TEXT_X: 0,
2467 TEXT_Y: 13,
2468 TEXT_WIDTH: 191,
2469 TEXT_HEIGHT: 11,
2470 RESTART_WIDTH: 36,
2471 RESTART_HEIGHT: 32
2472};
2473
2474
2475GameOverPanel.prototype = {
2476 /**
2477 * Update the panel dimensions.
2478 * @param {number} width New canvas width.
2479 * @param {number} opt_height Optional new canvas height.
2480 */
2481 updateDimensions: function(width, opt_height) {
2482 this.canvasDimensions.WIDTH = width;
2483 if (opt_height) {
2484 this.canvasDimensions.HEIGHT = opt_height;
2485 }
2486 },
2487
2488 /**
2489 * Draw the panel.
2490 */
2491 draw: function() {
2492 var dimensions = GameOverPanel.dimensions;
2493
2494 var centerX = this.canvasDimensions.WIDTH / 2;
2495
2496 // Game over text.
2497 var textSourceX = dimensions.TEXT_X;
2498 var textSourceY = dimensions.TEXT_Y;
2499 var textSourceWidth = dimensions.TEXT_WIDTH;
2500 var textSourceHeight = dimensions.TEXT_HEIGHT;
2501
2502 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
2503 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
2504 var textTargetWidth = dimensions.TEXT_WIDTH;
2505 var textTargetHeight = dimensions.TEXT_HEIGHT;
2506
2507 var restartSourceWidth = dimensions.RESTART_WIDTH;
2508 var restartSourceHeight = dimensions.RESTART_HEIGHT;
2509 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
2510 var restartTargetY = this.canvasDimensions.HEIGHT / 2;
2511
2512 if (IS_HIDPI) {
2513 textSourceY *= 2;
2514 textSourceX *= 2;
2515 textSourceWidth *= 2;
2516 textSourceHeight *= 2;
2517 restartSourceWidth *= 2;
2518 restartSourceHeight *= 2;
2519 }
2520
2521 textSourceX += this.textImgPos.x;
2522 textSourceY += this.textImgPos.y;
2523
2524 // Game over text from sprite.
2525 this.canvasCtx.drawImage(Runner.imageSprite,
2526 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
2527 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
2528
2529 // Restart button.
2530 this.canvasCtx.drawImage(Runner.imageSprite,
2531 this.restartImgPos.x, this.restartImgPos.y,
2532 restartSourceWidth, restartSourceHeight,
2533 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
2534 dimensions.RESTART_HEIGHT);
2535 }
2536};
2537
2538
2539//******************************************************************************
2540
2541/**
2542 * Check for a collision.
2543 * @param {!Obstacle} obstacle
2544 * @param {!Trex} tRex T-rex object.
2545 * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
2546 * collision boxes.
2547 * @return {Array<CollisionBox>}
2548 */
2549function checkForCollision(obstacle, tRex, opt_canvasCtx) {
2550 var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
2551
2552 // Adjustments are made to the bounding box as there is a 1 pixel white
2553 // border around the t-rex and obstacles.
2554 var tRexBox = new CollisionBox(
2555 tRex.xPos + 1,
2556 tRex.yPos + 1,
2557 tRex.config.WIDTH - 2,
2558 tRex.config.HEIGHT - 2);
2559
2560 var obstacleBox = new CollisionBox(
2561 obstacle.xPos + 1,
2562 obstacle.yPos + 1,
2563 obstacle.typeConfig.width * obstacle.size - 2,
2564 obstacle.typeConfig.height - 2);
2565
2566 // Debug outer box
2567 if (opt_canvasCtx) {
2568 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
2569 }
2570
2571 // Simple outer bounds check.
2572 if (boxCompare(tRexBox, obstacleBox)) {
2573 var collisionBoxes = obstacle.collisionBoxes;
2574 var tRexCollisionBoxes = tRex.ducking ?
2575 Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
2576
2577 // Detailed axis aligned box check.
2578 for (var t = 0; t < tRexCollisionBoxes.length; t++) {
2579 for (var i = 0; i < collisionBoxes.length; i++) {
2580 // Adjust the box to actual positions.
2581 var adjTrexBox =
2582 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
2583 var adjObstacleBox =
2584 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
2585 var crashed = boxCompare(adjTrexBox, adjObstacleBox);
2586
2587 // Draw boxes for debug.
2588 if (opt_canvasCtx) {
2589 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
2590 }
2591
2592 if (crashed) {
2593 return [adjTrexBox, adjObstacleBox];
2594 }
2595 }
2596 }
2597 }
2598 return false;
2599};
2600
2601
2602/**
2603 * Adjust the collision box.
2604 * @param {!CollisionBox} box The original box.
2605 * @param {!CollisionBox} adjustment Adjustment box.
2606 * @return {CollisionBox} The adjusted collision box object.
2607 */
2608function createAdjustedCollisionBox(box, adjustment) {
2609 return new CollisionBox(
2610 box.x + adjustment.x,
2611 box.y + adjustment.y,
2612 box.width,
2613 box.height);
2614};
2615
2616
2617/**
2618 * Draw the collision boxes for debug.
2619 */
2620function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
2621 canvasCtx.save();
2622 canvasCtx.strokeStyle = '#f00';
2623 canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
2624
2625 canvasCtx.strokeStyle = '#0f0';
2626 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
2627 obstacleBox.width, obstacleBox.height);
2628 canvasCtx.restore();
2629};
2630
2631
2632/**
2633 * Compare two collision boxes for a collision.
2634 * @param {CollisionBox} tRexBox
2635 * @param {CollisionBox} obstacleBox
2636 * @return {boolean} Whether the boxes intersected.
2637 */
2638function boxCompare(tRexBox, obstacleBox) {
2639 var crashed = false;
2640 var tRexBoxX = tRexBox.x;
2641 var tRexBoxY = tRexBox.y;
2642
2643 var obstacleBoxX = obstacleBox.x;
2644 var obstacleBoxY = obstacleBox.y;
2645
2646 // Axis-Aligned Bounding Box method.
2647 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
2648 tRexBox.x + tRexBox.width > obstacleBoxX &&
2649 tRexBox.y < obstacleBox.y + obstacleBox.height &&
2650 tRexBox.height + tRexBox.y > obstacleBox.y) {
2651 crashed = true;
2652 }
2653
2654 return crashed;
2655};
2656
2657
2658//******************************************************************************
2659
2660/**
2661 * Collision box object.
2662 * @param {number} x X position.
2663 * @param {number} y Y Position.
2664 * @param {number} w Width.
2665 * @param {number} h Height.
2666 */
2667function CollisionBox(x, y, w, h) {
2668 this.x = x;
2669 this.y = y;
2670 this.width = w;
2671 this.height = h;
2672};
2673
2674
2675//******************************************************************************
2676
2677/**
2678 * Obstacle.
2679 * @param {HTMLCanvasCtx} canvasCtx
2680 * @param {Obstacle.type} type
2681 * @param {Object} spritePos Obstacle position in sprite.
2682 * @param {Object} dimensions
2683 * @param {number} gapCoefficient Mutipler in determining the gap.
2684 * @param {number} speed
2685 * @param {number} opt_xOffset
2686 */
2687function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
2688 gapCoefficient, speed, opt_xOffset) {
2689
2690 this.canvasCtx = canvasCtx;
2691 this.spritePos = spriteImgPos;
2692 this.typeConfig = type;
2693 this.gapCoefficient = gapCoefficient;
2694 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
2695 this.dimensions = dimensions;
2696 this.remove = false;
2697 this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
2698 this.yPos = 0;
2699 this.width = 0;
2700 this.collisionBoxes = [];
2701 this.gap = 0;
2702 this.speedOffset = 0;
2703
2704 // For animated obstacles.
2705 this.currentFrame = 0;
2706 this.timer = 0;
2707
2708 this.init(speed);
2709};
2710
2711/**
2712 * Coefficient for calculating the maximum gap.
2713 * @const
2714 */
2715Obstacle.MAX_GAP_COEFFICIENT = 1.5;
2716
2717/**
2718 * Maximum obstacle grouping count.
2719 * @const
2720 */
2721Obstacle.MAX_OBSTACLE_LENGTH = 3,
2722
2723
2724Obstacle.prototype = {
2725 /**
2726 * Initialise the DOM for the obstacle.
2727 * @param {number} speed
2728 */
2729 init: function(speed) {
2730 this.cloneCollisionBoxes();
2731
2732 // Only allow sizing if we're at the right speed.
2733 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
2734 this.size = 1;
2735 }
2736
2737 this.width = this.typeConfig.width * this.size;
2738
2739 // Check if obstacle can be positioned at various heights.
2740 if (Array.isArray(this.typeConfig.yPos)) {
2741 var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
2742 this.typeConfig.yPos;
2743 this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
2744 } else {
2745 this.yPos = this.typeConfig.yPos;
2746 }
2747
2748 this.draw();
2749
2750 // Make collision box adjustments,
2751 // Central box is adjusted to the size as one box.
2752 // ____ ______ ________
2753 // _| |-| _| |-| _| |-|
2754 // | |<->| | | |<--->| | | |<----->| |
2755 // | | 1 | | | | 2 | | | | 3 | |
2756 // |_|___|_| |_|_____|_| |_|_______|_|
2757 //
2758 if (this.size > 1) {
2759 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
2760 this.collisionBoxes[2].width;
2761 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
2762 }
2763
2764 // For obstacles that go at a different speed from the horizon.
2765 if (this.typeConfig.speedOffset) {
2766 this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
2767 -this.typeConfig.speedOffset;
2768 }
2769
2770 this.gap = this.getGap(this.gapCoefficient, speed);
2771 },
2772
2773 /**
2774 * Draw and crop based on size.
2775 */
2776 draw: function() {
2777 var sourceWidth = this.typeConfig.width;
2778 var sourceHeight = this.typeConfig.height;
2779
2780 if (IS_HIDPI) {
2781 sourceWidth = sourceWidth * 2;
2782 sourceHeight = sourceHeight * 2;
2783 }
2784
2785 // X position in sprite.
2786 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
2787 this.spritePos.x;
2788
2789 // Animation frames.
2790 if (this.currentFrame > 0) {
2791 sourceX += sourceWidth * this.currentFrame;
2792 }
2793
2794 this.canvasCtx.drawImage(Runner.imageSprite,
2795 sourceX, this.spritePos.y,
2796 sourceWidth * this.size, sourceHeight,
2797 this.xPos, this.yPos,
2798 this.typeConfig.width * this.size, this.typeConfig.height);
2799 },
2800
2801 /**
2802 * Obstacle frame update.
2803 * @param {number} deltaTime
2804 * @param {number} speed
2805 */
2806 update: function(deltaTime, speed) {
2807 if (!this.remove) {
2808 if (this.typeConfig.speedOffset) {
2809 speed += this.speedOffset;
2810 }
2811 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
2812
2813 // Update frame
2814 if (this.typeConfig.numFrames) {
2815 this.timer += deltaTime;
2816 if (this.timer >= this.typeConfig.frameRate) {
2817 this.currentFrame =
2818 this.currentFrame == this.typeConfig.numFrames - 1 ?
2819 0 : this.currentFrame + 1;
2820 this.timer = 0;
2821 }
2822 }
2823 this.draw();
2824
2825 if (!this.isVisible()) {
2826 this.remove = true;
2827 }
2828 }
2829 },
2830
2831 /**
2832 * Calculate a random gap size.
2833 * - Minimum gap gets wider as speed increses
2834 * @param {number} gapCoefficient
2835 * @param {number} speed
2836 * @return {number} The gap size.
2837 */
2838 getGap: function(gapCoefficient, speed) {
2839 var minGap = Math.round(this.width * speed +
2840 this.typeConfig.minGap * gapCoefficient);
2841 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
2842 return getRandomNum(minGap, maxGap);
2843 },
2844
2845 /**
2846 * Check if obstacle is visible.
2847 * @return {boolean} Whether the obstacle is in the game area.
2848 */
2849 isVisible: function() {
2850 return this.xPos + this.width > 0;
2851 },
2852
2853 /**
2854 * Make a copy of the collision boxes, since these will change based on
2855 * obstacle type and size.
2856 */
2857 cloneCollisionBoxes: function() {
2858 var collisionBoxes = this.typeConfig.collisionBoxes;
2859
2860 for (var i = collisionBoxes.length - 1; i >= 0; i--) {
2861 this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
2862 collisionBoxes[i].y, collisionBoxes[i].width,
2863 collisionBoxes[i].height);
2864 }
2865 }
2866};
2867
2868
2869/**
2870 * Obstacle definitions.
2871 * minGap: minimum pixel space betweeen obstacles.
2872 * multipleSpeed: Speed at which multiples are allowed.
2873 * speedOffset: speed faster / slower than the horizon.
2874 * minSpeed: Minimum speed which the obstacle can make an appearance.
2875 */
2876Obstacle.types = [
2877 {
2878 type: 'CACTUS_SMALL',
2879 width: 17,
2880 height: 35,
2881 yPos: 105,
2882 multipleSpeed: 4,
2883 minGap: 120,
2884 minSpeed: 0,
2885 collisionBoxes: [
2886 new CollisionBox(0, 7, 5, 27),
2887 new CollisionBox(4, 0, 6, 34),
2888 new CollisionBox(10, 4, 7, 14)
2889 ]
2890 },
2891 {
2892 type: 'CACTUS_LARGE',
2893 width: 25,
2894 height: 50,
2895 yPos: 90,
2896 multipleSpeed: 7,
2897 minGap: 120,
2898 minSpeed: 0,
2899 collisionBoxes: [
2900 new CollisionBox(0, 12, 7, 38),
2901 new CollisionBox(8, 0, 7, 49),
2902 new CollisionBox(13, 10, 10, 38)
2903 ]
2904 },
2905 {
2906 type: 'PTERODACTYL',
2907 width: 46,
2908 height: 40,
2909 yPos: [ 100, 75, 50 ], // Variable height.
2910 yPosMobile: [ 100, 50 ], // Variable height mobile.
2911 multipleSpeed: 999,
2912 minSpeed: 8.5,
2913 minGap: 150,
2914 collisionBoxes: [
2915 new CollisionBox(15, 15, 16, 5),
2916 new CollisionBox(18, 21, 24, 6),
2917 new CollisionBox(2, 14, 4, 3),
2918 new CollisionBox(6, 10, 4, 7),
2919 new CollisionBox(10, 8, 6, 9)
2920 ],
2921 numFrames: 2,
2922 frameRate: 1000/6,
2923 speedOffset: .8
2924 }
2925];
2926
2927
2928//******************************************************************************
2929/**
2930 * T-rex game character.
2931 * @param {HTMLCanvas} canvas
2932 * @param {Object} spritePos Positioning within image sprite.
2933 * @constructor
2934 */
2935function Trex(canvas, spritePos) {
2936 this.canvas = canvas;
2937 this.canvasCtx = canvas.getContext('2d');
2938 this.spritePos = spritePos;
2939 this.xPos = 0;
2940 this.yPos = 0;
2941 // Position when on the ground.
2942 this.groundYPos = 0;
2943 this.currentFrame = 0;
2944 this.currentAnimFrames = [];
2945 this.blinkDelay = 0;
2946 this.blinkCount = 0;
2947 this.animStartTime = 0;
2948 this.timer = 0;
2949 this.msPerFrame = 1000 / FPS;
2950 this.config = Trex.config;
2951 // Current status.
2952 this.status = Trex.status.WAITING;
2953
2954 this.jumping = false;
2955 this.ducking = false;
2956 this.jumpVelocity = 0;
2957 this.reachedMinHeight = false;
2958 this.speedDrop = false;
2959 this.jumpCount = 0;
2960 this.jumpspotX = 0;
2961
2962 this.init();
2963};
2964
2965
2966/**
2967 * T-rex player config.
2968 * @enum {number}
2969 */
2970Trex.config = {
2971 DROP_VELOCITY: -5,
2972 GRAVITY: 0.6,
2973 HEIGHT: 47,
2974 HEIGHT_DUCK: 25,
2975 INIITAL_JUMP_VELOCITY: -10,
2976 INTRO_DURATION: 1500,
2977 MAX_JUMP_HEIGHT: 30,
2978 MIN_JUMP_HEIGHT: 30,
2979 SPEED_DROP_COEFFICIENT: 3,
2980 SPRITE_WIDTH: 262,
2981 START_X_POS: 50,
2982 WIDTH: 44,
2983 WIDTH_DUCK: 59
2984};
2985
2986
2987/**
2988 * Used in collision detection.
2989 * @type {Array<CollisionBox>}
2990 */
2991Trex.collisionBoxes = {
2992 DUCKING: [
2993 new CollisionBox(1, 18, 55, 25)
2994 ],
2995 RUNNING: [
2996 new CollisionBox(22, 0, 17, 16),
2997 new CollisionBox(1, 18, 30, 9),
2998 new CollisionBox(10, 35, 14, 8),
2999 new CollisionBox(1, 24, 29, 5),
3000 new CollisionBox(5, 30, 21, 4),
3001 new CollisionBox(9, 34, 15, 4)
3002 ]
3003};
3004
3005
3006/**
3007 * Animation states.
3008 * @enum {string}
3009 */
3010Trex.status = {
3011 CRASHED: 'CRASHED',
3012 DUCKING: 'DUCKING',
3013 JUMPING: 'JUMPING',
3014 RUNNING: 'RUNNING',
3015 WAITING: 'WAITING'
3016};
3017
3018/**
3019 * Blinking coefficient.
3020 * @const
3021 */
3022Trex.BLINK_TIMING = 7000;
3023
3024
3025/**
3026 * Animation config for different states.
3027 * @enum {Object}
3028 */
3029Trex.animFrames = {
3030 WAITING: {
3031 frames: [44, 0],
3032 msPerFrame: 1000 / 3
3033 },
3034 RUNNING: {
3035 frames: [88, 132],
3036 msPerFrame: 1000 / 12
3037 },
3038 CRASHED: {
3039 frames: [220],
3040 msPerFrame: 1000 / 60
3041 },
3042 JUMPING: {
3043 frames: [0],
3044 msPerFrame: 1000 / 60
3045 },
3046 DUCKING: {
3047 frames: [262, 321],
3048 msPerFrame: 1000 / 8
3049 }
3050};
3051
3052
3053Trex.prototype = {
3054 /**
3055 * T-rex player initaliser.
3056 * Sets the t-rex to blink at random intervals.
3057 */
3058 init: function() {
3059 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
3060 Runner.config.BOTTOM_PAD;
3061 this.yPos = this.groundYPos;
3062 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
3063
3064 this.draw(0, 0);
3065 this.update(0, Trex.status.WAITING);
3066 },
3067
3068 /**
3069 * Setter for the jump velocity.
3070 * The approriate drop velocity is also set.
3071 */
3072 setJumpVelocity: function(setting) {
3073 this.config.INIITAL_JUMP_VELOCITY = -setting;
3074 this.config.DROP_VELOCITY = -setting / 2;
3075 },
3076
3077 /**
3078 * Set the animation status.
3079 * @param {!number} deltaTime
3080 * @param {Trex.status} status Optional status to switch to.
3081 */
3082 update: function(deltaTime, opt_status) {
3083 this.timer += deltaTime;
3084
3085 // Update the status.
3086 if (opt_status) {
3087 this.status = opt_status;
3088 this.currentFrame = 0;
3089 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
3090 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
3091
3092 if (opt_status == Trex.status.WAITING) {
3093 this.animStartTime = getTimeStamp();
3094 this.setBlinkDelay();
3095 }
3096 }
3097
3098 // Game intro animation, T-rex moves in from the left.
3099 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
3100 this.xPos += Math.round((this.config.START_X_POS /
3101 this.config.INTRO_DURATION) * deltaTime);
3102 }
3103
3104 if (this.status == Trex.status.WAITING) {
3105 this.blink(getTimeStamp());
3106 } else {
3107 this.draw(this.currentAnimFrames[this.currentFrame], 0);
3108 }
3109
3110 // Update the frame position.
3111 if (this.timer >= this.msPerFrame) {
3112 this.currentFrame = this.currentFrame ==
3113 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
3114 this.timer = 0;
3115 }
3116
3117 // Speed drop becomes duck if the down key is still being pressed.
3118 if (this.speedDrop && this.yPos == this.groundYPos) {
3119 this.speedDrop = false;
3120 this.setDuck(true);
3121 }
3122 },
3123
3124 /**
3125 * Draw the t-rex to a particular position.
3126 * @param {number} x
3127 * @param {number} y
3128 */
3129 draw: function(x, y) {
3130 var sourceX = x;
3131 var sourceY = y;
3132 var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
3133 this.config.WIDTH_DUCK : this.config.WIDTH;
3134 var sourceHeight = this.config.HEIGHT;
3135
3136 if (IS_HIDPI) {
3137 sourceX *= 2;
3138 sourceY *= 2;
3139 sourceWidth *= 2;
3140 sourceHeight *= 2;
3141 }
3142
3143 // Adjustments for sprite sheet position.
3144 sourceX += this.spritePos.x;
3145 sourceY += this.spritePos.y;
3146
3147 // Ducking.
3148 if (this.ducking && this.status != Trex.status.CRASHED) {
3149 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
3150 sourceWidth, sourceHeight,
3151 this.xPos, this.yPos,
3152 this.config.WIDTH_DUCK, this.config.HEIGHT);
3153 } else {
3154 // Crashed whilst ducking. Trex is standing up so needs adjustment.
3155 if (this.ducking && this.status == Trex.status.CRASHED) {
3156 this.xPos++;
3157 }
3158 // Standing / running
3159 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
3160 sourceWidth, sourceHeight,
3161 this.xPos, this.yPos,
3162 this.config.WIDTH, this.config.HEIGHT);
3163 }
3164 },
3165
3166 /**
3167 * Sets a random time for the blink to happen.
3168 */
3169 setBlinkDelay: function() {
3170 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
3171 },
3172
3173 /**
3174 * Make t-rex blink at random intervals.
3175 * @param {number} time Current time in milliseconds.
3176 */
3177 blink: function(time) {
3178 var deltaTime = time - this.animStartTime;
3179
3180 if (deltaTime >= this.blinkDelay) {
3181 this.draw(this.currentAnimFrames[this.currentFrame], 0);
3182
3183 if (this.currentFrame == 1) {
3184 // Set new random delay to blink.
3185 this.setBlinkDelay();
3186 this.animStartTime = time;
3187 this.blinkCount++;
3188 }
3189 }
3190 },
3191
3192 /**
3193 * Initialise a jump.
3194 * @param {number} speed
3195 */
3196 startJump: function(speed) {
3197 if (!this.jumping) {
3198 this.update(0, Trex.status.JUMPING);
3199 // Tweak the jump velocity based on the speed.
3200 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
3201 this.jumping = true;
3202 this.reachedMinHeight = false;
3203 this.speedDrop = false;
3204 }
3205 },
3206
3207 /**
3208 * Jump is complete, falling down.
3209 */
3210 endJump: function() {
3211 if (this.reachedMinHeight &&
3212 this.jumpVelocity < this.config.DROP_VELOCITY) {
3213 this.jumpVelocity = this.config.DROP_VELOCITY;
3214 }
3215 },
3216
3217 /**
3218 * Update frame for a jump.
3219 * @param {number} deltaTime
3220 * @param {number} speed
3221 */
3222 updateJump: function(deltaTime, speed) {
3223 var msPerFrame = Trex.animFrames[this.status].msPerFrame;
3224 var framesElapsed = deltaTime / msPerFrame;
3225
3226 // Speed drop makes Trex fall faster.
3227 if (this.speedDrop) {
3228 this.yPos += Math.round(this.jumpVelocity *
3229 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
3230 } else {
3231 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
3232 }
3233
3234 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
3235
3236 // Minimum height has been reached.
3237 if (this.yPos < this.minJumpHeight || this.speedDrop) {
3238 this.reachedMinHeight = true;
3239 }
3240
3241 // Reached max height
3242 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
3243 this.endJump();
3244 }
3245
3246 // Back down at ground level. Jump completed.
3247 if (this.yPos > this.groundYPos) {
3248 this.reset();
3249 this.jumpCount++;
3250 }
3251
3252 this.update(deltaTime);
3253 },
3254
3255 /**
3256 * Set the speed drop. Immediately cancels the current jump.
3257 */
3258 setSpeedDrop: function() {
3259 this.speedDrop = true;
3260 this.jumpVelocity = 1;
3261 },
3262
3263 /**
3264 * @param {boolean} isDucking.
3265 */
3266 setDuck: function(isDucking) {
3267 if (isDucking && this.status != Trex.status.DUCKING) {
3268 this.update(0, Trex.status.DUCKING);
3269 this.ducking = true;
3270 } else if (this.status == Trex.status.DUCKING) {
3271 this.update(0, Trex.status.RUNNING);
3272 this.ducking = false;
3273 }
3274 },
3275
3276 /**
3277 * Reset the t-rex to running at start of game.
3278 */
3279 reset: function() {
3280 this.yPos = this.groundYPos;
3281 this.jumpVelocity = 0;
3282 this.jumping = false;
3283 this.ducking = false;
3284 this.update(0, Trex.status.RUNNING);
3285 this.midair = false;
3286 this.speedDrop = false;
3287 this.jumpCount = 0;
3288 }
3289};
3290
3291
3292//******************************************************************************
3293
3294/**
3295 * Handles displaying the distance meter.
3296 * @param {!HTMLCanvasElement} canvas
3297 * @param {Object} spritePos Image position in sprite.
3298 * @param {number} canvasWidth
3299 * @constructor
3300 */
3301function DistanceMeter(canvas, spritePos, canvasWidth) {
3302 this.canvas = canvas;
3303 this.canvasCtx = canvas.getContext('2d');
3304 this.image = Runner.imageSprite;
3305 this.spritePos = spritePos;
3306 this.x = 0;
3307 this.y = 5;
3308
3309 this.currentDistance = 0;
3310 this.maxScore = 0;
3311 this.highScore = 0;
3312 this.container = null;
3313
3314 this.digits = [];
3315 this.achievement = false;
3316 this.defaultString = '';
3317 this.flashTimer = 0;
3318 this.flashIterations = 0;
3319 this.invertTrigger = false;
3320
3321 this.config = DistanceMeter.config;
3322 this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
3323 this.init(canvasWidth);
3324};
3325
3326
3327/**
3328 * @enum {number}
3329 */
3330DistanceMeter.dimensions = {
3331 WIDTH: 10,
3332 HEIGHT: 13,
3333 DEST_WIDTH: 11
3334};
3335
3336
3337/**
3338 * Y positioning of the digits in the sprite sheet.
3339 * X position is always 0.
3340 * @type {Array<number>}
3341 */
3342DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
3343
3344
3345/**
3346 * Distance meter config.
3347 * @enum {number}
3348 */
3349DistanceMeter.config = {
3350 // Number of digits.
3351 MAX_DISTANCE_UNITS: 5,
3352
3353 // Distance that causes achievement animation.
3354 ACHIEVEMENT_DISTANCE: 100,
3355
3356 // Used for conversion from pixel distance to a scaled unit.
3357 COEFFICIENT: 0.025,
3358
3359 // Flash duration in milliseconds.
3360 FLASH_DURATION: 1000 / 4,
3361
3362 // Flash iterations for achievement animation.
3363 FLASH_ITERATIONS: 3
3364};
3365
3366
3367DistanceMeter.prototype = {
3368 /**
3369 * Initialise the distance meter to '00000'.
3370 * @param {number} width Canvas width in px.
3371 */
3372 init: function(width) {
3373 var maxDistanceStr = '';
3374
3375 this.calcXPos(width);
3376 this.maxScore = this.maxScoreUnits;
3377 for (var i = 0; i < this.maxScoreUnits; i++) {
3378 this.draw(i, 0);
3379 this.defaultString += '0';
3380 maxDistanceStr += '9';
3381 }
3382
3383 this.maxScore = parseInt(maxDistanceStr);
3384 },
3385
3386 /**
3387 * Calculate the xPos in the canvas.
3388 * @param {number} canvasWidth
3389 */
3390 calcXPos: function(canvasWidth) {
3391 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
3392 (this.maxScoreUnits + 1));
3393 },
3394
3395 /**
3396 * Draw a digit to canvas.
3397 * @param {number} digitPos Position of the digit.
3398 * @param {number} value Digit value 0-9.
3399 * @param {boolean} opt_highScore Whether drawing the high score.
3400 */
3401 draw: function(digitPos, value, opt_highScore) {
3402 var sourceWidth = DistanceMeter.dimensions.WIDTH;
3403 var sourceHeight = DistanceMeter.dimensions.HEIGHT;
3404 var sourceX = DistanceMeter.dimensions.WIDTH * value;
3405 var sourceY = 0;
3406
3407 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
3408 var targetY = this.y;
3409 var targetWidth = DistanceMeter.dimensions.WIDTH;
3410 var targetHeight = DistanceMeter.dimensions.HEIGHT;
3411
3412 // For high DPI we 2x source values.
3413 if (IS_HIDPI) {
3414 sourceWidth *= 2;
3415 sourceHeight *= 2;
3416 sourceX *= 2;
3417 }
3418
3419 sourceX += this.spritePos.x;
3420 sourceY += this.spritePos.y;
3421
3422 this.canvasCtx.save();
3423
3424 if (opt_highScore) {
3425 // Left of the current score.
3426 var highScoreX = this.x - (this.maxScoreUnits * 2) *
3427 DistanceMeter.dimensions.WIDTH;
3428 this.canvasCtx.translate(highScoreX, this.y);
3429 } else {
3430 this.canvasCtx.translate(this.x, this.y);
3431 }
3432
3433 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
3434 sourceWidth, sourceHeight,
3435 targetX, targetY,
3436 targetWidth, targetHeight
3437 );
3438
3439 this.canvasCtx.restore();
3440 },
3441
3442 /**
3443 * Covert pixel distance to a 'real' distance.
3444 * @param {number} distance Pixel distance ran.
3445 * @return {number} The 'real' distance ran.
3446 */
3447 getActualDistance: function(distance) {
3448 return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
3449 },
3450
3451 /**
3452 * Update the distance meter.
3453 * @param {number} distance
3454 * @param {number} deltaTime
3455 * @return {boolean} Whether the acheivement sound fx should be played.
3456 */
3457 update: function(deltaTime, distance) {
3458 var paint = true;
3459 var playSound = false;
3460
3461 if (!this.achievement) {
3462 distance = this.getActualDistance(distance);
3463 // Score has gone beyond the initial digit count.
3464 if (distance > this.maxScore && this.maxScoreUnits ==
3465 this.config.MAX_DISTANCE_UNITS) {
3466 this.maxScoreUnits++;
3467 this.maxScore = parseInt(this.maxScore + '9');
3468 } else {
3469 this.distance = 0;
3470 }
3471
3472 if (distance > 0) {
3473 // Acheivement unlocked
3474 if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
3475 // Flash score and play sound.
3476 this.achievement = true;
3477 this.flashTimer = 0;
3478 playSound = true;
3479 }
3480
3481 // Create a string representation of the distance with leading 0.
3482 var distanceStr = (this.defaultString +
3483 distance).substr(-this.maxScoreUnits);
3484 this.digits = distanceStr.split('');
3485 } else {
3486 this.digits = this.defaultString.split('');
3487 }
3488 } else {
3489 // Control flashing of the score on reaching acheivement.
3490 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
3491 this.flashTimer += deltaTime;
3492
3493 if (this.flashTimer < this.config.FLASH_DURATION) {
3494 paint = false;
3495 } else if (this.flashTimer >
3496 this.config.FLASH_DURATION * 2) {
3497 this.flashTimer = 0;
3498 this.flashIterations++;
3499 }
3500 } else {
3501 this.achievement = false;
3502 this.flashIterations = 0;
3503 this.flashTimer = 0;
3504 }
3505 }
3506
3507 // Draw the digits if not flashing.
3508 if (paint) {
3509 for (var i = this.digits.length - 1; i >= 0; i--) {
3510 this.draw(i, parseInt(this.digits[i]));
3511 }
3512 }
3513
3514 this.drawHighScore();
3515 return playSound;
3516 },
3517
3518 /**
3519 * Draw the high score.
3520 */
3521 drawHighScore: function() {
3522 this.canvasCtx.save();
3523 this.canvasCtx.globalAlpha = .8;
3524 for (var i = this.highScore.length - 1; i >= 0; i--) {
3525 this.draw(i, parseInt(this.highScore[i], 10), true);
3526 }
3527 this.canvasCtx.restore();
3528 },
3529
3530 /**
3531 * Set the highscore as a array string.
3532 * Position of char in the sprite: H - 10, I - 11.
3533 * @param {number} distance Distance ran in pixels.
3534 */
3535 setHighScore: function(distance) {
3536 distance = this.getActualDistance(distance);
3537 var highScoreStr = (this.defaultString +
3538 distance).substr(-this.maxScoreUnits);
3539
3540 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
3541 },
3542
3543 /**
3544 * Reset the distance meter back to '00000'.
3545 */
3546 reset: function() {
3547 this.update(0);
3548 this.achievement = false;
3549 }
3550};
3551
3552
3553//******************************************************************************
3554
3555/**
3556 * Cloud background item.
3557 * Similar to an obstacle object but without collision boxes.
3558 * @param {HTMLCanvasElement} canvas Canvas element.
3559 * @param {Object} spritePos Position of image in sprite.
3560 * @param {number} containerWidth
3561 */
3562function Cloud(canvas, spritePos, containerWidth) {
3563 this.canvas = canvas;
3564 this.canvasCtx = this.canvas.getContext('2d');
3565 this.spritePos = spritePos;
3566 this.containerWidth = containerWidth;
3567 this.xPos = containerWidth;
3568 this.yPos = 0;
3569 this.remove = false;
3570 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
3571 Cloud.config.MAX_CLOUD_GAP);
3572
3573 this.init();
3574};
3575
3576
3577/**
3578 * Cloud object config.
3579 * @enum {number}
3580 */
3581Cloud.config = {
3582 HEIGHT: 14,
3583 MAX_CLOUD_GAP: 400,
3584 MAX_SKY_LEVEL: 30,
3585 MIN_CLOUD_GAP: 100,
3586 MIN_SKY_LEVEL: 71,
3587 WIDTH: 46
3588};
3589
3590
3591Cloud.prototype = {
3592 /**
3593 * Initialise the cloud. Sets the Cloud height.
3594 */
3595 init: function() {
3596 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
3597 Cloud.config.MIN_SKY_LEVEL);
3598 this.draw();
3599 },
3600
3601 /**
3602 * Draw the cloud.
3603 */
3604 draw: function() {
3605 this.canvasCtx.save();
3606 var sourceWidth = Cloud.config.WIDTH;
3607 var sourceHeight = Cloud.config.HEIGHT;
3608
3609 if (IS_HIDPI) {
3610 sourceWidth = sourceWidth * 2;
3611 sourceHeight = sourceHeight * 2;
3612 }
3613
3614 this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
3615 this.spritePos.y,
3616 sourceWidth, sourceHeight,
3617 this.xPos, this.yPos,
3618 Cloud.config.WIDTH, Cloud.config.HEIGHT);
3619
3620 this.canvasCtx.restore();
3621 },
3622
3623 /**
3624 * Update the cloud position.
3625 * @param {number} speed
3626 */
3627 update: function(speed) {
3628 if (!this.remove) {
3629 this.xPos -= Math.ceil(speed);
3630 this.draw();
3631
3632 // Mark as removeable if no longer in the canvas.
3633 if (!this.isVisible()) {
3634 this.remove = true;
3635 }
3636 }
3637 },
3638
3639 /**
3640 * Check if the cloud is visible on the stage.
3641 * @return {boolean}
3642 */
3643 isVisible: function() {
3644 return this.xPos + Cloud.config.WIDTH > 0;
3645 }
3646};
3647
3648
3649//******************************************************************************
3650
3651/**
3652 * Nightmode shows a moon and stars on the horizon.
3653 */
3654function NightMode(canvas, spritePos, containerWidth) {
3655 this.spritePos = spritePos;
3656 this.canvas = canvas;
3657 this.canvasCtx = canvas.getContext('2d');
3658 this.xPos = containerWidth - 50;
3659 this.yPos = 30;
3660 this.currentPhase = 0;
3661 this.opacity = 0;
3662 this.containerWidth = containerWidth;
3663 this.stars = [];
3664 this.drawStars = false;
3665 this.placeStars();
3666};
3667
3668/**
3669 * @enum {number}
3670 */
3671NightMode.config = {
3672 FADE_SPEED: 0.035,
3673 HEIGHT: 40,
3674 MOON_SPEED: 0.25,
3675 NUM_STARS: 2,
3676 STAR_SIZE: 9,
3677 STAR_SPEED: 0.3,
3678 STAR_MAX_Y: 70,
3679 WIDTH: 20
3680};
3681
3682NightMode.phases = [140, 120, 100, 60, 40, 20, 0];
3683
3684NightMode.prototype = {
3685 /**
3686 * Update moving moon, changing phases.
3687 * @param {boolean} activated Whether night mode is activated.
3688 * @param {number} delta
3689 */
3690 update: function(activated, delta) {
3691 // Moon phase.
3692 if (activated && this.opacity == 0) {
3693 this.currentPhase++;
3694
3695 if (this.currentPhase >= NightMode.phases.length) {
3696 this.currentPhase = 0;
3697 }
3698 }
3699
3700 // Fade in / out.
3701 if (activated && (this.opacity < 1 || this.opacity == 0)) {
3702 this.opacity += NightMode.config.FADE_SPEED;
3703 } else if (this.opacity > 0) {
3704 this.opacity -= NightMode.config.FADE_SPEED;
3705 }
3706
3707 // Set moon positioning.
3708 if (this.opacity > 0) {
3709 this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);
3710
3711 // Update stars.
3712 if (this.drawStars) {
3713 for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
3714 this.stars[i].x = this.updateXPos(this.stars[i].x,
3715 NightMode.config.STAR_SPEED);
3716 }
3717 }
3718 this.draw();
3719 } else {
3720 this.opacity = 0;
3721 this.placeStars();
3722 }
3723 this.drawStars = true;
3724 },
3725
3726 updateXPos: function(currentPos, speed) {
3727 if (currentPos < -NightMode.config.WIDTH) {
3728 currentPos = this.containerWidth;
3729 } else {
3730 currentPos -= speed;
3731 }
3732 return currentPos;
3733 },
3734
3735 draw: function() {
3736 var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :
3737 NightMode.config.WIDTH;
3738 var moonSourceHeight = NightMode.config.HEIGHT;
3739 var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];
3740 var moonOutputWidth = moonSourceWidth;
3741 var starSize = NightMode.config.STAR_SIZE;
3742 var starSourceX = Runner.spriteDefinition.LDPI.STAR.x;
3743
3744 if (IS_HIDPI) {
3745 moonSourceWidth *= 2;
3746 moonSourceHeight *= 2;
3747 moonSourceX = this.spritePos.x +
3748 (NightMode.phases[this.currentPhase] * 2);
3749 starSize *= 2;
3750 starSourceX = Runner.spriteDefinition.HDPI.STAR.x;
3751 }
3752
3753 this.canvasCtx.save();
3754 this.canvasCtx.globalAlpha = this.opacity;
3755
3756 // Stars.
3757 if (this.drawStars) {
3758 for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
3759 this.canvasCtx.drawImage(Runner.imageSprite,
3760 starSourceX, this.stars[i].sourceY, starSize, starSize,
3761 Math.round(this.stars[i].x), this.stars[i].y,
3762 NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE);
3763 }
3764 }
3765
3766 // Moon.
3767 this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX,
3768 this.spritePos.y, moonSourceWidth, moonSourceHeight,
3769 Math.round(this.xPos), this.yPos,
3770 moonOutputWidth, NightMode.config.HEIGHT);
3771
3772 this.canvasCtx.globalAlpha = 1;
3773 this.canvasCtx.restore();
3774 },
3775
3776 // Do star placement.
3777 placeStars: function() {
3778 var segmentSize = Math.round(this.containerWidth /
3779 NightMode.config.NUM_STARS);
3780
3781 for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
3782 this.stars[i] = {};
3783 this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));
3784 this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);
3785
3786 if (IS_HIDPI) {
3787 this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y +
3788 NightMode.config.STAR_SIZE * 2 * i;
3789 } else {
3790 this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +
3791 NightMode.config.STAR_SIZE * i;
3792 }
3793 }
3794 },
3795
3796 reset: function() {
3797 this.currentPhase = 0;
3798 this.opacity = 0;
3799 this.update(false);
3800 }
3801
3802};
3803
3804
3805//******************************************************************************
3806
3807/**
3808 * Horizon Line.
3809 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
3810 * @param {HTMLCanvasElement} canvas
3811 * @param {Object} spritePos Horizon position in sprite.
3812 * @constructor
3813 */
3814function HorizonLine(canvas, spritePos) {
3815 this.spritePos = spritePos;
3816 this.canvas = canvas;
3817 this.canvasCtx = canvas.getContext('2d');
3818 this.sourceDimensions = {};
3819 this.dimensions = HorizonLine.dimensions;
3820 this.sourceXPos = [this.spritePos.x, this.spritePos.x +
3821 this.dimensions.WIDTH];
3822 this.xPos = [];
3823 this.yPos = 0;
3824 this.bumpThreshold = 0.5;
3825
3826 this.setSourceDimensions();
3827 this.draw();
3828};
3829
3830
3831/**
3832 * Horizon line dimensions.
3833 * @enum {number}
3834 */
3835HorizonLine.dimensions = {
3836 WIDTH: 600,
3837 HEIGHT: 12,
3838 YPOS: 127
3839};
3840
3841
3842HorizonLine.prototype = {
3843 /**
3844 * Set the source dimensions of the horizon line.
3845 */
3846 setSourceDimensions: function() {
3847
3848 for (var dimension in HorizonLine.dimensions) {
3849 if (IS_HIDPI) {
3850 if (dimension != 'YPOS') {
3851 this.sourceDimensions[dimension] =
3852 HorizonLine.dimensions[dimension] * 2;
3853 }
3854 } else {
3855 this.sourceDimensions[dimension] =
3856 HorizonLine.dimensions[dimension];
3857 }
3858 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
3859 }
3860
3861 this.xPos = [0, HorizonLine.dimensions.WIDTH];
3862 this.yPos = HorizonLine.dimensions.YPOS;
3863 },
3864
3865 /**
3866 * Return the crop x position of a type.
3867 */
3868 getRandomType: function() {
3869 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
3870 },
3871
3872 /**
3873 * Draw the horizon line.
3874 */
3875 draw: function() {
3876 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
3877 this.spritePos.y,
3878 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
3879 this.xPos[0], this.yPos,
3880 this.dimensions.WIDTH, this.dimensions.HEIGHT);
3881
3882 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
3883 this.spritePos.y,
3884 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
3885 this.xPos[1], this.yPos,
3886 this.dimensions.WIDTH, this.dimensions.HEIGHT);
3887 },
3888
3889 /**
3890 * Update the x position of an indivdual piece of the line.
3891 * @param {number} pos Line position.
3892 * @param {number} increment
3893 */
3894 updateXPos: function(pos, increment) {
3895 var line1 = pos;
3896 var line2 = pos == 0 ? 1 : 0;
3897
3898 this.xPos[line1] -= increment;
3899 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
3900
3901 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
3902 this.xPos[line1] += this.dimensions.WIDTH * 2;
3903 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
3904 this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
3905 }
3906 },
3907
3908 /**
3909 * Update the horizon line.
3910 * @param {number} deltaTime
3911 * @param {number} speed
3912 */
3913 update: function(deltaTime, speed) {
3914 var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
3915
3916 if (this.xPos[0] <= 0) {
3917 this.updateXPos(0, increment);
3918 } else {
3919 this.updateXPos(1, increment);
3920 }
3921 this.draw();
3922 },
3923
3924 /**
3925 * Reset horizon to the starting position.
3926 */
3927 reset: function() {
3928 this.xPos[0] = 0;
3929 this.xPos[1] = HorizonLine.dimensions.WIDTH;
3930 }
3931};
3932
3933
3934//******************************************************************************
3935
3936/**
3937 * Horizon background class.
3938 * @param {HTMLCanvasElement} canvas
3939 * @param {Object} spritePos Sprite positioning.
3940 * @param {Object} dimensions Canvas dimensions.
3941 * @param {number} gapCoefficient
3942 * @constructor
3943 */
3944function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
3945 this.canvas = canvas;
3946 this.canvasCtx = this.canvas.getContext('2d');
3947 this.config = Horizon.config;
3948 this.dimensions = dimensions;
3949 this.gapCoefficient = gapCoefficient;
3950 this.obstacles = [];
3951 this.obstacleHistory = [];
3952 this.horizonOffsets = [0, 0];
3953 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
3954 this.spritePos = spritePos;
3955 this.nightMode = null;
3956
3957 // Cloud
3958 this.clouds = [];
3959 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
3960
3961 // Horizon
3962 this.horizonLine = null;
3963 this.init();
3964};
3965
3966
3967/**
3968 * Horizon config.
3969 * @enum {number}
3970 */
3971Horizon.config = {
3972 BG_CLOUD_SPEED: 0.2,
3973 BUMPY_THRESHOLD: .3,
3974 CLOUD_FREQUENCY: .5,
3975 HORIZON_HEIGHT: 16,
3976 MAX_CLOUDS: 6
3977};
3978
3979
3980Horizon.prototype = {
3981 /**
3982 * Initialise the horizon. Just add the line and a cloud. No obstacles.
3983 */
3984 init: function() {
3985 this.addCloud();
3986 this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
3987 this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
3988 this.dimensions.WIDTH);
3989 },
3990
3991 /**
3992 * @param {number} deltaTime
3993 * @param {number} currentSpeed
3994 * @param {boolean} updateObstacles Used as an override to prevent
3995 * the obstacles from being updated / added. This happens in the
3996 * ease in section.
3997 * @param {boolean} showNightMode Night mode activated.
3998 */
3999 update: function(deltaTime, currentSpeed, updateObstacles, showNightMode) {
4000 this.runningTime += deltaTime;
4001 this.horizonLine.update(deltaTime, currentSpeed);
4002 this.nightMode.update(showNightMode);
4003 this.updateClouds(deltaTime, currentSpeed);
4004
4005 if (updateObstacles) {
4006 this.updateObstacles(deltaTime, currentSpeed);
4007 }
4008 },
4009
4010 /**
4011 * Update the cloud positions.
4012 * @param {number} deltaTime
4013 * @param {number} currentSpeed
4014 */
4015 updateClouds: function(deltaTime, speed) {
4016 var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
4017 var numClouds = this.clouds.length;
4018
4019 if (numClouds) {
4020 for (var i = numClouds - 1; i >= 0; i--) {
4021 this.clouds[i].update(cloudSpeed);
4022 }
4023
4024 var lastCloud = this.clouds[numClouds - 1];
4025
4026 // Check for adding a new cloud.
4027 if (numClouds < this.config.MAX_CLOUDS &&
4028 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
4029 this.cloudFrequency > Math.random()) {
4030 this.addCloud();
4031 }
4032
4033 // Remove expired clouds.
4034 this.clouds = this.clouds.filter(function(obj) {
4035 return !obj.remove;
4036 });
4037 } else {
4038 this.addCloud();
4039 }
4040 },
4041
4042 /**
4043 * Update the obstacle positions.
4044 * @param {number} deltaTime
4045 * @param {number} currentSpeed
4046 */
4047 updateObstacles: function(deltaTime, currentSpeed) {
4048 // Obstacles, move to Horizon layer.
4049 var updatedObstacles = this.obstacles.slice(0);
4050
4051 for (var i = 0; i < this.obstacles.length; i++) {
4052 var obstacle = this.obstacles[i];
4053 obstacle.update(deltaTime, currentSpeed);
4054
4055 // Clean up existing obstacles.
4056 if (obstacle.remove) {
4057 updatedObstacles.shift();
4058 }
4059 }
4060 this.obstacles = updatedObstacles;
4061
4062 if (this.obstacles.length > 0) {
4063 var lastObstacle = this.obstacles[this.obstacles.length - 1];
4064
4065 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
4066 lastObstacle.isVisible() &&
4067 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
4068 this.dimensions.WIDTH) {
4069 this.addNewObstacle(currentSpeed);
4070 lastObstacle.followingObstacleCreated = true;
4071 }
4072 } else {
4073 // Create new obstacles.
4074 this.addNewObstacle(currentSpeed);
4075 }
4076 },
4077
4078 removeFirstObstacle: function() {
4079 this.obstacles.shift();
4080 },
4081
4082 /**
4083 * Add a new obstacle.
4084 * @param {number} currentSpeed
4085 */
4086 addNewObstacle: function(currentSpeed) {
4087 var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
4088 var obstacleType = Obstacle.types[obstacleTypeIndex];
4089
4090 // Check for multiples of the same type of obstacle.
4091 // Also check obstacle is available at current speed.
4092 if (this.duplicateObstacleCheck(obstacleType.type) ||
4093 currentSpeed < obstacleType.minSpeed) {
4094 this.addNewObstacle(currentSpeed);
4095 } else {
4096 var obstacleSpritePos = this.spritePos[obstacleType.type];
4097
4098 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
4099 obstacleSpritePos, this.dimensions,
4100 this.gapCoefficient, currentSpeed, obstacleType.width));
4101
4102 this.obstacleHistory.unshift(obstacleType.type);
4103
4104 if (this.obstacleHistory.length > 1) {
4105 this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
4106 }
4107 }
4108 },
4109
4110 /**
4111 * Returns whether the previous two obstacles are the same as the next one.
4112 * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
4113 * @return {boolean}
4114 */
4115 duplicateObstacleCheck: function(nextObstacleType) {
4116 var duplicateCount = 0;
4117
4118 for (var i = 0; i < this.obstacleHistory.length; i++) {
4119 duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
4120 duplicateCount + 1 : 0;
4121 }
4122 return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
4123 },
4124
4125 /**
4126 * Reset the horizon layer.
4127 * Remove existing obstacles and reposition the horizon line.
4128 */
4129 reset: function() {
4130 this.obstacles = [];
4131 this.horizonLine.reset();
4132 this.nightMode.reset();
4133 },
4134
4135 /**
4136 * Update the canvas width and scaling.
4137 * @param {number} width Canvas width.
4138 * @param {number} height Canvas height.
4139 */
4140 resize: function(width, height) {
4141 this.canvas.width = width;
4142 this.canvas.height = height;
4143 },
4144
4145 /**
4146 * Add a new cloud to the horizon.
4147 */
4148 addCloud: function() {
4149 this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
4150 this.dimensions.WIDTH));
4151 }
4152};
4153})();
4154</script>
4155</head>
4156<body id="t" style="font-family: system-ui, sans-serif; font-size: 75%" jstcache="0" class="neterror">
4157 <div id="main-frame-error" class="interstitial-wrapper" jstcache="0">
4158 <div id="main-content" jstcache="0">
4159 <div class="icon icon-generic" jseval="updateIconClass(this.classList, iconClass)" alt="" jstcache="1"></div>
4160 <div id="main-message" jstcache="0">
4161 <h1 jsselect="heading" jsvalues=".innerHTML:msg" jstcache="5">Your file was not found</h1>
4162 <p jsselect="summary" jsvalues=".innerHTML:msg" jstcache="2">It may have been moved or deleted.</p>
4163 <div id="suggestions-list" style="display:none" jsdisplay="(suggestionsSummaryList && suggestionsSummaryList.length)" jstcache="6">
4164 <p jsvalues=".innerHTML:suggestionsSummaryListHeader" jstcache="13"></p>
4165 <ul jsvalues=".className:suggestionsSummaryList.length == 1 ? 'single-suggestion' : ''" jstcache="14">
4166 <li jsselect="suggestionsSummaryList" jsvalues=".innerHTML:summary" jstcache="15"></li>
4167 </ul>
4168 </div>
4169 <div class="error-code" jscontent="errorCode" jstcache="7">ERR_FILE_NOT_FOUND</div>
4170 <div id="diagnose-frame" class="hidden" jstcache="0"></div>
4171 </div>
4172 </div>
4173 <div id="buttons" class="nav-wrapper suggested-right" jstcache="0">
4174 <div id="control-buttons" hidden="" jstcache="0">
4175
4176 <button id="reload-button" class="blue-button text-button" onclick="trackClick(this.trackingId);
4177 reloadButtonClick(this.url);" jsselect="reloadButton" jsvalues=".url:reloadUrl; .trackingId:reloadTrackingId" jscontent="msg" jstcache="8" style="display: none;"></button><button id="show-saved-copy-button" class="blue-button text-button" onclick="showSavedCopyButtonClick()" jsselect="showSavedCopyButton" jscontent="msg" jsvalues="title:title; .primary:primary" jstcache="9" style="display: none;">
4178 </button>
4179 <button id="download-button" class="blue-button text-button" onclick="downloadButtonClick()" jsselect="downloadButton" jscontent="msg" jsvalues=".disabledText:disabledMsg" jstcache="10" style="display: none;">
4180 </button>
4181 </div>
4182 <button id="details-button" class="text-button small-link singular" onclick="detailsButtonClick(); toggleHelpBox()" jscontent="details" jsdisplay="(suggestionsDetails && suggestionsDetails.length > 0) || diagnose" jsvalues=".detailsText:details; .hideDetailsText:hideDetails;" jstcache="3" style="display: none;"></button>
4183 </div>
4184 <div id="details" class="hidden" jstcache="0">
4185 <div class="suggestions" jsselect="suggestionsDetails" jstcache="4" jsinstance="*0" style="display: none;">
4186 <div class="suggestion-header" jsvalues=".innerHTML:header" jstcache="11"></div>
4187 <div class="suggestion-body" jsvalues=".innerHTML:body" jstcache="12"></div>
4188 </div>
4189 </div>
4190 </div>
4191 <div id="sub-frame-error" jstcache="0">
4192 <!-- Show details when hovering over the icon, in case the details are
4193 hidden because they're too large. -->
4194 <div class="icon icon-generic" jseval="updateIconClass(this.classList, iconClass)" jstcache="1"></div>
4195 <div id="sub-frame-error-details" jsselect="summary" jsvalues=".innerHTML:msg" jstcache="2">It may have been moved or deleted.</div>
4196 </div>
4197
4198 <div id="offline-resources" jstcache="0">
4199 <img id="offline-resources-1x" src="data:image/png;base64,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" jstcache="0">
4200 <img id="offline-resources-2x" src="data:image/png;base64,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" jstcache="0">
4201 <template id="audio-resources" jstcache="0">
4202 <audio id="offline-sound-press" src="data:audio/mpeg;base64,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" i18n-processed=""></audio>
4203 <audio id="offline-sound-hit" src="data:audio/mpeg;base64,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" i18n-processed=""></audio>
4204 <audio id="offline-sound-reached" src="data:audio/mpeg;base64,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" i18n-processed=""></audio>
4205 </template>
4206 </div>
4207
4208
4209<script jstcache="0">// Copyright (c) 2012 The Chromium Authors. All rights reserved.
4210// Use of this source code is governed by a BSD-style license that can be
4211// found in the LICENSE file.
4212
4213/**
4214 * @fileoverview This file defines a singleton which provides access to all data
4215 * that is available as soon as the page's resources are loaded (before DOM
4216 * content has finished loading). This data includes both localized strings and
4217 * any data that is important to have ready from a very early stage (e.g. things
4218 * that must be displayed right away).
4219 *
4220 * Note that loadTimeData is not guaranteed to be consistent between page
4221 * refreshes (https://crbug.com/740629) and should not contain values that might
4222 * change if the page is re-opened later.
4223 */
4224
4225/**
4226 * @typedef {{
4227 * substitutions: (Array<string>|undefined),
4228 * attrs: (Object<function(Node, string):boolean>|undefined),
4229 * tags: (Array<string>|undefined),
4230 * }}
4231 */
4232var SanitizeInnerHtmlOpts;
4233
4234/** @type {!LoadTimeData} */ var loadTimeData;
4235
4236// Expose this type globally as a temporary work around until
4237// https://github.com/google/closure-compiler/issues/544 is fixed.
4238/** @constructor */
4239function LoadTimeData(){}
4240
4241(function() {
4242 'use strict';
4243
4244 LoadTimeData.prototype = {
4245 /**
4246 * Sets the backing object.
4247 *
4248 * Note that there is no getter for |data_| to discourage abuse of the form:
4249 *
4250 * var value = loadTimeData.data()['key'];
4251 *
4252 * @param {Object} value The de-serialized page data.
4253 */
4254 set data(value) {
4255 expect(!this.data_, 'Re-setting data.');
4256 this.data_ = value;
4257 },
4258
4259 /**
4260 * Returns a JsEvalContext for |data_|.
4261 * @returns {JsEvalContext}
4262 */
4263 createJsEvalContext: function() {
4264 return new JsEvalContext(this.data_);
4265 },
4266
4267 /**
4268 * @param {string} id An ID of a value that might exist.
4269 * @return {boolean} True if |id| is a key in the dictionary.
4270 */
4271 valueExists: function(id) {
4272 return id in this.data_;
4273 },
4274
4275 /**
4276 * Fetches a value, expecting that it exists.
4277 * @param {string} id The key that identifies the desired value.
4278 * @return {*} The corresponding value.
4279 */
4280 getValue: function(id) {
4281 expect(this.data_, 'No data. Did you remember to include strings.js?');
4282 var value = this.data_[id];
4283 expect(typeof value != 'undefined', 'Could not find value for ' + id);
4284 return value;
4285 },
4286
4287 /**
4288 * As above, but also makes sure that the value is a string.
4289 * @param {string} id The key that identifies the desired string.
4290 * @return {string} The corresponding string value.
4291 */
4292 getString: function(id) {
4293 var value = this.getValue(id);
4294 expectIsType(id, value, 'string');
4295 return /** @type {string} */ (value);
4296 },
4297
4298 /**
4299 * Returns a formatted localized string where $1 to $9 are replaced by the
4300 * second to the tenth argument.
4301 * @param {string} id The ID of the string we want.
4302 * @param {...(string|number)} var_args The extra values to include in the
4303 * formatted output.
4304 * @return {string} The formatted string.
4305 */
4306 getStringF: function(id, var_args) {
4307 var value = this.getString(id);
4308 if (!value)
4309 return '';
4310
4311 var args = Array.prototype.slice.call(arguments);
4312 args[0] = value;
4313 return this.substituteString.apply(this, args);
4314 },
4315
4316 /**
4317 * Make a string safe for use with with Polymer bindings that are
4318 * inner-h-t-m-l (or other innerHTML use).
4319 * @param {string} rawString The unsanitized string.
4320 * @param {SanitizeInnerHtmlOpts=} opts Optional additional allowed tags and
4321 * attributes.
4322 * @return {string}
4323 */
4324 sanitizeInnerHtml: function(rawString, opts) {
4325 opts = opts || {};
4326 return parseHtmlSubset('<b>' + rawString + '</b>', opts.tags, opts.attrs)
4327 .firstChild.innerHTML;
4328 },
4329
4330 /**
4331 * Returns a formatted localized string where $1 to $9 are replaced by the
4332 * second to the tenth argument. Any standalone $ signs must be escaped as
4333 * $$.
4334 * @param {string} label The label to substitute through.
4335 * This is not an resource ID.
4336 * @param {...(string|number)} var_args The extra values to include in the
4337 * formatted output.
4338 * @return {string} The formatted string.
4339 */
4340 substituteString: function(label, var_args) {
4341 var varArgs = arguments;
4342 return label.replace(/\$(.|$|\n)/g, function(m) {
4343 assert(m.match(/\$[$1-9]/), 'Unescaped $ found in localized string.');
4344 return m == '$$' ? '$' : varArgs[m[1]];
4345 });
4346 },
4347
4348 /**
4349 * Returns a formatted string where $1 to $9 are replaced by the second to
4350 * tenth argument, split apart into a list of pieces describing how the
4351 * substitution was performed. Any standalone $ signs must be escaped as $$.
4352 * @param {string} label A localized string to substitute through.
4353 * This is not an resource ID.
4354 * @param {...(string|number)} var_args The extra values to include in the
4355 * formatted output.
4356 * @return {!Array<!{value: string, arg: (null|string)}>} The formatted
4357 * string pieces.
4358 */
4359 getSubstitutedStringPieces: function(label, var_args) {
4360 var varArgs = arguments;
4361 // Split the string by separately matching all occurrences of $1-9 and of
4362 // non $1-9 pieces.
4363 var pieces = (label.match(/(\$[1-9])|(([^$]|\$([^1-9]|$))+)/g) ||
4364 []).map(function(p) {
4365 // Pieces that are not $1-9 should be returned after replacing $$
4366 // with $.
4367 if (!p.match(/^\$[1-9]$/)) {
4368 assert(
4369 (p.match(/\$/g) || []).length % 2 == 0,
4370 'Unescaped $ found in localized string.');
4371 return {value: p.replace(/\$\$/g, '$'), arg: null};
4372 }
4373
4374 // Otherwise, return the substitution value.
4375 return {value: varArgs[p[1]], arg: p};
4376 });
4377
4378 return pieces;
4379 },
4380
4381 /**
4382 * As above, but also makes sure that the value is a boolean.
4383 * @param {string} id The key that identifies the desired boolean.
4384 * @return {boolean} The corresponding boolean value.
4385 */
4386 getBoolean: function(id) {
4387 var value = this.getValue(id);
4388 expectIsType(id, value, 'boolean');
4389 return /** @type {boolean} */ (value);
4390 },
4391
4392 /**
4393 * As above, but also makes sure that the value is an integer.
4394 * @param {string} id The key that identifies the desired number.
4395 * @return {number} The corresponding number value.
4396 */
4397 getInteger: function(id) {
4398 var value = this.getValue(id);
4399 expectIsType(id, value, 'number');
4400 expect(value == Math.floor(value), 'Number isn\'t integer: ' + value);
4401 return /** @type {number} */ (value);
4402 },
4403
4404 /**
4405 * Override values in loadTimeData with the values found in |replacements|.
4406 * @param {Object} replacements The dictionary object of keys to replace.
4407 */
4408 overrideValues: function(replacements) {
4409 expect(
4410 typeof replacements == 'object',
4411 'Replacements must be a dictionary object.');
4412 for (var key in replacements) {
4413 this.data_[key] = replacements[key];
4414 }
4415 }
4416 };
4417
4418 /**
4419 * Checks condition, displays error message if expectation fails.
4420 * @param {*} condition The condition to check for truthiness.
4421 * @param {string} message The message to display if the check fails.
4422 */
4423 function expect(condition, message) {
4424 if (!condition) {
4425 console.error(
4426 'Unexpected condition on ' + document.location.href + ': ' + message);
4427 }
4428 }
4429
4430 /**
4431 * Checks that the given value has the given type.
4432 * @param {string} id The id of the value (only used for error message).
4433 * @param {*} value The value to check the type on.
4434 * @param {string} type The type we expect |value| to be.
4435 */
4436 function expectIsType(id, value, type) {
4437 expect(
4438 typeof value == type, '[' + value + '] (' + id + ') is not a ' + type);
4439 }
4440
4441 expect(!loadTimeData, 'should only include this file once');
4442 loadTimeData = new LoadTimeData;
4443})();
4444</script><script jstcache="0">loadTimeData.data = {"details":"Details","errorCode":"ERR_FILE_NOT_FOUND","fontfamily":"system-ui, sans-serif","fontsize":"75%","heading":{"hostName":"flejfacjooompmliegamfbpjjdlhokhj","msg":"Your file was not found"},"hideDetails":"Hide details","iconClass":"icon-generic","language":"en","suggestionsDetails":[],"suggestionsSummaryList":[],"summary":{"failedUrl":"chrome-extension://flejfacjooompmliegamfbpjjdlhokhj/devices.html","hostName":"flejfacjooompmliegamfbpjjdlhokhj","msg":"It may have been moved or deleted."},"textdirection":"ltr","title":"chrome-extension://flejfacjooompmliegamfbpjjdlhokhj/devices.html"};</script><script jstcache="0">// Copyright (c) 2012 The Chromium Authors. All rights reserved.
4445// Use of this source code is governed by a BSD-style license that can be
4446// found in the LICENSE file.
4447
4448// Note: vulcanize sometimes disables GRIT processing. If you're importing i18n
4449// stuff with <link rel="import">, you should probably be using
4450// html/i18n_template.html instead of this file.
4451
4452// // Copyright (c) 2012 The Chromium Authors. All rights reserved.
4453// Use of this source code is governed by a BSD-style license that can be
4454// found in the LICENSE file.
4455
4456/** @typedef {Document|DocumentFragment|Element} */
4457var ProcessingRoot;
4458
4459/**
4460 * @fileoverview This is a simple template engine inspired by JsTemplates
4461 * optimized for i18n.
4462 *
4463 * It currently supports three handlers:
4464 *
4465 * * i18n-content which sets the textContent of the element.
4466 *
4467 * <span i18n-content="myContent"></span>
4468 *
4469 * * i18n-options which generates <option> elements for a <select>.
4470 *
4471 * <select i18n-options="myOptionList"></select>
4472 *
4473 * * i18n-values is a list of attribute-value or property-value pairs.
4474 * Properties are prefixed with a '.' and can contain nested properties.
4475 *
4476 * <span i18n-values="title:myTitle;.style.fontSize:fontSize"></span>
4477 *
4478 * This file is a copy of i18n_template.js, with minor tweaks to support using
4479 * load_time_data.js. It should replace i18n_template.js eventually.
4480 */
4481
4482var i18nTemplate = (function() {
4483 /**
4484 * This provides the handlers for the templating engine. The key is used as
4485 * the attribute name and the value is the function that gets called for every
4486 * single node that has this attribute.
4487 * @type {!Object}
4488 */
4489 var handlers = {
4490 /**
4491 * This handler sets the textContent of the element.
4492 * @param {!HTMLElement} element The node to modify.
4493 * @param {string} key The name of the value in |data|.
4494 * @param {!LoadTimeData} data The data source to draw from.
4495 * @param {!Set<ProcessingRoot>} visited
4496 */
4497 'i18n-content': function(element, key, data, visited) {
4498 element.textContent = data.getString(key);
4499 },
4500
4501 /**
4502 * This handler adds options to a <select> element.
4503 * @param {!HTMLElement} select The node to modify.
4504 * @param {string} key The name of the value in |data|. It should
4505 * identify an array of values to initialize an <option>. Each value,
4506 * if a pair, represents [content, value]. Otherwise, it should be a
4507 * content string with no value.
4508 * @param {!LoadTimeData} data The data source to draw from.
4509 * @param {!Set<ProcessingRoot>} visited
4510 */
4511 'i18n-options': function(select, key, data, visited) {
4512 var options = data.getValue(key);
4513 options.forEach(function(optionData) {
4514 var option = typeof optionData == 'string' ?
4515 new Option(optionData) :
4516 new Option(optionData[1], optionData[0]);
4517 select.appendChild(option);
4518 });
4519 },
4520
4521 /**
4522 * This is used to set HTML attributes and DOM properties. The syntax is:
4523 * attributename:key;
4524 * .domProperty:key;
4525 * .nested.dom.property:key
4526 * @param {!HTMLElement} element The node to modify.
4527 * @param {string} attributeAndKeys The path of the attribute to modify
4528 * followed by a colon, and the name of the value in |data|.
4529 * Multiple attribute/key pairs may be separated by semicolons.
4530 * @param {!LoadTimeData} data The data source to draw from.
4531 * @param {!Set<ProcessingRoot>} visited
4532 */
4533 'i18n-values': function(element, attributeAndKeys, data, visited) {
4534 var parts = attributeAndKeys.replace(/\s/g, '').split(/;/);
4535 parts.forEach(function(part) {
4536 if (!part)
4537 return;
4538
4539 var attributeAndKeyPair = part.match(/^([^:]+):(.+)$/);
4540 if (!attributeAndKeyPair)
4541 throw new Error('malformed i18n-values: ' + attributeAndKeys);
4542
4543 var propName = attributeAndKeyPair[1];
4544 var propExpr = attributeAndKeyPair[2];
4545
4546 var value = data.getValue(propExpr);
4547
4548 // Allow a property of the form '.foo.bar' to assign a value into
4549 // element.foo.bar.
4550 if (propName[0] == '.') {
4551 var path = propName.slice(1).split('.');
4552 var targetObject = element;
4553 while (targetObject && path.length > 1) {
4554 targetObject = targetObject[path.shift()];
4555 }
4556 if (targetObject) {
4557 targetObject[path] = value;
4558 // In case we set innerHTML (ignoring others) we need to recursively
4559 // check the content.
4560 if (path == 'innerHTML') {
4561 for (var i = 0; i < element.children.length; ++i) {
4562 processWithoutCycles(element.children[i], data, visited, false);
4563 }
4564 }
4565 }
4566 } else {
4567 element.setAttribute(propName, /** @type {string} */ (value));
4568 }
4569 });
4570 }
4571 };
4572
4573 var prefixes = [''];
4574
4575 // Only look through shadow DOM when it's supported. As of April 2015, iOS
4576 // Chrome doesn't support shadow DOM.
4577 if (Element.prototype.createShadowRoot)
4578 prefixes.push('* /deep/ ');
4579
4580 var attributeNames = Object.keys(handlers);
4581 var selector = prefixes
4582 .map(function(prefix) {
4583 return prefix + '[' +
4584 attributeNames.join('], ' + prefix + '[') + ']';
4585 })
4586 .join(', ');
4587
4588 /**
4589 * Processes a DOM tree using a |data| source to populate template values.
4590 * @param {!ProcessingRoot} root The root of the DOM tree to process.
4591 * @param {!LoadTimeData} data The data to draw from.
4592 */
4593 function process(root, data) {
4594 processWithoutCycles(root, data, new Set(), true);
4595 }
4596
4597 /**
4598 * Internal process() method that stops cycles while processing.
4599 * @param {!ProcessingRoot} root
4600 * @param {!LoadTimeData} data
4601 * @param {!Set<ProcessingRoot>} visited Already visited roots.
4602 * @param {boolean} mark Whether nodes should be marked processed.
4603 */
4604 function processWithoutCycles(root, data, visited, mark) {
4605 if (visited.has(root)) {
4606 // Found a cycle. Stop it.
4607 return;
4608 }
4609
4610 // Mark the node as visited before recursing.
4611 visited.add(root);
4612
4613 var importLinks = root.querySelectorAll('link[rel=import]');
4614 for (var i = 0; i < importLinks.length; ++i) {
4615 var importLink = /** @type {!HTMLLinkElement} */ (importLinks[i]);
4616 if (!importLink.import) {
4617 // Happens when a <link rel=import> is inside a <template>.
4618 // TODO(dbeam): should we log an error if we detect that here?
4619 continue;
4620 }
4621 processWithoutCycles(importLink.import, data, visited, mark);
4622 }
4623
4624 var templates = root.querySelectorAll('template');
4625 for (var i = 0; i < templates.length; ++i) {
4626 var template = /** @type {HTMLTemplateElement} */ (templates[i]);
4627 if (!template.content)
4628 continue;
4629 processWithoutCycles(template.content, data, visited, mark);
4630 }
4631
4632 var isElement = root instanceof Element;
4633 if (isElement && root.webkitMatchesSelector(selector))
4634 processElement(/** @type {!Element} */ (root), data, visited);
4635
4636 var elements = root.querySelectorAll(selector);
4637 for (var i = 0; i < elements.length; ++i) {
4638 processElement(elements[i], data, visited);
4639 }
4640
4641 if (mark) {
4642 var processed = isElement ? [root] : root.children;
4643 if (processed) {
4644 for (var i = 0; i < processed.length; ++i) {
4645 processed[i].setAttribute('i18n-processed', '');
4646 }
4647 }
4648 }
4649 }
4650
4651 /**
4652 * Run through various [i18n-*] attributes and populate.
4653 * @param {!Element} element
4654 * @param {!LoadTimeData} data
4655 * @param {!Set<ProcessingRoot>} visited
4656 */
4657 function processElement(element, data, visited) {
4658 for (var i = 0; i < attributeNames.length; i++) {
4659 var name = attributeNames[i];
4660 var attribute = element.getAttribute(name);
4661 if (attribute != null)
4662 handlers[name](element, attribute, data, visited);
4663 }
4664 }
4665
4666 return {process: process};
4667}());
4668
4669// // Copyright 2017 The Chromium Authors. All rights reserved.
4670// Use of this source code is governed by a BSD-style license that can be
4671// found in the LICENSE file.
4672
4673i18nTemplate.process(document, loadTimeData);
4674
4675</script><script jstcache="0">// Copyright (c) 2012 The Chromium Authors. All rights reserved.
4676// Use of this source code is governed by a BSD-style license that can be
4677// found in the LICENSE file.
4678
4679// This file serves as a proxy to bring the included js file from /third_party
4680// into its correct location under the resources directory tree, whence it is
4681// delivered via a chrome://resources URL. See ../webui_resources.grd.
4682
4683// Note: this <include> is not behind a single-line comment because the first
4684// line of the file is source code (so the first line would be skipped) instead
4685// of a licence header.
4686// clang-format off
4687(function(){var i=null;function k(){return Function.prototype.call.apply(Array.prototype.slice,arguments)}function l(a,b){var c=k(arguments,2);return function(){return b.apply(a,c)}}function m(a,b){var c=new n(b);for(c.f=[a];c.f.length;){var e=c,d=c.f.shift();e.g(d);for(d=d.firstChild;d;d=d.nextSibling)d.nodeType==1&&e.f.push(d)}}function n(a){this.g=a}function o(a){a.style.display=""}function p(a){a.style.display="none"};var q=":",r=/\s*;\s*/;function s(){this.i.apply(this,arguments)}s.prototype.i=function(a,b){if(!this.a)this.a={};if(b){var c=this.a,e=b.a,d;for(d in e)c[d]=e[d]}else for(c in d=this.a,e=t,e)d[c]=e[c];this.a.$this=a;this.a.$context=this;this.d=typeof a!="undefined"&&a!=i?a:"";if(!b)this.a.$top=this.d};var t={$default:i},u=[];function v(a){for(var b in a.a)delete a.a[b];a.d=i;u.push(a)}function w(a,b,c){try{return b.call(c,a.a,a.d)}catch(e){return t.$default}}
4688function x(a,b,c,e){if(u.length>0){var d=u.pop();s.call(d,b,a);a=d}else a=new s(b,a);a.a.$index=c;a.a.$count=e;return a}var y="a_",z="b_",A="with (a_) with (b_) return ",D={};function E(a){if(!D[a])try{D[a]=new Function(y,z,A+a)}catch(b){}return D[a]}function F(a){for(var b=[],a=a.split(r),c=0,e=a.length;c<e;++c){var d=a[c].indexOf(q);if(!(d<0)){var f;f=a[c].substr(0,d).replace(/^\s+/,"").replace(/\s+$/,"");d=E(a[c].substr(d+1));b.push(f,d)}}return b};var G="jsinstance",H="jsts",I="*",J="div",K="id";function L(){}var M=0,N={0:{}},P={},Q={},R=[];function S(a){a.__jstcache||m(a,function(a){T(a)})}var U=[["jsselect",E],["jsdisplay",E],["jsvalues",F],["jsvars",F],["jseval",function(a){for(var b=[],a=a.split(r),c=0,e=a.length;c<e;++c)if(a[c]){var d=E(a[c]);b.push(d)}return b}],["transclude",function(a){return a}],["jscontent",E],["jsskip",E]];
4689function T(a){if(a.__jstcache)return a.__jstcache;var b=a.getAttribute("jstcache");if(b!=i)return a.__jstcache=N[b];for(var b=R.length=0,c=U.length;b<c;++b){var e=U[b][0],d=a.getAttribute(e);Q[e]=d;d!=i&&R.push(e+"="+d)}if(R.length==0)return a.setAttribute("jstcache","0"),a.__jstcache=N[0];var f=R.join("&");if(b=P[f])return a.setAttribute("jstcache",b),a.__jstcache=N[b];for(var h={},b=0,c=U.length;b<c;++b){var d=U[b],e=d[0],g=d[1],d=Q[e];d!=i&&(h[e]=g(d))}b=""+ ++M;a.setAttribute("jstcache",b);N[b]=
4690h;P[f]=b;return a.__jstcache=h}function V(a,b){a.h.push(b);a.k.push(0)}function W(a){return a.c.length?a.c.pop():[]}
4691L.prototype.e=function(a,b){var c=X(b),e=c.transclude;if(e)(c=Y(e))?(b.parentNode.replaceChild(c,b),e=W(this),e.push(this.e,a,c),V(this,e)):b.parentNode.removeChild(b);else if(c=c.jsselect){var c=w(a,c,b),d=b.getAttribute(G),f=!1;d&&(d.charAt(0)==I?(d=parseInt(d.substr(1),10),f=!0):d=parseInt(d,10));var h=c!=i&&typeof c=="object"&&typeof c.length=="number",e=h?c.length:1,g=h&&e==0;if(h)if(g)d?b.parentNode.removeChild(b):(b.setAttribute(G,"*0"),p(b));else if(o(b),d===i||d===""||f&&d<e-1){f=W(this);
4692d=d||0;for(h=e-1;d<h;++d){var j=b.cloneNode(!0);b.parentNode.insertBefore(j,b);Z(j,c,d);g=x(a,c[d],d,e);f.push(this.b,g,j,v,g,i)}Z(b,c,d);g=x(a,c[d],d,e);f.push(this.b,g,b,v,g,i);V(this,f)}else d<e?(f=c[d],Z(b,c,d),g=x(a,f,d,e),f=W(this),f.push(this.b,g,b,v,g,i),V(this,f)):b.parentNode.removeChild(b);else c==i?p(b):(o(b),g=x(a,c,0,1),f=W(this),f.push(this.b,g,b,v,g,i),V(this,f))}else this.b(a,b)};
4693L.prototype.b=function(a,b){var c=X(b),e=c.jsdisplay;if(e){if(!w(a,e,b)){p(b);return}o(b)}if(e=c.jsvars)for(var d=0,f=e.length;d<f;d+=2){var h=e[d],g=w(a,e[d+1],b);a.a[h]=g}if(e=c.jsvalues){d=0;for(f=e.length;d<f;d+=2)if(g=e[d],h=w(a,e[d+1],b),g.charAt(0)=="$")a.a[g]=h;else if(g.charAt(0)=="."){for(var g=g.substr(1).split("."),j=b,O=g.length,B=0,$=O-1;B<$;++B){var C=g[B];j[C]||(j[C]={});j=j[C]}j[g[O-1]]=h}else g&&(typeof h=="boolean"?h?b.setAttribute(g,g):b.removeAttribute(g):b.setAttribute(g,""+
4694h))}if(e=c.jseval){d=0;for(f=e.length;d<f;++d)w(a,e[d],b)}e=c.jsskip;if(!e||!w(a,e,b))if(c=c.jscontent){if(c=""+w(a,c,b),b.innerHTML!=c){for(;b.firstChild;)e=b.firstChild,e.parentNode.removeChild(e);b.appendChild(this.j.createTextNode(c))}}else{c=W(this);for(e=b.firstChild;e;e=e.nextSibling)e.nodeType==1&&c.push(this.e,a,e);c.length&&V(this,c)}};function X(a){if(a.__jstcache)return a.__jstcache;var b=a.getAttribute("jstcache");if(b)return a.__jstcache=N[b];return T(a)}
4695function Y(a,b){var c=document;if(b){var e=c.getElementById(a);if(!e){var e=b(),d=H,f=c.getElementById(d);if(!f)f=c.createElement(J),f.id=d,p(f),f.style.position="absolute",c.body.appendChild(f);d=c.createElement(J);f.appendChild(d);d.innerHTML=e;e=c.getElementById(a)}c=e}else c=c.getElementById(a);return c?(S(c),c=c.cloneNode(!0),c.removeAttribute(K),c):i}function Z(a,b,c){c==b.length-1?a.setAttribute(G,I+c):a.setAttribute(G,""+c)};window.jstGetTemplate=Y;window.JsEvalContext=s;window.jstProcess=function(a,b){var c=new L;S(b);c.j=b?b.nodeType==9?b:b.ownerDocument||document:document;var e=l(c,c.e,a,b),d=c.h=[],f=c.k=[];c.c=[];e();for(var h,g,j;d.length;)h=d[d.length-1],e=f[f.length-1],e>=h.length?(e=c,g=d.pop(),g.length=0,e.c.push(g),f.pop()):(g=h[e++],j=h[e++],h=h[e++],f[f.length-1]=e,g.call(c,j,h))};
4696})()
4697</script><script jstcache="0">var tp = document.getElementById('t');jstProcess(loadTimeData.createJsEvalContext(), tp);</script></body></html>