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