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