· 5 years ago · Sep 22, 2020, 02:24 PM
1<!DOCTYPE html>
2<!-- saved from url=(0029)chrome-error://chromewebdata/ -->
3<html dir="ltr" lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
4
5 <meta name="theme-color" content="#fff">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0,
7 maximum-scale=1.0, user-scalable=no">
8 <title>brontoforum.us</title>
9 <style>/* Copyright 2017 The Chromium Authors. All rights reserved.
10 * Use of this source code is governed by a BSD-style license that can be
11 * found in the LICENSE file. */
12
13a {
14 color: var(--link-color);
15}
16
17body {
18 --background-color: #fff;
19 --error-code-color: var(--google-gray-700);
20 --google-blue-100: rgb(210, 227, 252);
21 --google-blue-300: rgb(138, 180, 248);
22 --google-blue-600: rgb(26, 115, 232);
23 --google-blue-700: rgb(25, 103, 210);
24 --google-gray-100: rgb(241, 243, 244);
25 --google-gray-300: rgb(218, 220, 224);
26 --google-gray-500: rgb(154, 160, 166);
27 --google-gray-50: rgb(248, 249, 250);
28 --google-gray-600: rgb(128, 134, 139);
29 --google-gray-700: rgb(95, 99, 104);
30 --google-gray-800: rgb(60, 64, 67);
31 --google-gray-900: rgb(32, 33, 36);
32 --heading-color: var(--google-gray-900);
33 --link-color: rgb(88, 88, 88);
34 --popup-container-background-color: rgba(0,0,0,.65);
35 --primary-button-fill-color-active: var(--google-blue-700);
36 --primary-button-fill-color: var(--google-blue-600);
37 --primary-button-text-color: #fff;
38 --quiet-background-color: rgb(247, 247, 247);
39 --secondary-button-border-color: var(--google-gray-500);
40 --secondary-button-fill-color: #fff;
41 --secondary-button-hover-border-color: var(--google-gray-600);
42 --secondary-button-hover-fill-color: var(--google-gray-50);
43 --secondary-button-text-color: var(--google-gray-700);
44 --small-link-color: var(--google-gray-700);
45 --text-color: var(--google-gray-700);
46 background: var(--background-color);
47 color: var(--text-color);
48 word-wrap: break-word;
49}
50
51.nav-wrapper .secondary-button {
52 background: var(--secondary-button-fill-color);
53 border: 1px solid var(--secondary-button-border-color);
54 color: var(--secondary-button-text-color);
55 float: none;
56 margin: 0;
57 padding: 8px 16px;
58}
59
60.hidden {
61 display: none;
62}
63
64html {
65 -webkit-text-size-adjust: 100%;
66 font-size: 125%;
67}
68
69.icon {
70 background-repeat: no-repeat;
71 background-size: 100%;
72}
73
74@media (prefers-color-scheme: dark) {
75 body {
76 --background-color: var(--google-gray-900);
77 --error-code-color: var(--google-gray-500);
78 --heading-color: var(--google-gray-500);
79 --link-color: var(--google-blue-300);
80 --primary-button-fill-color-active: rgb(129, 162, 208);
81 --primary-button-fill-color: var(--google-blue-300);
82 --primary-button-text-color: var(--google-gray-900);
83 --quiet-background-color: var(--background-color);
84 --secondary-button-border-color: var(--google-gray-700);
85 --secondary-button-fill-color: var(--google-gray-900);
86 --secondary-button-hover-fill-color: rgb(48, 51, 57);
87 --secondary-button-text-color: var(--google-blue-300);
88 --small-link-color: var(--google-blue-300);
89 --text-color: var(--google-gray-500);
90 }
91}
92</style>
93 <style>/* Copyright 2014 The Chromium Authors. All rights reserved.
94 Use of this source code is governed by a BSD-style license that can be
95 found in the LICENSE file. */
96
97button {
98 border: 0;
99 border-radius: 4px;
100 box-sizing: border-box;
101 color: var(--primary-button-text-color);
102 cursor: pointer;
103 float: right;
104 font-size: .875em;
105 margin: 0;
106 padding: 8px 16px;
107 transition: box-shadow 150ms cubic-bezier(0.4, 0, 0.2, 1);
108 user-select: none;
109}
110
111[dir='rtl'] button {
112 float: left;
113}
114
115.bad-clock button,
116.captive-portal button,
117.lookalike-url button,
118.main-frame-blocked button,
119.neterror button,
120.offline button,
121.pdf button,
122.ssl button,
123.safe-browsing-billing button {
124 background: #656565;
125}
126
127button:active {
128 background: #333333;
129 outline: 0;
130}
131
132#debugging {
133 display: inline;
134 overflow: auto;
135}
136
137.debugging-content {
138 line-height: 1em;
139 margin-bottom: 0;
140 margin-top: 1em;
141}
142
143.debugging-content-fixed-width {
144 display: block;
145 font-family: monospace;
146 font-size: 1.2em;
147 margin-top: 0.5em;
148}
149
150.debugging-title {
151 font-weight: bold;
152}
153
154#details {
155 margin: 0 0 50px;
156}
157
158#details p:not(:first-of-type) {
159 margin-top: 20px;
160}
161
162.secondary-button:active {
163 border-color: white;
164 box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3),
165 0 2px 6px 2px rgba(60, 64, 67, .15);
166}
167
168.secondary-button:hover {
169 background: var(--secondary-button-hover-fill-color);
170 border-color: var(--secondary-button-hover-border-color);
171 text-decoration: none;
172}
173
174.error-code {
175 color: var(--error-code-color);
176 font-size: .86667em;
177 text-transform: uppercase;
178 margin-top: 12px;
179}
180
181#error-debugging-info {
182 font-size: 0.8em;
183}
184
185h1 {
186 color: var(--heading-color);
187 font-size: 1.6em;
188 font-weight: normal;
189 line-height: 1.25em;
190 margin-bottom: 16px;
191}
192
193h2 {
194 font-size: 1.2em;
195 font-weight: normal;
196}
197
198.icon {
199 height: 72px;
200 margin: 0 0 40px;
201 width: 72px;
202}
203
204input[type=checkbox] {
205 opacity: 0;
206}
207
208input[type=checkbox]:focus ~ .checkbox:after {
209 outline: -webkit-focus-ring-color auto 5px;
210}
211
212.interstitial-wrapper {
213 box-sizing: border-box;
214 font-size: 1em;
215 line-height: 1.6em;
216 margin: 14vh auto 0;
217 max-width: 600px;
218 width: 100%;
219}
220
221#main-message > p {
222 display: inline;
223}
224
225#extended-reporting-opt-in {
226 font-size: .875em;
227 margin-top: 32px;
228}
229
230#extended-reporting-opt-in label {
231 display: grid;
232 grid-template-columns: 1.8em 1fr;
233 position: relative;
234}
235
236.nav-wrapper {
237 margin-top: 51px;
238}
239
240.nav-wrapper::after {
241 clear: both;
242 content: '';
243 display: table;
244 width: 100%;
245}
246
247.small-link {
248 color: var(--small-link-color);
249 font-size: .875em;
250}
251
252.checkboxes {
253 flex: 0 0 24px;
254}
255
256.checkbox {
257 --padding: .9em;
258 background: transparent;
259 display: block;
260 height: 1em;
261 left: -1em;
262 padding-inline-start: var(--padding);
263 position: absolute;
264 right: 0;
265 top: -.5em;
266 width: 1em;
267}
268
269.checkbox::after {
270 border: 1px solid white;
271 border-radius: 2px;
272 content: '';
273 height: 1em;
274 position: absolute;
275 top: var(--padding);
276 left: var(--padding);
277 width: 1em;
278}
279
280.checkbox::before {
281 background: transparent;
282 border: 2px solid white;
283 border-right-width: 0;
284 border-top-width: 0;
285 content: '';
286 height: .2em;
287 left: calc(.3em + var(--padding));
288 opacity: 0;
289 position: absolute;
290 top: calc(.3em + var(--padding));
291 transform: rotate(-45deg);
292 width: .5em;
293}
294
295input[type=checkbox]:checked ~ .checkbox::before {
296 opacity: 1;
297}
298
299#recurrent-error-message {
300 background: #ededed;
301 border-radius: 4px;
302 padding: 12px 16px;
303 margin-top: 12px;
304 margin-bottom: 16px;
305}
306
307.showing-recurrent-error-message #extended-reporting-opt-in {
308 margin-top: 16px;
309}
310
311@media (max-width: 700px) {
312 .interstitial-wrapper {
313 padding: 0 10%;
314 }
315
316 #error-debugging-info {
317 overflow: auto;
318 }
319}
320
321@media (max-width: 420px) {
322 button,
323 [dir='rtl'] button,
324 .small-link {
325 float: none;
326 font-size: .825em;
327 font-weight: 500;
328 margin: 0;
329 width: 100%;
330 }
331
332 button {
333 padding: 16px 24px;
334 }
335
336 #details {
337 margin: 20px 0 20px 0;
338 }
339
340 #details p:not(:first-of-type) {
341 margin-top: 10px;
342 }
343
344 .secondary-button:not(.hidden) {
345 display: block;
346 margin-top: 20px;
347 text-align: center;
348 width: 100%;
349 }
350
351 .interstitial-wrapper {
352 padding: 0 5%;
353 }
354
355 #extended-reporting-opt-in {
356 margin-top: 24px;
357 }
358
359 .nav-wrapper {
360 margin-top: 30px;
361 }
362}
363
364/**
365 * Mobile specific styling.
366 * Navigation buttons are anchored to the bottom of the screen.
367 * Details message replaces the top content in its own scrollable area.
368 */
369
370@media (max-width: 420px) {
371 .nav-wrapper .secondary-button {
372 border: 0;
373 margin: 16px 0 0;
374 margin-inline-end: 0;
375 padding-bottom: 16px;
376 padding-top: 16px;
377 }
378}
379
380/* Fixed nav. */
381@media (min-width: 240px) and (max-width: 420px) and
382 (min-height: 401px),
383 (min-width: 421px) and (min-height: 240px) and
384 (max-height: 560px) {
385 body .nav-wrapper {
386 background: var(--background-color);
387 bottom: 0;
388 box-shadow: 0 -12px 24px var(--background-color);
389 left: 0;
390 margin: 0 auto;
391 max-width: 736px;
392 padding-left: 24px;
393 padding-right: 24px;
394 position: fixed;
395 right: 0;
396 width: 100%;
397 z-index: 2;
398 }
399
400 .interstitial-wrapper {
401 max-width: 736px;
402 }
403
404 #details,
405 #main-content {
406 padding-bottom: 40px;
407 }
408
409 #details {
410 padding-top: 5.5vh;
411 }
412
413 button.small-link {
414 color: var(--google-blue-600);
415 }
416}
417
418@media (max-width: 420px) and (orientation: portrait),
419 (max-height: 560px) {
420 body {
421 margin: 0 auto;
422 }
423
424 button,
425 [dir='rtl'] button,
426 button.small-link,
427 .nav-wrapper .secondary-button {
428 font-family: Roboto-Regular,Helvetica;
429 font-size: .933em;
430 margin: 6px 0;
431 transform: translatez(0);
432 }
433
434 .nav-wrapper {
435 box-sizing: border-box;
436 padding-bottom: 8px;
437 width: 100%;
438 }
439
440 #details {
441 box-sizing: border-box;
442 height: auto;
443 margin: 0;
444 opacity: 1;
445 transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
446 }
447
448 #details.hidden,
449 #main-content.hidden {
450 display: block;
451 height: 0;
452 opacity: 0;
453 overflow: hidden;
454 padding-bottom: 0;
455 transition: none;
456 }
457
458 h1 {
459 font-size: 1.5em;
460 margin-bottom: 8px;
461 }
462
463 .icon {
464 margin-bottom: 5.69vh;
465 }
466
467 .interstitial-wrapper {
468 box-sizing: border-box;
469 margin: 7vh auto 12px;
470 padding: 0 24px;
471 position: relative;
472 }
473
474 .interstitial-wrapper p {
475 font-size: .95em;
476 line-height: 1.61em;
477 margin-top: 8px;
478 }
479
480 #main-content {
481 margin: 0;
482 transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1);
483 }
484
485 .small-link {
486 border: 0;
487 }
488
489 .suggested-left > #control-buttons,
490 .suggested-right > #control-buttons {
491 float: none;
492 margin: 0;
493 }
494}
495
496@media (min-width: 421px) and (min-height: 500px) and (max-height: 560px) {
497 .interstitial-wrapper {
498 margin-top: 10vh;
499 }
500}
501
502@media (min-height: 400px) and (orientation:portrait) {
503 .interstitial-wrapper {
504 margin-bottom: 145px;
505 }
506}
507
508@media (min-height: 299px) {
509 .nav-wrapper {
510 padding-bottom: 16px;
511 }
512}
513
514@media (max-height: 560px) and (min-height: 240px) and (orientation:landscape) {
515 .extended-reporting-has-checkbox #details {
516 padding-bottom: 80px;
517 }
518}
519
520@media (min-height: 500px) and (max-height: 650px) and (max-width: 414px) and
521 (orientation: portrait) {
522 .interstitial-wrapper {
523 margin-top: 7vh;
524 }
525}
526
527@media (min-height: 650px) and (max-width: 414px) and (orientation: portrait) {
528 .interstitial-wrapper {
529 margin-top: 10vh;
530 }
531}
532
533/* Small mobile screens. No fixed nav. */
534@media (max-height: 400px) and (orientation: portrait),
535 (max-height: 239px) and (orientation: landscape),
536 (max-width: 419px) and (max-height: 399px) {
537 .interstitial-wrapper {
538 display: flex;
539 flex-direction: column;
540 margin-bottom: 0;
541 }
542
543 #details {
544 flex: 1 1 auto;
545 order: 0;
546 }
547
548 #main-content {
549 flex: 1 1 auto;
550 order: 0;
551 }
552
553 .nav-wrapper {
554 flex: 0 1 auto;
555 margin-top: 8px;
556 order: 1;
557 padding-left: 0;
558 padding-right: 0;
559 position: relative;
560 width: 100%;
561 }
562
563 button,
564 .nav-wrapper .secondary-button {
565 padding: 16px 24px;
566 }
567
568 button.small-link {
569 color: var(--google-blue-600);
570 }
571}
572
573@media (max-width: 239px) and (orientation: portrait) {
574 .nav-wrapper {
575 padding-left: 0;
576 padding-right: 0;
577 }
578}
579</style>
580 <style>/* Copyright 2013 The Chromium Authors. All rights reserved.
581 * Use of this source code is governed by a BSD-style license that can be
582 * found in the LICENSE file. */
583
584/* Don't use the main frame div when the error is in a subframe. */
585html[subframe] #main-frame-error {
586 display: none;
587}
588
589/* Don't use the subframe error div when the error is in a main frame. */
590html:not([subframe]) #sub-frame-error {
591 display: none;
592}
593
594#diagnose-button {
595 float: none;
596 margin-bottom: 10px;
597 margin-inline-start: 0;
598 margin-top: 20px;
599}
600
601h1 {
602 margin-top: 0;
603 word-wrap: break-word;
604}
605
606h1 span {
607 font-weight: 500;
608}
609
610h2 {
611 color: var(--heading-color);
612 font-size: 1.2em;
613 font-weight: normal;
614 margin: 10px 0;
615}
616
617a {
618 text-decoration: none;
619}
620
621.icon {
622 -webkit-user-select: none;
623 display: inline-block;
624}
625
626.icon-generic {
627 /**
628 * Can't access chrome://theme/IDR_ERROR_NETWORK_GENERIC from an untrusted
629 * renderer process, so embed the resource manually.
630 */
631 content: -webkit-image-set(
632 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAMAAABiM0N1AAABSlBMVEUAAADj4+P/xwDj4+P/xwDn5+fk5OT/xwDk5OTf39//zwD/xwDj4+Pj4+Pk5OT/xgD//wDh4eH/3wDj4+Ph4eH/xADi4uLj4+P/xwDh4eH/wwDj4+P/xwDi4uLf39//yQD/xQD/wwDj4+P/xwD/xgD/xgDj4+Pf39/i4uLm5ubh4eH/zwD/xwD/ygD/xwDk5OTl5eX/xgDf39//yAD/yAD/zwDj4+P/xgD/xAD/yAD/xgDq6uosLCzj4+P////y8vL/xwD+/v7x8fH7+/v9/f34+Pjm5ubl5eX29vbn5+ft7e38/Pz09PT6+vrk5OSwjRBHQCbs7OxGPyY5Ninv7++Ibxnp6enz8/Po6Oj+xgDr6+vXqgh7ZhujgxPyvQOigxNhUyFGQCe9lg5TSSO9lg1uXB5uXR5GQCY6Nik6Nyn19fX5+fnu7u7ltAVVnLrAAAAAO3RSTlMA3/6/3yCfIDAgEO9/gF+gAJAAQHBgUJC/gEDvIaBQf4BBr0CPcG9AsG+gEZ8wz3BP7zAArwDPUDCP7uuCuk4AAAMzSURBVHhe7ZdHe9s4EIbByLLl3hLX2IlLeu/ZRWGnuntP7233/1/DAmkIkCCkUw7xewJpPp8x38xgIPRnOGd49dbgeskwDCoRviqV5hdXy6gHbswbVIsx+FgjUynRHikVbus67YMxpGSM9sWYWqdPLiqSRfumgvLI8bmKzZrrMeb98h2c53huwqgMNj2WwmvVqcwsyrIiy/gsQ02WmkZZDDGoY5aLI1Wmzuq6yxS4YnzZspyjKY48psQ9oSkeIplFjQ7gnRSW0nQqLtDRRjdYUEVVl2lwt2iHmwVCx0yLCSWpzn6dRdhmo9VkEraz20heYsi/Uiiuw2YQrgJBib+stqOlrxc6Sv1HzAQaqZdYKUQ5LRYBu0vBk2ULLimFbBbB0xIohbwtjRCGKCIcqc/gC6wRcsRKqdoMcGkErzJTI+RLXX7AgDjgBl/XNEJtLuTVc/xOHwuuRqgbig+mdWinTfM0QhAHhnrgHCRlz9EKyVuqduq7KagyfWiwgRDoFD+gdJdxNKFBcoWao7vmTxNLf21rhGrZwwJoMMDXCB2LPaGeCI6+RbJ+Q/7kpBrLKqGqnf0aTjzJayNv0MJBm+kusA9o0ZAJNDKiHLQ4O1jBaSCIJ8jQwMBI4VGbLQFIPbRsGf1jWZtKIZw/L5zMhsbRkBVyr8dxhPOGppPMj4FIaAGJrMOAbMolIKe+mTh9yYq5hgRug62BnfH7P0Gn43QiNCr6fZECR4JS6+xM2I8dXSKWy2jS4mwWXGtspqR5QsHphCX1RQuGrEw7oOD0a0LeR34/6f/qZyaFVUmc/kDIp9jvqcwFCQh8lsGPiwGc/kLI19jvOwiYpTK4JVhlm92B33H6kJDDeHF3RipJGez47VDNdmsmhm6ZQC+tmDeEvEtWa+KNXQ84HfGRkM/J6v4zBDygvVFGV6yE/wl5xZdX036PUx3c6Udc6O3OjsUZvYz6UAKnOaen3eXTGaEsS1QDNGvI9v7edvdhbQqlqawYtIjnsdOcb4Tsdx+yh+Xw3OJ8SfVzHZwO+U7IHl/KfusZsoDtnR8Qmui3nklLyQL4reeFVcAS6p2NUOCCgn830N/NOb8B26/tMXzSLIQAAAAASUVORK5CYII=) 1x,
633 url(data:image/png;base64,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) 2x);
634}
635
636.icon-offline {
637 content: -webkit-image-set(
638 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAMAAABiM0N1AAABSlBMVEUAAADj4+P/xwDj4+P/xwDn5+fk5OT/xwDk5OTf39//zwD/xwDj4+Pj4+Pk5OTi4uL//wDh4eH/3wDj4+Ph4eH/xADi4uLj4+P/xwDh4eH/wwDj4+P/xwD/xgDf39//yQD/xQD/wwDj4+P/xwD/xgD/xgDj4+Pf39/i4uLm5ubh4eH/zwD/xwD/ygD/xwDk5OTl5eX/xgDf39//yAD/yAD/zwDj4+P/xgD/xAD/yAD/xgAsLCzq6urx8fHm5uby8vL8/Pzj4+P/xwD////+/v74+Pj9/f3l5eX29vbn5+ft7e37+/v09PT6+vrk5OSwjRBHQCbs7OxGPyY5Ninv7++Ibxnp6enz8/Po6Oj+xgDr6+vXqgh7ZhujgxPyvQOigxNhUyFGQCe9lg5TSSO9lg1uXB5uXR5GQCY6Nik6Nyn19fX5+fnu7u7ltAWzfMZ2AAAAO3RSTlMA3/6/3yCfIDAgEO9/gF+gAJAAQHBgUJC/gEDvIaBQf4BBr0CPcG9AsG+gEZ8wz3BP7zAArwDPUDCP7uuCuk4AAAMsSURBVHhe7ZdFd90wEEblOi95YWywTRsoM4NkhsdhLjP//20NiseyLMteddHclY6TcyN9M4Kgf8MZg8s3+ldVRVGsDMEnVZ1bWK6VsVybUywpSv9DiaauWiVRC6d11arAiNgzAoLqpuoeYEZQLKsy9VxRTs4NYjQ7vuP4vzyN5CWeWzBOQ4zAAfjdFmeaRjzjWY3ncDSzqknEo7CLOnRy0ViRIou61XEEdNhJ8W05m/YcQDi86Sgtus+JFiQewD8qbKVJ8LTAI11df0EXNSAfkWkjEV0vEB06UgxoSXH1W5HANdpdMytwte12/JFA/YWiqA9NPRjpjIl+bPTCoScXHaT+ImFF7dRHIhRZlG4kgtmloMVymZSEIjcS0bLoQpG/IRERWEWIltlnsF4iEWlspzRcB+hEItplhkTkZXb5HnjiBbfpuCkR9ajIb+XknT4WOhJRshQvCS2hlw7Nl4ggEQL9QNmL255SUgRTapjUYzJWR740mEAA7BRPt6xt8MiWRrNkes7aNn4aJPPTnkTU5A8LoO0AnkR0yO4J8Y2gybcInzfUL1tUZUkkarj8b8OJl8lamUY83EEL7QvxAd1QNIaGhoQHG+EvVkga0KMbZKCvb6jwqOVbAEoPW7aGXtj2ulBE8u8LjZvQKBqwA+6UvI5I3qWpxad1XyiaRyyrcEGabAvwpTfjpM/bEVdY0U2IVXe5vH8zntOkY9Ewm/eMBRwwpu7JCTMfN3xELNXQRZuyLn7WgInHDD00acqi+KGlmyJPT7cg6dcYvw/zflT96WfEjVWPk/6A8aco7wnugQToHq/xomaApL9g/DXK+xYCpq0spMtE5RrJhX+a9D7G+9Hg9hTTkjxE83qBze00DQK7ZQw9tyPeYPwuHq2wL3Y5kHTIR4w/x6O7TxBwr6Sohi7ZMX8wfkWHl9N5j0odNOkHVPR2a8umDF9AFUyQNOX4OBk+nmLaUpVo6GalbO7ubCamlQmUpj6uFHqeRklTvmG8m4j4w3JwdmFOFf27DkkHfMd4hw4h77IM2MDm1g9YGuRdCkiaZx7ylvPMLmCxgmgtEJwT8HIN/d+c8RcO3hkvbtK1IAAAAABJRU5ErkJggg==) 1x,
639 url(data:image/png;base64,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) 2x);
640 position: relative;
641}
642
643.icon-disabled {
644 content: -webkit-image-set(
645 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,
646 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOAAAACQAQMAAAArwfVjAAAABlBMVEVMaXFTU1OXUj8tAAAAAXRSTlMAQObYZgAAAYdJREFUeF7F1EFqwzAUBNARAmVj0FZe5QoBH6BX+dn4GlY2PYNzGx/A0CvkCIJuvIraKJKbgBvzf2g62weDGD7CYggpfFReis4J0ey9EGFIiEQQojFSlA9kSIiqd0KkFjKsewgRbStEN19mxUPTtmW9HQ/h6tyqNQ8NlSMZdzyE6qkoE0trVYGFm0n1WYeBhduzwbwBC7voS+vIxfeMjeaiLxsMMtQNwMPtuew+DjzcTHk8YMfDknEcIUOtf2lVfgVH3K4Xv5PRYAXRVMtItIJ3rfaCIVn9DsTH2NxisAVRex2Hh3hX+/mRUR08bAwPEYsI51ZxWH4Q0SpicQRXeyEaIug48FEdegARfMz/tADVsRciwTAxW308ehmC2gLraC+YCbV3QoTZexa+zegAEW5PhhgYfmbvJgcRqngGByOSXdFJcLk2JeDPEN0kxe1JhIt5FiFA+w+ItMELsUyPF2IaJ4aILqb4FbxPwhImwj6JauKgDUCYaxmYIsd4KXdMjIC9ItB5Bn4BNRwsG0XM2nwAAAAASUVORK5CYII=) 2x);
647 width: 112px;
648}
649
650.error-code {
651 display: block;
652 font-size: .8em;
653}
654
655#content-top {
656 margin: 20px;
657}
658
659#help-box-inner {
660 background-color: #f9f9f9;
661 border-top: 1px solid #EEE;
662 color: #444;
663 padding: 20px;
664 text-align: start;
665}
666
667.hidden {
668 display: none;
669}
670
671#suggestion {
672 margin-top: 15px;
673}
674
675#suggestions-list a {
676 color: var(--google-blue-600);
677}
678
679#suggestions-list p {
680 margin-block-end: 0;
681}
682
683#suggestions-list ul {
684 margin-top: 0;
685}
686
687.single-suggestion {
688 list-style-type: none;
689 padding-left: 0;
690}
691
692#short-suggestion {
693 margin-top: 5px;
694}
695
696#error-information-button {
697 content: url((../../../../chromium/components/neterror/resources/images/help_outline.svg);
698 height: 24px;
699 vertical-align: -.15em;
700 width: 24px;
701}
702
703.use-popup-container#error-information-popup-container
704 #error-information-popup {
705 align-items: center;
706 background-color: var(--popup-container-background-color);
707 display: flex;
708 height: 100%;
709 left: 0;
710 position: fixed;
711 top: 0;
712 width: 100%;
713 z-index: 100;
714}
715
716.use-popup-container#error-information-popup-container
717 #error-information-popup-box {
718 background-color: var(--background-color);
719 left: 5%;
720 position: fixed;
721 width: 90%;
722 z-index: 101;
723}
724
725.use-popup-container#error-information-popup-container
726 #error-information-popup-content {
727 padding: 0.5em;
728}
729
730.use-popup-container#error-information-popup-container
731 #error-information-popup-content {
732 padding: 0.5em;
733}
734
735:not(.use-popup-container)#error-information-popup-container
736 #error-information-popup-close {
737 display: none;
738}
739
740#error-information-popup-close {
741 margin-right: 2em;
742 text-align: right;
743}
744
745.link-button {
746 color: rgb(66, 133, 244);
747 display: inline-block;
748 font-weight: bold;
749 text-transform: uppercase;
750}
751
752#sub-frame-error-details {
753
754 color: #8F8F8F;
755
756 /* Not done on mobile for performance reasons. */
757 text-shadow: 0 1px 0 rgba(255,255,255,0.3);
758
759}
760
761[jscontent=hostName],
762[jscontent=failedUrl] {
763 overflow-wrap: break-word;
764}
765
766#search-container {
767 /* Prevents a space between controls. */
768 display: flex;
769 margin-top: 20px;
770}
771
772#search-box {
773 border: 1px solid #cdcdcd;
774 flex-grow: 1;
775 font-size: 1em;
776 height: 26px;
777 margin-right: 0;
778 padding: 1px 9px;
779}
780
781#search-box:focus {
782 border: 1px solid rgb(93, 154, 255);
783 outline: none;
784}
785
786#search-button {
787 border: none;
788 border-bottom-left-radius: 0;
789 border-top-left-radius: 0;
790 box-shadow: none;
791 display: flex;
792 height: 30px;
793 margin: 0;
794 padding: 0;
795 width: 60px;
796}
797
798#search-image {
799 content:
800 -webkit-image-set(
801 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAPCAQAAAB+HTb/AAAArElEQVR4Xn3NsUoCUBzG0XvB3U0chR4geo5qihpt6gkCx0bXFsMERWj2KWqIanAvmlUUoQapwU6g4l8H5bd9Z/iSPS0hu/RqZqrncBuzLl7U3Rn4cSpQFTeroejJl1Lgs7f4ceDPdeBMXYp86gaONYJkY83AnqHiGk9wHnjk16PKgo5N9BUCkzPf5j6M0PfuVg5MymoetFwoaKAlB26WdXAvJ7u5mezitqtkT//7Sv/u96CaLQAAAABJRU5ErkJggg==) 1x,
802 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAeCAQAAACVzLYUAAABYElEQVR4Xr3VMUuVURzH8XO98jgkGikENkRD0KRGDUVDQy0h2SiC4IuIiktL4AvQt1CDBJUJwo1KXXS6cWdHw7tcjWwoC5Hrx+UZgnNO5CXiO/75jD/+QZf9MzjskVU7DrU1zRv9G9ir5hsA4Nii83+GA9ZI1nI1D6tWAE1TRlQMuuuFDthzMQefgo4nKr+f3dIGDdUUHPYD1ISoMQdgJgUfgqaKEOcxWE/BVTArJBvwC0cGY7gNLgiZNsD1GP4EPVn4EtyLYRuczcJ34HYMP4E7GdajDS7FcB48z8AJ8FmI4TjouBkzZ2yBuRQMlsButIZ+dfDVUBqOaIHvavpLVHXfFmAqv45r9gEHNr3y3hcAfLSgSMPgiiZR+6Z9AMuKNAwqpjUcA2h55pxgAfBWkYRlQ254YMJloaxPHbCkiGCymL5RlLA7GnRDXyuC7uhicLoKdRyaDE5Pl00K//93nABqPgBDK8sfWgAAAABJRU5ErkJggg==) 2x);
803 margin: auto;
804}
805
806.secondary-button {
807 background: #d9d9d9;
808 color: #696969;
809 margin-inline-end: 16px;
810}
811
812.snackbar {
813 background: #323232;
814 border-radius: 2px;
815 bottom: 24px;
816 box-sizing: border-box;
817 color: #fff;
818 font-size: .87em;
819 left: 24px;
820 max-width: 568px;
821 min-width: 288px;
822 opacity: 0;
823 padding: 16px 24px 12px;
824 position: fixed;
825 transform: translateY(90px);
826 will-change: opacity, transform;
827 z-index: 999;
828}
829
830.snackbar-show {
831 -webkit-animation:
832 show-snackbar .25s cubic-bezier(0.0, 0.0, 0.2, 1) forwards,
833 hide-snackbar .25s cubic-bezier(0.4, 0.0, 1, 1) forwards 5s;
834}
835
836@-webkit-keyframes show-snackbar {
837 100% {
838 opacity: 1;
839 transform: translateY(0);
840 }
841}
842
843@-webkit-keyframes hide-snackbar {
844 0% {
845 opacity: 1;
846 transform: translateY(0);
847 }
848 100% {
849 opacity: 0;
850 transform: translateY(90px);
851 }
852}
853
854.suggestions {
855 margin-top: 18px;
856}
857
858.suggestion-header {
859 font-weight: bold;
860 margin-bottom: 4px;
861}
862
863.suggestion-body {
864 color: #777;
865}
866
867/* Increase line height at higher resolutions. */
868@media (min-width: 641px) and (min-height: 641px) {
869 #help-box-inner {
870 line-height: 18px;
871 }
872}
873
874/* Decrease padding at low sizes. */
875@media (max-width: 640px), (max-height: 640px) {
876 h1 {
877 margin: 0 0 15px;
878 }
879 #content-top {
880 margin: 15px;
881 }
882 #help-box-inner {
883 padding: 20px;
884 }
885 .suggestions {
886 margin-top: 10px;
887 }
888 .suggestion-header {
889 margin-bottom: 0;
890 }
891}
892
893#download-link, #download-link-clicked {
894 margin-bottom: 30px;
895 margin-top: 30px;
896}
897
898#download-link-clicked {
899 color: #BBB;
900}
901
902 #download-link:before, #download-link-clicked:before {
903 content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxLjJlbSIgaGVpZ2h0PSIxLjJlbSIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNNSAyMGgxNHYtMkg1bTE0LTloLTRWM0g5djZINWw3IDcgNy03eiIgZmlsbD0iIzQyODVGNCIvPjwvc3ZnPg==);
904 display: inline-block;
905 margin-inline-end: 4px;
906 vertical-align: -webkit-baseline-middle;
907 }
908
909#download-link-clicked:before {
910 width: 0px;
911 opacity: 0;
912}
913
914#offline-content-list-visibility-card {
915 border: 1px solid white;
916 border-radius: 8px;
917 display: flex;
918 font-size: .8em;
919 justify-content: space-between;
920 line-height: 1;
921}
922
923#offline-content-list.list-hidden #offline-content-list-visibility-card {
924 border-color: rgb(218, 220, 224);
925}
926
927#offline-content-list-visibility-card > div {
928 padding: 1em;
929}
930
931#offline-content-list-title {
932 color: var(--google-gray-700);
933}
934
935#offline-content-list-show-text, #offline-content-list-hide-text {
936 color: rgb(66, 133, 244);
937}
938
939/* Hides the "hide" text div when the offline content list is collapsed/hidden
940 * and, alternatively, hides the "show" text div when the offline content list
941 * is expanded/shown.
942 */
943#offline-content-list.list-hidden #offline-content-list-hide-text,
944#offline-content-list:not(.list-hidden) #offline-content-list-show-text {
945 display: none;
946}
947
948/* Controls the animation of the offline content list when it is expanded/shown.
949 */
950#offline-content-suggestions {
951 /* Max-height has to be set for the height animation to work. The chosen value
952 * is a little greater than the maximum height the list will have, when all
953 * suggestions have images, so that it is never clamped. This makes so that
954 * when the actual height is smaller then the animation is not as smooth.
955 */
956 max-height: 27em;
957 transition: max-height 0.2s ease-in, visibility 0s 0.2s,
958 opacity 0.2s 0.2s linear;
959}
960
961/* Controls the animation of the offline content list when it is
962 * collapsed/hidden.
963 */
964#offline-content-list.list-hidden #offline-content-suggestions {
965 max-height: 0;
966 visibility: hidden;
967 opacity: 0;
968 transition: opacity 0.2s linear, visibility 0s 0.2s,
969 max-height 0.2s 0.2s ease-out;
970}
971
972#offline-content-list {
973 margin-inline-start: -5%;
974 width: 110%;
975}
976
977/* The selectors below adjust the "overflow" of the suggestion cards contents
978 * based on the same screen size based strategy used for the main frame, which
979 * is applied by the `interstitial-wrapper` class. */
980@media (max-width: 420px) {
981 #offline-content-list {
982 margin-inline-start: -2.5%;
983 width: 105%;
984 }
985}
986@media (max-width: 420px) and (orientation: portrait),
987 (max-height: 560px) {
988 #offline-content-list {
989 margin-inline-start: -12px;
990 width: calc(100% + 24px);
991 }
992}
993
994.suggestion-with-image .offline-content-suggestion-thumbnail {
995 flex-basis: 8.2em;
996 flex-shrink: 0;
997}
998
999.suggestion-with-image .offline-content-suggestion-thumbnail > img {
1000 height: 100%;
1001 width: 100%;
1002}
1003
1004.suggestion-with-image #offline-content-list:not(.is-rtl)
1005.offline-content-suggestion-thumbnail > img {
1006 border-bottom-right-radius: 7px;
1007 border-top-right-radius: 7px;
1008}
1009
1010.suggestion-with-image #offline-content-list.is-rtl
1011.offline-content-suggestion-thumbnail > img {
1012 border-bottom-left-radius: 7px;
1013 border-top-left-radius: 7px;
1014}
1015
1016.suggestion-with-icon .offline-content-suggestion-thumbnail {
1017 align-items: center;
1018 display: flex;
1019 justify-content: center;
1020 min-height: 4.2em;
1021 min-width: 4.2em;
1022}
1023
1024.suggestion-with-icon .offline-content-suggestion-thumbnail > div {
1025 align-items: center;
1026 background-color: rgb(241, 243, 244);
1027 border-radius: 50%;
1028 display: flex;
1029 height: 2.3em;
1030 justify-content: center;
1031 width: 2.3em;
1032}
1033
1034.suggestion-with-icon .offline-content-suggestion-thumbnail > div > img {
1035 height: 1.45em;
1036 width: 1.45em;
1037}
1038
1039.offline-content-suggestion-favicon {
1040 height: 1em;
1041 margin-inline-end: 0.4em;
1042 width: 1.4em;
1043}
1044
1045.offline-content-suggestion-favicon > img {
1046 height: 1.4em;
1047 width: 1.4em;
1048}
1049
1050.no-favicon .offline-content-suggestion-favicon {
1051 display: none;
1052}
1053
1054.image-video {
1055 content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTcgMTAuNVY3YTEgMSAwIDAgMC0xLTFINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJhMSAxIDAgMCAwIDEtMXYtMy41bDQgNHYtMTFsLTQgNHoiIGZpbGw9IiMzQzQwNDMiLz48L3N2Zz4=);
1056}
1057
1058.image-music-note {
1059 content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgM3Y5LjI2Yy0uNS0uMTctMS0uMjYtMS41LS4yNkM4IDEyIDYgMTQgNiAxNi41UzggMjEgMTAuNSAyMXM0LjUtMiA0LjUtNC41VjZoNFYzaC03eiIgZmlsbD0iIzNDNDA0MyIvPjwvc3ZnPg==);
1060}
1061
1062.image-earth {
1063 content: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTIgMmM1LjUyIDAgMTAgNC40OCAxMCAxMHMtNC40OCAxMC0xMCAxMFMyIDE3LjUyIDIgMTIgNi40OCAyIDEyIDJ6TTQgMTJoNC40YzMuNDA3LjAyMiA0LjkyMiAxLjczIDQuNTQzIDUuMTI3SDkuNDg4djIuNDdhOC4wMDQgOC4wMDQgMCAwIDAgMTAuNDk4LTguMDgzQzE5LjMyNyAxMi41MDQgMTguMzMyIDEzIDE3IDEzYy0yLjEzNyAwLTMuMjA2LS45MTYtMy4yMDYtMi43NWgtMy43NDhjLS4yNzQtMi43MjguNjgzLTQuMDkyIDIuODctNC4wOTIgMC0uOTc1LjMyNy0xLjU5Ny44MTEtMS45N0E4LjAwNCA4LjAwNCAwIDAgMCA0IDEyeiIgZmlsbD0iIzNDNDA0MyIvPjwvc3ZnPg==);
1064}
1065
1066.image-file {
1067 content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTMgOVYzLjVMMTguNSA5TTYgMmMtMS4xMSAwLTIgLjg5LTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWOGwtNi02SDZ6IiBmaWxsPSIjM0M0MDQzIi8+PC9zdmc+);
1068}
1069
1070.offline-content-suggestion-texts {
1071 display: flex;
1072 flex-direction: column;
1073 justify-content: space-between;
1074 line-height: 1.3;
1075 padding: .9em;
1076 width: 100%;
1077}
1078
1079.offline-content-suggestion-title {
1080 -webkit-box-orient: vertical;
1081 -webkit-line-clamp: 3;
1082 color: rgb(32, 33, 36);
1083 display: -webkit-box;
1084 font-size: 1.1em;
1085 overflow: hidden;
1086 text-overflow: ellipsis;
1087}
1088
1089div.offline-content-suggestion {
1090 align-items: stretch;
1091 border: 1px solid rgb(218, 220, 224);
1092 border-radius: 8px;
1093 display: flex;
1094 justify-content: space-between;
1095 margin-bottom: .8em;
1096}
1097
1098.suggestion-with-image {
1099 flex-direction: row;
1100 height: 8.2em;
1101 max-height: 8.2em;
1102}
1103
1104.suggestion-with-icon {
1105 flex-direction: row-reverse;
1106 height: 4.2em;
1107 max-height: 4.2em;
1108}
1109
1110.suggestion-with-icon .offline-content-suggestion-title {
1111 -webkit-line-clamp: 1;
1112 word-break: break-all;
1113}
1114
1115.suggestion-with-icon .offline-content-suggestion-texts {
1116 padding-inline-start: 0px;
1117}
1118
1119.offline-content-suggestion-attribution-freshness {
1120 color: rgb(95, 99, 104);
1121 display: flex;
1122 font-size: .8em;
1123 line-height: 1.7em;
1124}
1125
1126.offline-content-suggestion-attribution {
1127 -webkit-box-orient: vertical;
1128 -webkit-line-clamp: 1;
1129 display: -webkit-box;
1130 flex-shrink: 1;
1131 margin-inline-end: 0.3em;
1132 overflow-wrap: break-word;
1133 overflow: hidden;
1134 text-overflow: ellipsis;
1135 word-break: break-all;
1136}
1137
1138.no-attribution .offline-content-suggestion-attribution {
1139 display: none;
1140}
1141
1142.offline-content-suggestion-freshness:before {
1143 content: '-';
1144 display: inline-block;
1145 flex-shrink: 0;
1146 margin-inline-end: .1em;
1147 margin-inline-start: .1em;
1148}
1149
1150.no-attribution .offline-content-suggestion-freshness:before {
1151 display: none;
1152}
1153
1154.offline-content-suggestion-freshness {
1155 flex-shrink: 0;
1156}
1157
1158.suggestion-with-image .offline-content-suggestion-pin-spacer {
1159 flex-shrink: 1;
1160 flex-grow: 100;
1161}
1162
1163.suggestion-with-image .offline-content-suggestion-pin {
1164 content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PGRlZnM+PHBhdGggaWQ9ImEiIGQ9Ik0wIDBoMjR2MjRIMFYweiIvPjwvZGVmcz48Y2xpcFBhdGggaWQ9ImIiPjx1c2UgeGxpbms6aHJlZj0iI2EiIG92ZXJmbG93PSJ2aXNpYmxlIi8+PC9jbGlwUGF0aD48cGF0aCBjbGlwLXBhdGg9InVybCgjYikiIGQ9Ik0xMiAyQzYuNSAyIDIgNi41IDIgMTJzNC41IDEwIDEwIDEwIDEwLTQuNSAxMC0xMFMxNy41IDIgMTIgMnptNSAxNkg3di0yaDEwdjJ6bS02LjctNEw3IDEwLjdsMS40LTEuNCAxLjkgMS45IDUuMy01LjNMMTcgNy4zIDEwLjMgMTR6IiBmaWxsPSIjOUFBMEE2Ii8+PC9zdmc+);
1165 flex-shrink: 0;
1166 height: 1.4em;
1167 margin-inline-start: .4em;
1168 width: 1.4em;
1169}
1170
1171/* Controls the animation (and a bit more) of the launch-downloads-home action
1172 * button when the offline content list is expanded/shown.
1173 */
1174#offline-content-list-action {
1175 text-align: center;
1176 transition: visibility 0s 0.2s, opacity 0.2s 0.2s linear;
1177}
1178
1179/* Controls the animation of the launch-downloads-home action button when the
1180 * offline content list is collapsed/hidden.
1181 */
1182#offline-content-list.list-hidden #offline-content-list-action {
1183 visibility: hidden;
1184 opacity: 0;
1185 transition: opacity 0.2s linear, visibility 0s 0.2s;
1186}
1187
1188#cancel-save-page-button {
1189 background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48Y2xpcFBhdGggaWQ9Im1hc2siPjxwYXRoIGQ9Ik0xMiAyQzYuNSAyIDIgNi41IDIgMTJzNC41IDEwIDEwIDEwIDEwLTQuNSAxMC0xMFMxNy41IDIgMTIgMnptNSAxNkg3di0yaDEwdjJ6bS02LjctNEw3IDEwLjdsMS40LTEuNCAxLjkgMS45IDUuMy01LjNMMTcgNy4zIDEwLjMgMTR6IiBmaWxsPSIjOUFBMEE2Ii8+PC9jbGlwUGF0aD48cGF0aCBjbGlwLXBhdGg9InVybCgjbWFzaykiIGZpbGw9IiM5QUEwQTYiIGQ9Ik0wIDBoMjR2MjRIMHoiLz48cGF0aCBjbGlwLXBhdGg9InVybCgjbWFzaykiIGZpbGw9IiMxQTczRTgiIHN0eWxlPSJhbmltYXRpb246b2ZmbGluZUFuaW1hdGlvbiA0cyBpbmZpbml0ZSIgZD0iTTAgMGgyNHYyNEgweiIvPjxzdHlsZT5Aa2V5ZnJhbWVzIG9mZmxpbmVBbmltYXRpb257MCUsMzUle2hlaWdodDowfTYwJXtoZWlnaHQ6MTAwJX05MCV7ZmlsbC1vcGFjaXR5OjF9dG97ZmlsbC1vcGFjaXR5OjB9fTwvc3R5bGU+PC9zdmc+);
1190 background-position: right 27px center;
1191 background-repeat: no-repeat;
1192 border: 1px solid var(--google-gray-300);
1193 border-radius: 5px;
1194 color: var(--google-gray-700);
1195 margin-bottom: 26px;
1196 padding-bottom: 16px;
1197 padding-inline-end: 88px;
1198 padding-inline-start: 16px;
1199 padding-top: 16px;
1200 text-align: start;
1201}
1202
1203html[dir="rtl"] #cancel-save-page-button {
1204 background-position: left 27px center;
1205}
1206
1207#save-page-for-later-button {
1208 display: flex;
1209 justify-content: start;
1210}
1211
1212#save-page-for-later-button a:before {
1213 content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxLjJlbSIgaGVpZ2h0PSIxLjJlbSIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNNSAyMGgxNHYtMkg1bTE0LTloLTRWM0g5djZINWw3IDcgNy03eiIgZmlsbD0iIzQyODVGNCIvPjwvc3ZnPg==);
1214 display: inline-block;
1215 margin-inline-end: 4px;
1216 vertical-align: -webkit-baseline-middle;
1217}
1218
1219.hidden#save-page-for-later-button {
1220 display: none;
1221}
1222
1223/* Don't allow overflow when in a subframe. */
1224html[subframe] body {
1225 overflow: hidden;
1226}
1227
1228#sub-frame-error {
1229 -webkit-align-items: center;
1230 background-color: #DDD;
1231 display: -webkit-flex;
1232 -webkit-flex-flow: column;
1233 height: 100%;
1234 -webkit-justify-content: center;
1235 left: 0;
1236 position: absolute;
1237 text-align: center;
1238 top: 0;
1239 transition: background-color .2s ease-in-out;
1240 width: 100%;
1241}
1242
1243#sub-frame-error:hover {
1244 background-color: #EEE;
1245}
1246
1247#sub-frame-error .icon-generic {
1248 margin: 0 0 16px;
1249}
1250
1251#sub-frame-error-details {
1252 margin: 0 10px;
1253 text-align: center;
1254 visibility: hidden;
1255}
1256
1257/* Show details only when hovering. */
1258#sub-frame-error:hover #sub-frame-error-details {
1259 visibility: visible;
1260}
1261
1262/* If the iframe is too small, always hide the error code. */
1263/* TODO(mmenke): See if overflow: no-display works better, once supported. */
1264@media (max-width: 200px), (max-height: 95px) {
1265 #sub-frame-error-details {
1266 display: none;
1267 }
1268}
1269
1270/* Adjust icon for small embedded frames in apps. */
1271@media (max-height: 100px) {
1272 #sub-frame-error .icon-generic {
1273 height: auto;
1274 margin: 0;
1275 padding-top: 0;
1276 width: 25px;
1277 }
1278}
1279
1280/* details-button is special; it's a <button> element that looks like a link. */
1281#details-button {
1282 box-shadow: none;
1283 min-width: 0;
1284}
1285
1286/* Styles for platform dependent separation of controls and details button. */
1287.suggested-left > #control-buttons,
1288.suggested-right > #details-button {
1289 float: left;
1290}
1291
1292.suggested-right > #control-buttons,
1293.suggested-left > #details-button {
1294 float: right;
1295}
1296
1297.suggested-left .secondary-button {
1298 margin-inline-end: 0px;
1299 margin-inline-start: 16px;
1300}
1301
1302#details-button.singular {
1303 float: none;
1304}
1305
1306/* download-button shows both icon and text. */
1307#download-button {
1308 padding-bottom: 4px;
1309 padding-top: 4px;
1310 position: relative;
1311}
1312
1313#download-button:before {
1314 background: -webkit-image-set(
1315 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAAO0lEQVQ4y2NgGArgPxIY1YChsOE/LtBAmpYG0mxpIOSDBpKUo2lpIDZxNJCkHKqlYZAla3RAHQ1DFgAARRroHyLNTwwAAAAASUVORK5CYII=) 1x,
1316 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAAAZElEQVRYw+3Ruw3AMAwDUY3OzZUmRRD4E9iim9wNwAdbEURHyk4AAAAATiCVK8lLyPsKeT9K3lsownnunfkPxO78hKiYHxBV8x2icr5BVM+/CMf8g3DN34Rzns6ViwHUAUQ/6wIAd5Km7l6c8AAAAABJRU5ErkJggg==) 2x)
1317 no-repeat;
1318 content: '';
1319 display: inline-block;
1320 width: 24px;
1321 height: 24px;
1322 margin-inline-end: 4px;
1323 margin-inline-start: -4px;
1324 vertical-align: middle;
1325}
1326
1327#download-button:disabled {
1328 background: rgb(180, 206, 249);
1329 color: rgb(255, 255, 255);
1330}
1331
1332/*
1333TODO(https://crbug.com/852872): UI for offline suggested content is incomplete.
1334*/
1335.suggested-thumbnail {
1336 width: 25vw;
1337 height: 25vw;
1338}
1339
1340/* Alternate dino page button styles */
1341#control-buttons .reload-button-alternate:disabled {
1342 background: #ccc;
1343 color: #fff;
1344 font-size: 14px;
1345 height: 48px;
1346}
1347
1348#buttons::after {
1349 clear: both;
1350 content: '';
1351 display: block;
1352 width: 100%;
1353}
1354
1355/* Offline page */
1356.offline {
1357 transition: filter 1.5s cubic-bezier(0.65, 0.05, 0.36, 1),
1358 background-color 1.5s cubic-bezier(0.65, 0.05, 0.36, 1);
1359
1360 will-change: filter, background-color;
1361
1362}
1363
1364.offline body {
1365 transition: background-color 1.5s cubic-bezier(0.65, 0.05, 0.36, 1);
1366}
1367
1368.offline #main-message > p {
1369 display: none;
1370}
1371
1372/* iOS WKWebView inverts the background color set at the HTML level
1373whereas Blink does not. */
1374.offline.inverted {
1375 filter: invert(1);
1376
1377 background-color: #000;
1378
1379
1380}
1381
1382.offline.inverted body {
1383 background-color: #fff;
1384}
1385
1386.offline .interstitial-wrapper {
1387 color: var(--text-color);
1388 font-size: 1em;
1389 line-height: 1.55;
1390 margin: 0 auto;
1391 max-width: 600px;
1392 padding-top: 100px;
1393 width: 100%;
1394}
1395
1396.offline .runner-container {
1397 direction: ltr;
1398 height: 150px;
1399 max-width: 600px;
1400 overflow: hidden;
1401 position: absolute;
1402 top: 35px;
1403 width: 44px;
1404}
1405
1406.offline .runner-canvas {
1407 height: 150px;
1408 max-width: 600px;
1409 opacity: 1;
1410 overflow: hidden;
1411 position: absolute;
1412 top: 0;
1413 z-index: 10;
1414}
1415
1416.offline .controller {
1417 background: rgba(247,247,247, .1);
1418 height: 100vh;
1419 left: 0;
1420 position: absolute;
1421 top: 0;
1422 width: 100vw;
1423 z-index: 9;
1424}
1425
1426#offline-resources {
1427 display: none;
1428}
1429
1430@media (max-width: 420px) {
1431 #download-button {
1432 padding-bottom: 12px;
1433 padding-top: 12px;
1434 }
1435
1436 .suggested-left > #control-buttons,
1437 .suggested-right > #control-buttons {
1438 float: none;
1439 }
1440
1441 .snackbar {
1442 left: 0;
1443 bottom: 0;
1444 width: 100%;
1445 border-radius: 0;
1446 }
1447}
1448
1449@media (max-height: 350px) {
1450 h1 {
1451 margin: 0 0 15px;
1452 }
1453
1454 .icon-offline {
1455 margin: 0 0 10px;
1456 }
1457
1458 .interstitial-wrapper {
1459 margin-top: 5%;
1460 }
1461
1462 .nav-wrapper {
1463 margin-top: 30px;
1464 }
1465}
1466
1467@media (min-width: 420px) and (max-width: 736px) and
1468 (min-height: 240px) and (max-height: 420px) and
1469 (orientation:landscape) {
1470 .interstitial-wrapper {
1471 margin-bottom: 100px;
1472 }
1473}
1474
1475@media (max-width: 360px) and (max-height: 480px) {
1476 .offline .interstitial-wrapper {
1477 padding-top: 60px;
1478 }
1479
1480 .offline .runner-container {
1481 top: 8px;
1482 }
1483}
1484
1485@media (min-height: 240px) and (orientation: landscape) {
1486 .offline .interstitial-wrapper {
1487 margin-bottom: 90px;
1488 }
1489
1490 .icon-offline {
1491 margin-bottom: 20px;
1492 }
1493}
1494
1495@media (max-height: 320px) and (orientation: landscape) {
1496 .icon-offline {
1497 margin-bottom: 0;
1498 }
1499
1500 .offline .runner-container {
1501 top: 10px;
1502 }
1503}
1504
1505@media (max-width: 240px) {
1506 button {
1507 padding-left: 12px;
1508 padding-right: 12px;
1509 }
1510
1511 .interstitial-wrapper {
1512 overflow: inherit;
1513 padding: 0 8px;
1514 }
1515}
1516
1517@media (max-width: 120px) {
1518 button {
1519 width: auto;
1520 }
1521}
1522
1523.arcade-mode,
1524.arcade-mode .runner-container,
1525.arcade-mode .runner-canvas {
1526 image-rendering: pixelated;
1527 max-width: 100%;
1528 overflow: hidden;
1529}
1530
1531.arcade-mode #buttons,
1532.arcade-mode #main-content {
1533 opacity: 0;
1534 overflow: hidden;
1535}
1536
1537.arcade-mode .interstitial-wrapper {
1538 height: 100vh;
1539 max-width: 100%;
1540 overflow: hidden;
1541}
1542
1543.arcade-mode .runner-container {
1544 left: 0;
1545 margin: auto;
1546 right: 0;
1547 transform-origin: top center;
1548 transition: transform 250ms cubic-bezier(0.4, 0.0, 1, 1) .4s;
1549 z-index: 2;
1550}
1551
1552@media (prefers-color-scheme: dark) {
1553 .icon {
1554 filter: invert(1);
1555 }
1556
1557 .offline .runner-canvas {
1558 filter: invert(1);
1559 }
1560
1561 .offline.inverted {
1562 filter: invert(0);
1563
1564 background-color: var(--background-color);
1565
1566
1567 }
1568
1569 .offline.inverted body {
1570 background-color: #fff;
1571 }
1572
1573 #suggestions-list a {
1574 color: var(--link-color);
1575 }
1576
1577 #error-information-button {
1578 filter: invert(0.6);
1579 }
1580}
1581</style>
1582 <script>// Copyright 2017 The Chromium Authors. All rights reserved.
1583// Use of this source code is governed by a BSD-style license that can be
1584// found in the LICENSE file.
1585
1586// This is the shared code for security interstitials. It is used for both SSL
1587// interstitials and Safe Browsing interstitials.
1588
1589/**
1590 * @typedef {{
1591 * dontProceed: function(),
1592 * proceed: function(),
1593 * showMoreSection: function(),
1594 * openHelpCenter: function(),
1595 * openDiagnostic: function(),
1596 * reload: function(),
1597 * openDateSettings: function(),
1598 * openLogin: function(),
1599 * doReport: function(),
1600 * dontReport: function(),
1601 * openReportingPrivacy: function(),
1602 * openWhitepaper: function(),
1603 * reportPhishingError: function(),
1604 * }}
1605 */
1606// eslint-disable-next-line no-var
1607var certificateErrorPageController;
1608
1609// Should match security_interstitials::SecurityInterstitialCommand
1610/** @enum {number} */
1611const SecurityInterstitialCommandId = {
1612 CMD_DONT_PROCEED: 0,
1613 CMD_PROCEED: 1,
1614 // Ways for user to get more information
1615 CMD_SHOW_MORE_SECTION: 2,
1616 CMD_OPEN_HELP_CENTER: 3,
1617 CMD_OPEN_DIAGNOSTIC: 4,
1618 // Primary button actions
1619 CMD_RELOAD: 5,
1620 CMD_OPEN_DATE_SETTINGS: 6,
1621 CMD_OPEN_LOGIN: 7,
1622 // Safe Browsing Extended Reporting
1623 CMD_DO_REPORT: 8,
1624 CMD_DONT_REPORT: 9,
1625 CMD_OPEN_REPORTING_PRIVACY: 10,
1626 CMD_OPEN_WHITEPAPER: 11,
1627 // Report a phishing error.
1628 CMD_REPORT_PHISHING_ERROR: 12
1629};
1630
1631const HIDDEN_CLASS = 'hidden';
1632
1633/**
1634 * A convenience method for sending commands to the parent page.
1635 * @param {SecurityInterstitialCommandId} cmd The command to send.
1636 */
1637function sendCommand(cmd) {
1638 if (window.certificateErrorPageController) {
1639 switch (cmd) {
1640 case SecurityInterstitialCommandId.CMD_DONT_PROCEED:
1641 certificateErrorPageController.dontProceed();
1642 break;
1643 case SecurityInterstitialCommandId.CMD_PROCEED:
1644 certificateErrorPageController.proceed();
1645 break;
1646 case SecurityInterstitialCommandId.CMD_SHOW_MORE_SECTION:
1647 certificateErrorPageController.showMoreSection();
1648 break;
1649 case SecurityInterstitialCommandId.CMD_OPEN_HELP_CENTER:
1650 certificateErrorPageController.openHelpCenter();
1651 break;
1652 case SecurityInterstitialCommandId.CMD_OPEN_DIAGNOSTIC:
1653 certificateErrorPageController.openDiagnostic();
1654 break;
1655 case SecurityInterstitialCommandId.CMD_RELOAD:
1656 certificateErrorPageController.reload();
1657 break;
1658 case SecurityInterstitialCommandId.CMD_OPEN_DATE_SETTINGS:
1659 certificateErrorPageController.openDateSettings();
1660 break;
1661 case SecurityInterstitialCommandId.CMD_OPEN_LOGIN:
1662 certificateErrorPageController.openLogin();
1663 break;
1664 case SecurityInterstitialCommandId.CMD_DO_REPORT:
1665 certificateErrorPageController.doReport();
1666 break;
1667 case SecurityInterstitialCommandId.CMD_DONT_REPORT:
1668 certificateErrorPageController.dontReport();
1669 break;
1670 case SecurityInterstitialCommandId.CMD_OPEN_REPORTING_PRIVACY:
1671 certificateErrorPageController.openReportingPrivacy();
1672 break;
1673 case SecurityInterstitialCommandId.CMD_OPEN_WHITEPAPER:
1674 certificateErrorPageController.openWhitepaper();
1675 break;
1676 case SecurityInterstitialCommandId.CMD_REPORT_PHISHING_ERROR:
1677 certificateErrorPageController.reportPhishingError();
1678 break;
1679 }
1680 return;
1681 }
1682 //
1683 window.domAutomationController.send(cmd);
1684 //
1685 //
1686}
1687
1688/**
1689 * Call this to stop clicks on <a href="#"> links from scrolling to the top of
1690 * the page (and possibly showing a # in the link).
1691 */
1692function preventDefaultOnPoundLinkClicks() {
1693 document.addEventListener('click', function(e) {
1694 const anchor = findAncestor(/** @type {Node} */ (e.target), function(el) {
1695 return el.tagName === 'A';
1696 });
1697 // Use getAttribute() to prevent URL normalization.
1698 if (anchor && anchor.getAttribute('href') === '#') {
1699 e.preventDefault();
1700 }
1701 });
1702}
1703
1704//
1705
1706//
1707</script>
1708 <script>// Copyright 2015 The Chromium Authors. All rights reserved.
1709// Use of this source code is governed by a BSD-style license that can be
1710// found in the LICENSE file.
1711
1712let mobileNav = false;
1713
1714/**
1715 * For small screen mobile the navigation buttons are moved
1716 * below the advanced text.
1717 */
1718function onResize() {
1719 const helpOuterBox = document.querySelector('#details');
1720 const mainContent = document.querySelector('#main-content');
1721 const mediaQuery = '(min-width: 240px) and (max-width: 420px) and ' +
1722 '(min-height: 401px), ' +
1723 '(max-height: 560px) and (min-height: 240px) and ' +
1724 '(min-width: 421px)';
1725
1726 const detailsHidden = helpOuterBox.classList.contains(HIDDEN_CLASS);
1727 const runnerContainer = document.querySelector('.runner-container');
1728
1729 // Check for change in nav status.
1730 if (mobileNav !== window.matchMedia(mediaQuery).matches) {
1731 mobileNav = !mobileNav;
1732
1733 // Handle showing the top content / details sections according to state.
1734 if (mobileNav) {
1735 mainContent.classList.toggle(HIDDEN_CLASS, !detailsHidden);
1736 helpOuterBox.classList.toggle(HIDDEN_CLASS, detailsHidden);
1737 if (runnerContainer) {
1738 runnerContainer.classList.toggle(HIDDEN_CLASS, !detailsHidden);
1739 }
1740 } else if (!detailsHidden) {
1741 // Non mobile nav with visible details.
1742 mainContent.classList.remove(HIDDEN_CLASS);
1743 helpOuterBox.classList.remove(HIDDEN_CLASS);
1744 if (runnerContainer) {
1745 runnerContainer.classList.remove(HIDDEN_CLASS);
1746 }
1747 }
1748 }
1749}
1750
1751function setupMobileNav() {
1752 window.addEventListener('resize', onResize);
1753 onResize();
1754}
1755
1756document.addEventListener('DOMContentLoaded', setupMobileNav);
1757</script>
1758 <script>// Copyright 2013 The Chromium Authors. All rights reserved.
1759// Use of this source code is governed by a BSD-style license that can be
1760// found in the LICENSE file.
1761
1762/**
1763 * @typedef {{
1764 * downloadButtonClick: function(),
1765 * reloadButtonClick: function(),
1766 * detailsButtonClick: function(),
1767 * diagnoseErrorsButtonClick: function(),
1768 * trackClick: function(number),
1769 * trackEasterEgg: function(),
1770 * updateEasterEggHighScore: function(number),
1771 * resetEasterEggHighScore: function(),
1772 * trackCachedCopyButtonClick: function(),
1773 * launchOfflineItem: function(string, string),
1774 * savePageForLater: function(),
1775 * cancelSavePage: function(),
1776 * listVisibilityChange: function(boolean),
1777 * }}
1778 */
1779// eslint-disable-next-line no-var
1780var errorPageController;
1781
1782// Decodes a UTF16 string that is encoded as base64.
1783function decodeUTF16Base64ToString(encoded_text) {
1784 const data = atob(encoded_text);
1785 let result = '';
1786 for (let i = 0; i < data.length; i += 2) {
1787 result +=
1788 String.fromCharCode(data.charCodeAt(i) * 256 + data.charCodeAt(i + 1));
1789 }
1790 return result;
1791}
1792
1793function toggleHelpBox() {
1794 const helpBoxOuter = document.getElementById('details');
1795 helpBoxOuter.classList.toggle(HIDDEN_CLASS);
1796 const detailsButton = document.getElementById('details-button');
1797 if (helpBoxOuter.classList.contains(HIDDEN_CLASS)) {
1798 /** @suppress {missingProperties} */
1799 detailsButton.innerText = detailsButton.detailsText;
1800 } else {
1801 /** @suppress {missingProperties} */
1802 detailsButton.innerText = detailsButton.hideDetailsText;
1803 }
1804
1805 // Details appears over the main content on small screens.
1806 if (mobileNav) {
1807 document.getElementById('main-content').classList.toggle(HIDDEN_CLASS);
1808 const runnerContainer = document.querySelector('.runner-container');
1809 if (runnerContainer) {
1810 runnerContainer.classList.toggle(HIDDEN_CLASS);
1811 }
1812 }
1813}
1814
1815function diagnoseErrors() {
1816//
1817if (window.errorPageController) {
1818 errorPageController.diagnoseErrorsButtonClick();
1819}
1820//
1821//
1822}
1823
1824// Subframes use a different layout but the same html file. This is to make it
1825// easier to support platforms that load the error page via different
1826// mechanisms (Currently just iOS). We also use the subframe style for portals
1827// as they are embedded like subframes and can't be interacted with by the user.
1828if (window.top.location !== window.location || window.portalHost) {
1829 document.documentElement.setAttribute('subframe', '');
1830}
1831
1832// Re-renders the error page using |strings| as the dictionary of values.
1833// Used by NetErrorTabHelper to update DNS error pages with probe results.
1834function updateForDnsProbe(strings) {
1835 const context = new JsEvalContext(strings);
1836 jstProcess(context, document.getElementById('t'));
1837 onDocumentLoadOrUpdate();
1838}
1839
1840// Given the classList property of an element, adds an icon class to the list
1841// and removes the previously-
1842function updateIconClass(classList, newClass) {
1843 let oldClass;
1844
1845 if (classList.hasOwnProperty('last_icon_class')) {
1846 oldClass = classList['last_icon_class'];
1847 if (oldClass === newClass) {
1848 return;
1849 }
1850 }
1851
1852 classList.add(newClass);
1853 if (oldClass !== undefined) {
1854 classList.remove(oldClass);
1855 }
1856
1857 classList['last_icon_class'] = newClass;
1858
1859 if (newClass === 'icon-offline') {
1860 document.firstElementChild.classList.add('offline');
1861 new Runner('.interstitial-wrapper');
1862 } else {
1863 document.body.classList.add('neterror');
1864 }
1865}
1866
1867// Does a search using |baseSearchUrl| and the text in the search box.
1868function search(baseSearchUrl) {
1869 const searchTextNode = document.getElementById('search-box');
1870 document.location = baseSearchUrl + searchTextNode.value;
1871 return false;
1872}
1873
1874// Use to track clicks on elements generated by the navigation correction
1875// service. If |trackingId| is negative, the element does not come from the
1876// correction service.
1877function trackClick(trackingId) {
1878 // This can't be done with XHRs because XHRs are cancelled on navigation
1879 // start, and because these are cross-site requests.
1880 if (trackingId >= 0 && errorPageController) {
1881 errorPageController.trackClick(trackingId);
1882 }
1883}
1884
1885// Called when an <a> tag generated by the navigation correction service is
1886// clicked. Separate function from trackClick so the resources don't have to
1887// be updated if new data is added to jstdata.
1888// @param {{trackingId: number}} jstdata
1889function linkClicked(jstdata) {
1890 trackClick(jstdata.trackingId);
1891}
1892
1893// Implements button clicks. This function is needed during the transition
1894// between implementing these in trunk chromium and implementing them in
1895// iOS.
1896function reloadButtonClick(url) {
1897 if (window.errorPageController) {
1898 errorPageController.reloadButtonClick();
1899 } else {
1900 window.location = url;
1901 }
1902}
1903
1904function downloadButtonClick() {
1905 if (window.errorPageController) {
1906 errorPageController.downloadButtonClick();
1907 const downloadButton = document.getElementById('download-button');
1908 downloadButton.disabled = true;
1909 /** @suppress {missingProperties} */
1910 downloadButton.textContent = downloadButton.disabledText;
1911
1912 document.getElementById('download-link-wrapper')
1913 .classList.add(HIDDEN_CLASS);
1914 document.getElementById('download-link-clicked-wrapper')
1915 .classList.remove(HIDDEN_CLASS);
1916 }
1917}
1918
1919function detailsButtonClick() {
1920 if (window.errorPageController) {
1921 errorPageController.detailsButtonClick();
1922 }
1923}
1924
1925/**
1926 * @typedef {{
1927 * msg: string,
1928 * cacheUrl: string,
1929 * trackingId: number
1930 * }}
1931 */
1932let CacheButtonParams;
1933
1934/**
1935 * Replace the reload button with the Google cached copy suggestion.
1936 * @param {CacheButtonParams} buttonStrings
1937 */
1938function setUpCachedButton(buttonStrings) {
1939 const reloadButton = document.getElementById('reload-button');
1940
1941 reloadButton.textContent = buttonStrings.msg;
1942 const url = buttonStrings.cacheUrl;
1943 const trackingId = buttonStrings.trackingId;
1944 reloadButton.onclick = function(e) {
1945 e.preventDefault();
1946 trackClick(trackingId);
1947 if (window.errorPageController) {
1948 errorPageController.trackCachedCopyButtonClick();
1949 }
1950 window.location = url;
1951 };
1952 reloadButton.style.display = '';
1953}
1954
1955let primaryControlOnLeft = true;
1956//
1957
1958function setAutoFetchState(scheduled, can_schedule) {
1959 document.getElementById('cancel-save-page-button')
1960 .classList.toggle(HIDDEN_CLASS, !scheduled);
1961 document.getElementById('save-page-for-later-button')
1962 .classList.toggle(HIDDEN_CLASS, scheduled || !can_schedule);
1963}
1964
1965function savePageLaterClick() {
1966 errorPageController.savePageForLater();
1967 // savePageForLater will eventually trigger a call to setAutoFetchState() when
1968 // it completes.
1969}
1970
1971function cancelSavePageClick() {
1972 errorPageController.cancelSavePage();
1973 // setAutoFetchState is not called in response to cancelSavePage(), so do it
1974 // now.
1975 setAutoFetchState(false, true);
1976}
1977
1978function toggleErrorInformationPopup() {
1979 document.getElementById('error-information-popup-container')
1980 .classList.toggle(HIDDEN_CLASS);
1981}
1982
1983function launchOfflineItem(itemID, name_space) {
1984 errorPageController.launchOfflineItem(itemID, name_space);
1985}
1986
1987function launchDownloadsPage() {
1988 errorPageController.launchDownloadsPage();
1989}
1990
1991function getIconForSuggestedItem(item) {
1992 // Note: |item.content_type| contains the enum values from
1993 // chrome::mojom::AvailableContentType.
1994 switch (item.content_type) {
1995 case 1: // kVideo
1996 return 'image-video';
1997 case 2: // kAudio
1998 return 'image-music-note';
1999 case 0: // kPrefetchedPage
2000 case 3: // kOtherPage
2001 return 'image-earth';
2002 }
2003 return 'image-file';
2004}
2005
2006function getSuggestedContentDiv(item, index) {
2007 // Note: See AvailableContentToValue in available_offline_content_helper.cc
2008 // for the data contained in an |item|.
2009 // TODO(carlosk): Present |snippet_base64| when that content becomes
2010 // available.
2011 let thumbnail = '';
2012 const extraContainerClasses = [];
2013 // html_inline.py will try to replace src attributes with data URIs using a
2014 // simple regex. The following is obfuscated slightly to avoid that.
2015 const src = 'src';
2016 if (item.thumbnail_data_uri) {
2017 extraContainerClasses.push('suggestion-with-image');
2018 thumbnail = `<img ${src}="${item.thumbnail_data_uri}">`;
2019 } else {
2020 extraContainerClasses.push('suggestion-with-icon');
2021 const iconClass = getIconForSuggestedItem(item);
2022 thumbnail = `<div><img class="${iconClass}"></div>`;
2023 }
2024
2025 let favicon = '';
2026 if (item.favicon_data_uri) {
2027 favicon = `<img ${src}="${item.favicon_data_uri}">`;
2028 } else {
2029 extraContainerClasses.push('no-favicon');
2030 }
2031
2032 if (!item.attribution_base64) {
2033 extraContainerClasses.push('no-attribution');
2034 }
2035
2036 return `
2037 <div class="offline-content-suggestion ${extraContainerClasses.join(' ')}"
2038 onclick="launchOfflineItem('${item.ID}', '${item.name_space}')">
2039 <div class="offline-content-suggestion-texts">
2040 <div id="offline-content-suggestion-title-${index}"
2041 class="offline-content-suggestion-title">
2042 </div>
2043 <div class="offline-content-suggestion-attribution-freshness">
2044 <div id="offline-content-suggestion-favicon-${index}"
2045 class="offline-content-suggestion-favicon">
2046 ${favicon}
2047 </div>
2048 <div id="offline-content-suggestion-attribution-${index}"
2049 class="offline-content-suggestion-attribution">
2050 </div>
2051 <div class="offline-content-suggestion-freshness">
2052 ${item.date_modified}
2053 </div>
2054 <div class="offline-content-suggestion-pin-spacer"></div>
2055 <div class="offline-content-suggestion-pin"></div>
2056 </div>
2057 </div>
2058 <div class="offline-content-suggestion-thumbnail">
2059 ${thumbnail}
2060 </div>
2061 </div>`;
2062}
2063
2064/**
2065 * @typedef {{
2066 * ID: string,
2067 * name_space: string,
2068 * title_base64: string,
2069 * snippet_base64: string,
2070 * date_modified: string,
2071 * attribution_base64: string,
2072 * thumbnail_data_uri: string,
2073 * favicon_data_uri: string,
2074 * content_type: number,
2075 * }}
2076 */
2077let AvailableOfflineContent;
2078
2079// Populates a list of suggested offline content.
2080// Note: For security reasons all content downloaded from the web is considered
2081// unsafe and must be securely handled to be presented on the dino page. Images
2082// have already been safely re-encoded but textual content -- like title and
2083// attribution -- must be properly handled here.
2084// @param {boolean} isShown
2085// @param {Array<AvailableOfflineContent>} suggestions
2086function offlineContentAvailable(isShown, suggestions) {
2087 if (!suggestions || !loadTimeData.valueExists('offlineContentList')) {
2088 return;
2089 }
2090
2091 const suggestionsHTML = [];
2092 for (let index = 0; index < suggestions.length; index++) {
2093 suggestionsHTML.push(getSuggestedContentDiv(suggestions[index], index));
2094 }
2095
2096 document.getElementById('offline-content-suggestions').innerHTML =
2097 suggestionsHTML.join('\n');
2098
2099 // Sets textual web content using |textContent| to make sure it's handled as
2100 // plain text.
2101 for (let index = 0; index < suggestions.length; index++) {
2102 document.getElementById(`offline-content-suggestion-title-${index}`)
2103 .textContent =
2104 decodeUTF16Base64ToString(suggestions[index].title_base64);
2105 document.getElementById(`offline-content-suggestion-attribution-${index}`)
2106 .textContent =
2107 decodeUTF16Base64ToString(suggestions[index].attribution_base64);
2108 }
2109
2110 const contentListElement = document.getElementById('offline-content-list');
2111 if (document.dir === 'rtl') {
2112 contentListElement.classList.add('is-rtl');
2113 }
2114 contentListElement.hidden = false;
2115 // The list is configured as hidden by default. Show it if needed.
2116 if (isShown) {
2117 toggleOfflineContentListVisibility(false);
2118 }
2119}
2120
2121function toggleOfflineContentListVisibility(updatePref) {
2122 if (!loadTimeData.valueExists('offlineContentList')) {
2123 return;
2124 }
2125
2126 const contentListElement = document.getElementById('offline-content-list');
2127 const isVisible = !contentListElement.classList.toggle('list-hidden');
2128
2129 if (updatePref && window.errorPageController) {
2130 errorPageController.listVisibilityChanged(isVisible);
2131 }
2132}
2133
2134// Called on document load, and from updateForDnsProbe().
2135function onDocumentLoadOrUpdate() {
2136 const downloadButtonVisible = loadTimeData.valueExists('downloadButton') &&
2137 loadTimeData.getValue('downloadButton').msg;
2138 const detailsButton = document.getElementById('details-button');
2139
2140 // If offline content suggestions will be visible, the usual buttons will not
2141 // be presented.
2142 const offlineContentVisible =
2143 loadTimeData.valueExists('suggestedOfflineContentPresentation');
2144 if (offlineContentVisible) {
2145 document.querySelector('.nav-wrapper').classList.add(HIDDEN_CLASS);
2146 detailsButton.classList.add(HIDDEN_CLASS);
2147
2148 document.getElementById('download-link').hidden = !downloadButtonVisible;
2149 document.getElementById('download-links-wrapper')
2150 .classList.remove(HIDDEN_CLASS);
2151 document.getElementById('error-information-popup-container')
2152 .classList.add('use-popup-container', HIDDEN_CLASS);
2153 document.getElementById('error-information-button')
2154 .classList.remove(HIDDEN_CLASS);
2155 }
2156
2157 const attemptAutoFetch = loadTimeData.valueExists('attemptAutoFetch') &&
2158 loadTimeData.getValue('attemptAutoFetch');
2159
2160 const reloadButtonVisible = loadTimeData.valueExists('reloadButton') &&
2161 loadTimeData.getValue('reloadButton').msg;
2162
2163 // Check for Google cached copy suggestion.
2164 let cacheButtonVisible = false;
2165 if (loadTimeData.valueExists('cacheButton')) {
2166 setUpCachedButton(/** @type {CacheButtonParams} */
2167 (loadTimeData.getValue('cacheButton')));
2168 cacheButtonVisible = true;
2169 }
2170
2171 const reloadButton = document.getElementById('reload-button');
2172 const downloadButton = document.getElementById('download-button');
2173 if (reloadButton.style.display === 'none' &&
2174 downloadButton.style.display === 'none') {
2175 detailsButton.classList.add('singular');
2176 }
2177
2178 // Show or hide control buttons.
2179 const controlButtonDiv = document.getElementById('control-buttons');
2180 controlButtonDiv.hidden = offlineContentVisible ||
2181 !(reloadButtonVisible || downloadButtonVisible || cacheButtonVisible);
2182}
2183
2184function onDocumentLoad() {
2185 // Sets up the proper button layout for the current platform.
2186 const buttonsDiv = document.getElementById('buttons');
2187 if (primaryControlOnLeft) {
2188 buttonsDiv.classList.add('suggested-left');
2189 } else {
2190 buttonsDiv.classList.add('suggested-right');
2191 }
2192
2193 onDocumentLoadOrUpdate();
2194}
2195
2196document.addEventListener('DOMContentLoaded', onDocumentLoad);
2197</script>
2198 <script>// Copyright (c) 2014 The Chromium Authors. All rights reserved.
2199// Use of this source code is governed by a BSD-style license that can be
2200// found in the LICENSE file.
2201
2202/**
2203 * T-Rex runner.
2204 * @param {string} outerContainerId Outer containing element id.
2205 * @param {!Object=} opt_config
2206 * @constructor
2207 * @implements {EventListener}
2208 * @export
2209 */
2210function Runner(outerContainerId, opt_config) {
2211 // Singleton
2212 if (Runner.instance_) {
2213 return Runner.instance_;
2214 }
2215 Runner.instance_ = this;
2216
2217 this.outerContainerEl = document.querySelector(outerContainerId);
2218 this.containerEl = null;
2219 this.snackbarEl = null;
2220 // A div to intercept touch events. Only set while (playing && useTouch).
2221 this.touchController = null;
2222
2223 this.config = opt_config || Runner.config;
2224 // Logical dimensions of the container.
2225 this.dimensions = Runner.defaultDimensions;
2226
2227 this.canvas = null;
2228 this.canvasCtx = null;
2229
2230 this.tRex = null;
2231
2232 this.distanceMeter = null;
2233 this.distanceRan = 0;
2234
2235 this.highestScore = 0;
2236 this.syncHighestScore = false;
2237
2238 this.time = 0;
2239 this.runningTime = 0;
2240 this.msPerFrame = 1000 / FPS;
2241 this.currentSpeed = this.config.SPEED;
2242
2243 this.obstacles = [];
2244
2245 this.activated = false; // Whether the easter egg has been activated.
2246 this.playing = false; // Whether the game is currently in play state.
2247 this.crashed = false;
2248 this.paused = false;
2249 this.inverted = false;
2250 this.invertTimer = 0;
2251 this.resizeTimerId_ = null;
2252
2253 this.playCount = 0;
2254
2255 // Sound FX.
2256 this.audioBuffer = null;
2257 /** @type {Object} */
2258 this.soundFx = {};
2259
2260 // Global web audio context for playing sounds.
2261 this.audioContext = null;
2262
2263 // Images.
2264 this.images = {};
2265 this.imagesLoaded = 0;
2266
2267 // Gamepad state.
2268 this.pollingGamepads = false;
2269 this.gamepadIndex = undefined;
2270 this.previousGamepad = null;
2271
2272 if (this.isDisabled()) {
2273 this.setupDisabledRunner();
2274 } else {
2275 this.loadImages();
2276
2277 window['initializeEasterEggHighScore'] =
2278 this.initializeHighScore.bind(this);
2279 }
2280}
2281
2282/**
2283 * Default game width.
2284 * @const
2285 */
2286const DEFAULT_WIDTH = 600;
2287
2288/**
2289 * Frames per second.
2290 * @const
2291 */
2292const FPS = 60;
2293
2294/** @const */
2295const IS_HIDPI = window.devicePixelRatio > 1;
2296
2297/** @const */
2298const IS_IOS = /CriOS/.test(window.navigator.userAgent);
2299
2300/** @const */
2301const IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS;
2302
2303/** @const */
2304const ARCADE_MODE_URL = 'chrome://dino/';
2305
2306/**
2307 * Default game configuration.
2308 */
2309Runner.config = {
2310 ACCELERATION: 0.001,
2311 BG_CLOUD_SPEED: 0.2,
2312 BOTTOM_PAD: 10,
2313 // Scroll Y threshold at which the game can be activated.
2314 CANVAS_IN_VIEW_OFFSET: -10,
2315 CLEAR_TIME: 3000,
2316 CLOUD_FREQUENCY: 0.5,
2317 GAMEOVER_CLEAR_TIME: 750,
2318 GAP_COEFFICIENT: 0.6,
2319 GRAVITY: 0.6,
2320 INITIAL_JUMP_VELOCITY: 12,
2321 INVERT_FADE_DURATION: 12000,
2322 INVERT_DISTANCE: 700,
2323 MAX_BLINK_COUNT: 3,
2324 MAX_CLOUDS: 6,
2325 MAX_OBSTACLE_LENGTH: 3,
2326 MAX_OBSTACLE_DUPLICATION: 2,
2327 MAX_SPEED: 13,
2328 MIN_JUMP_HEIGHT: 35,
2329 MOBILE_SPEED_COEFFICIENT: 1.2,
2330 RESOURCE_TEMPLATE_ID: 'audio-resources',
2331 SPEED: 6,
2332 SPEED_DROP_COEFFICIENT: 3,
2333 ARCADE_MODE_INITIAL_TOP_POSITION: 35,
2334 ARCADE_MODE_TOP_POSITION_PERCENT: 0.1
2335};
2336
2337
2338/**
2339 * Default dimensions.
2340 */
2341Runner.defaultDimensions = {
2342 WIDTH: DEFAULT_WIDTH,
2343 HEIGHT: 150
2344};
2345
2346
2347/**
2348 * CSS class names.
2349 * @enum {string}
2350 */
2351Runner.classes = {
2352 ARCADE_MODE: 'arcade-mode',
2353 CANVAS: 'runner-canvas',
2354 CONTAINER: 'runner-container',
2355 CRASHED: 'crashed',
2356 ICON: 'icon-offline',
2357 INVERTED: 'inverted',
2358 SNACKBAR: 'snackbar',
2359 SNACKBAR_SHOW: 'snackbar-show',
2360 TOUCH_CONTROLLER: 'controller'
2361};
2362
2363
2364/**
2365 * Sprite definition layout of the spritesheet.
2366 * @enum {Object}
2367 */
2368Runner.spriteDefinition = {
2369 LDPI: {
2370 CACTUS_LARGE: {x: 332, y: 2},
2371 CACTUS_SMALL: {x: 228, y: 2},
2372 CLOUD: {x: 86, y: 2},
2373 HORIZON: {x: 2, y: 54},
2374 MOON: {x: 484, y: 2},
2375 PTERODACTYL: {x: 134, y: 2},
2376 RESTART: {x: 2, y: 2},
2377 TEXT_SPRITE: {x: 655, y: 2},
2378 TREX: {x: 848, y: 2},
2379 STAR: {x: 645, y: 2}
2380 },
2381 HDPI: {
2382 CACTUS_LARGE: {x: 652, y: 2},
2383 CACTUS_SMALL: {x: 446, y: 2},
2384 CLOUD: {x: 166, y: 2},
2385 HORIZON: {x: 2, y: 104},
2386 MOON: {x: 954, y: 2},
2387 PTERODACTYL: {x: 260, y: 2},
2388 RESTART: {x: 2, y: 2},
2389 TEXT_SPRITE: {x: 1294, y: 2},
2390 TREX: {x: 1678, y: 2},
2391 STAR: {x: 1276, y: 2}
2392 }
2393};
2394
2395
2396/**
2397 * Sound FX. Reference to the ID of the audio tag on interstitial page.
2398 * @enum {string}
2399 */
2400Runner.sounds = {
2401 BUTTON_PRESS: 'offline-sound-press',
2402 HIT: 'offline-sound-hit',
2403 SCORE: 'offline-sound-reached'
2404};
2405
2406
2407/**
2408 * Key code mapping.
2409 * @enum {Object}
2410 */
2411Runner.keycodes = {
2412 JUMP: {'38': 1, '32': 1}, // Up, spacebar
2413 DUCK: {'40': 1}, // Down
2414 RESTART: {'13': 1} // Enter
2415};
2416
2417
2418/**
2419 * Runner event names.
2420 * @enum {string}
2421 */
2422Runner.events = {
2423 ANIM_END: 'webkitAnimationEnd',
2424 CLICK: 'click',
2425 KEYDOWN: 'keydown',
2426 KEYUP: 'keyup',
2427 POINTERDOWN: 'pointerdown',
2428 POINTERUP: 'pointerup',
2429 RESIZE: 'resize',
2430 TOUCHEND: 'touchend',
2431 TOUCHSTART: 'touchstart',
2432 VISIBILITY: 'visibilitychange',
2433 BLUR: 'blur',
2434 FOCUS: 'focus',
2435 LOAD: 'load',
2436 GAMEPADCONNECTED: 'gamepadconnected',
2437};
2438
2439Runner.prototype = {
2440 /**
2441 * Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
2442 * @return {boolean}
2443 */
2444 isDisabled() {
2445 return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
2446 },
2447
2448 /**
2449 * For disabled instances, set up a snackbar with the disabled message.
2450 */
2451 setupDisabledRunner() {
2452 this.containerEl = document.createElement('div');
2453 this.containerEl.className = Runner.classes.SNACKBAR;
2454 this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
2455 this.outerContainerEl.appendChild(this.containerEl);
2456
2457 // Show notification when the activation key is pressed.
2458 document.addEventListener(Runner.events.KEYDOWN, function(e) {
2459 if (Runner.keycodes.JUMP[e.keyCode]) {
2460 this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
2461 document.querySelector('.icon').classList.add('icon-disabled');
2462 }
2463 }.bind(this));
2464 },
2465
2466 /**
2467 * Setting individual settings for debugging.
2468 * @param {string} setting
2469 * @param {number|string} value
2470 */
2471 updateConfigSetting(setting, value) {
2472 if (setting in this.config && value !== undefined) {
2473 this.config[setting] = value;
2474
2475 switch (setting) {
2476 case 'GRAVITY':
2477 case 'MIN_JUMP_HEIGHT':
2478 case 'SPEED_DROP_COEFFICIENT':
2479 this.tRex.config[setting] = value;
2480 break;
2481 case 'INITIAL_JUMP_VELOCITY':
2482 this.tRex.setJumpVelocity(value);
2483 break;
2484 case 'SPEED':
2485 this.setSpeed(/** @type {number} */ (value));
2486 break;
2487 }
2488 }
2489 },
2490
2491 /**
2492 * Cache the appropriate image sprite from the page and get the sprite sheet
2493 * definition.
2494 */
2495 loadImages() {
2496 if (IS_HIDPI) {
2497 Runner.imageSprite = /** @type {HTMLImageElement} */
2498 (document.getElementById('offline-resources-2x'));
2499 this.spriteDef = Runner.spriteDefinition.HDPI;
2500 } else {
2501 Runner.imageSprite = /** @type {HTMLImageElement} */
2502 (document.getElementById('offline-resources-1x'));
2503 this.spriteDef = Runner.spriteDefinition.LDPI;
2504 }
2505
2506 if (Runner.imageSprite.complete) {
2507 this.init();
2508 } else {
2509 // If the images are not yet loaded, add a listener.
2510 Runner.imageSprite.addEventListener(Runner.events.LOAD,
2511 this.init.bind(this));
2512 }
2513 },
2514
2515 /**
2516 * Load and decode base 64 encoded sounds.
2517 */
2518 loadSounds() {
2519 if (!IS_IOS) {
2520 this.audioContext = new AudioContext();
2521
2522 const resourceTemplate =
2523 document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
2524
2525 for (const sound in Runner.sounds) {
2526 let soundSrc =
2527 resourceTemplate.getElementById(Runner.sounds[sound]).src;
2528 soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
2529 const buffer = decodeBase64ToArrayBuffer(soundSrc);
2530
2531 // Async, so no guarantee of order in array.
2532 this.audioContext.decodeAudioData(buffer, function(index, audioData) {
2533 this.soundFx[index] = audioData;
2534 }.bind(this, sound));
2535 }
2536 }
2537 },
2538
2539 /**
2540 * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
2541 * @param {number=} opt_speed
2542 */
2543 setSpeed(opt_speed) {
2544 const speed = opt_speed || this.currentSpeed;
2545
2546 // Reduce the speed on smaller mobile screens.
2547 if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
2548 const mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
2549 this.config.MOBILE_SPEED_COEFFICIENT;
2550 this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
2551 } else if (opt_speed) {
2552 this.currentSpeed = opt_speed;
2553 }
2554 },
2555
2556 /**
2557 * Game initialiser.
2558 */
2559 init() {
2560 // Hide the static icon.
2561 document.querySelector('.' + Runner.classes.ICON).style.visibility =
2562 'hidden';
2563
2564 this.adjustDimensions();
2565 this.setSpeed();
2566
2567 this.containerEl = document.createElement('div');
2568 this.containerEl.className = Runner.classes.CONTAINER;
2569
2570 // Player canvas container.
2571 this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
2572 this.dimensions.HEIGHT);
2573
2574 this.canvasCtx =
2575 /** @type {CanvasRenderingContext2D} */ (this.canvas.getContext('2d'));
2576 this.canvasCtx.fillStyle = '#f7f7f7';
2577 this.canvasCtx.fill();
2578 Runner.updateCanvasScaling(this.canvas);
2579
2580 // Horizon contains clouds, obstacles and the ground.
2581 this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
2582 this.config.GAP_COEFFICIENT);
2583
2584 // Distance meter
2585 this.distanceMeter = new DistanceMeter(this.canvas,
2586 this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
2587
2588 // Draw t-rex
2589 this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
2590
2591 this.outerContainerEl.appendChild(this.containerEl);
2592
2593 this.startListening();
2594 this.update();
2595
2596 window.addEventListener(Runner.events.RESIZE,
2597 this.debounceResize.bind(this));
2598
2599 // Handle dark mode
2600 const darkModeMediaQuery =
2601 window.matchMedia('(prefers-color-scheme: dark)');
2602 this.isDarkMode = darkModeMediaQuery && darkModeMediaQuery.matches;
2603 darkModeMediaQuery.addListener((e) => {
2604 this.isDarkMode = e.matches;
2605 });
2606 },
2607
2608 /**
2609 * Create the touch controller. A div that covers whole screen.
2610 */
2611 createTouchController() {
2612 this.touchController = document.createElement('div');
2613 this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
2614 this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
2615 this.touchController.addEventListener(Runner.events.TOUCHEND, this);
2616 this.outerContainerEl.appendChild(this.touchController);
2617 },
2618
2619 /**
2620 * Debounce the resize event.
2621 */
2622 debounceResize() {
2623 if (!this.resizeTimerId_) {
2624 this.resizeTimerId_ =
2625 setInterval(this.adjustDimensions.bind(this), 250);
2626 }
2627 },
2628
2629 /**
2630 * Adjust game space dimensions on resize.
2631 */
2632 adjustDimensions() {
2633 clearInterval(this.resizeTimerId_);
2634 this.resizeTimerId_ = null;
2635
2636 const boxStyles = window.getComputedStyle(this.outerContainerEl);
2637 const padding = Number(boxStyles.paddingLeft.substr(0,
2638 boxStyles.paddingLeft.length - 2));
2639
2640 this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
2641 if (this.isArcadeMode()) {
2642 this.dimensions.WIDTH = Math.min(DEFAULT_WIDTH, this.dimensions.WIDTH);
2643 if (this.activated) {
2644 this.setArcadeModeContainerScale();
2645 }
2646 }
2647
2648 // Redraw the elements back onto the canvas.
2649 if (this.canvas) {
2650 this.canvas.width = this.dimensions.WIDTH;
2651 this.canvas.height = this.dimensions.HEIGHT;
2652
2653 Runner.updateCanvasScaling(this.canvas);
2654
2655 this.distanceMeter.calcXPos(this.dimensions.WIDTH);
2656 this.clearCanvas();
2657 this.horizon.update(0, 0, true);
2658 this.tRex.update(0);
2659
2660 // Outer container and distance meter.
2661 if (this.playing || this.crashed || this.paused) {
2662 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
2663 this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
2664 this.distanceMeter.update(0, Math.ceil(this.distanceRan));
2665 this.stop();
2666 } else {
2667 this.tRex.draw(0, 0);
2668 }
2669
2670 // Game over panel.
2671 if (this.crashed && this.gameOverPanel) {
2672 this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
2673 this.gameOverPanel.draw();
2674 }
2675 }
2676 },
2677
2678 /**
2679 * Play the game intro.
2680 * Canvas container width expands out to the full width.
2681 */
2682 playIntro() {
2683 if (!this.activated && !this.crashed) {
2684 this.playingIntro = true;
2685 this.tRex.playingIntro = true;
2686
2687 // CSS animation definition.
2688 const keyframes = '@-webkit-keyframes intro { ' +
2689 'from { width:' + Trex.config.WIDTH + 'px }' +
2690 'to { width: ' + this.dimensions.WIDTH + 'px }' +
2691 '}';
2692 document.styleSheets[0].insertRule(keyframes, 0);
2693
2694 this.containerEl.addEventListener(Runner.events.ANIM_END,
2695 this.startGame.bind(this));
2696
2697 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
2698 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
2699
2700 this.setPlayStatus(true);
2701 this.activated = true;
2702 } else if (this.crashed) {
2703 this.restart();
2704 }
2705 },
2706
2707
2708 /**
2709 * Update the game status to started.
2710 */
2711 startGame() {
2712 if (this.isArcadeMode()) {
2713 this.setArcadeMode();
2714 }
2715 this.runningTime = 0;
2716 this.playingIntro = false;
2717 this.tRex.playingIntro = false;
2718 this.containerEl.style.webkitAnimation = '';
2719 this.playCount++;
2720
2721 // Handle tabbing off the page. Pause the current game.
2722 document.addEventListener(Runner.events.VISIBILITY,
2723 this.onVisibilityChange.bind(this));
2724
2725 window.addEventListener(Runner.events.BLUR,
2726 this.onVisibilityChange.bind(this));
2727
2728 window.addEventListener(Runner.events.FOCUS,
2729 this.onVisibilityChange.bind(this));
2730 },
2731
2732 clearCanvas() {
2733 this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
2734 this.dimensions.HEIGHT);
2735 },
2736
2737 /**
2738 * Checks whether the canvas area is in the viewport of the browser
2739 * through the current scroll position.
2740 * @return boolean.
2741 */
2742 isCanvasInView() {
2743 return this.containerEl.getBoundingClientRect().top >
2744 Runner.config.CANVAS_IN_VIEW_OFFSET;
2745 },
2746
2747 /**
2748 * Update the game frame and schedules the next one.
2749 */
2750 update() {
2751 this.updatePending = false;
2752
2753 const now = getTimeStamp();
2754 let deltaTime = now - (this.time || now);
2755
2756 this.time = now;
2757
2758 if (this.playing) {
2759 this.clearCanvas();
2760
2761 if (this.tRex.jumping) {
2762 this.tRex.updateJump(deltaTime);
2763 }
2764
2765 this.runningTime += deltaTime;
2766 const hasObstacles = this.runningTime > this.config.CLEAR_TIME;
2767
2768 // First jump triggers the intro.
2769 if (this.tRex.jumpCount === 1 && !this.playingIntro) {
2770 this.playIntro();
2771 }
2772
2773 // The horizon doesn't move until the intro is over.
2774 if (this.playingIntro) {
2775 this.horizon.update(0, this.currentSpeed, hasObstacles);
2776 } else {
2777 const showNightMode = this.isDarkMode ^ this.inverted;
2778 deltaTime = !this.activated ? 0 : deltaTime;
2779 this.horizon.update(
2780 deltaTime, this.currentSpeed, hasObstacles, showNightMode);
2781 }
2782
2783 // Check for collisions.
2784 const collision = hasObstacles &&
2785 checkForCollision(this.horizon.obstacles[0], this.tRex);
2786
2787 if (!collision) {
2788 this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
2789
2790 if (this.currentSpeed < this.config.MAX_SPEED) {
2791 this.currentSpeed += this.config.ACCELERATION;
2792 }
2793 } else {
2794 this.gameOver();
2795 }
2796
2797 const playAchievementSound = this.distanceMeter.update(deltaTime,
2798 Math.ceil(this.distanceRan));
2799
2800 if (playAchievementSound) {
2801 this.playSound(this.soundFx.SCORE);
2802 }
2803
2804 // Night mode.
2805 if (this.invertTimer > this.config.INVERT_FADE_DURATION) {
2806 this.invertTimer = 0;
2807 this.invertTrigger = false;
2808 this.invert(false);
2809 } else if (this.invertTimer) {
2810 this.invertTimer += deltaTime;
2811 } else {
2812 const actualDistance =
2813 this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));
2814
2815 if (actualDistance > 0) {
2816 this.invertTrigger = !(actualDistance %
2817 this.config.INVERT_DISTANCE);
2818
2819 if (this.invertTrigger && this.invertTimer === 0) {
2820 this.invertTimer += deltaTime;
2821 this.invert(false);
2822 }
2823 }
2824 }
2825 }
2826
2827 if (this.playing || (!this.activated &&
2828 this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
2829 this.tRex.update(deltaTime);
2830 this.scheduleNextUpdate();
2831 }
2832 },
2833
2834 /**
2835 * Event handler.
2836 * @param {Event} e
2837 */
2838 handleEvent(e) {
2839 return (function(evtType, events) {
2840 switch (evtType) {
2841 case events.KEYDOWN:
2842 case events.TOUCHSTART:
2843 case events.POINTERDOWN:
2844 this.onKeyDown(e);
2845 break;
2846 case events.KEYUP:
2847 case events.TOUCHEND:
2848 case events.POINTERUP:
2849 this.onKeyUp(e);
2850 break;
2851 case events.GAMEPADCONNECTED:
2852 this.onGamepadConnected(e);
2853 break;
2854 }
2855 }.bind(this))(e.type, Runner.events);
2856 },
2857
2858 /**
2859 * Bind relevant key / mouse / touch listeners.
2860 */
2861 startListening() {
2862 // Keys.
2863 document.addEventListener(Runner.events.KEYDOWN, this);
2864 document.addEventListener(Runner.events.KEYUP, this);
2865
2866 // Touch / pointer.
2867 this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
2868 document.addEventListener(Runner.events.POINTERDOWN, this);
2869 document.addEventListener(Runner.events.POINTERUP, this);
2870
2871 if (this.isArcadeMode()) {
2872 // Gamepad
2873 window.addEventListener(Runner.events.GAMEPADCONNECTED, this);
2874 }
2875 },
2876
2877 /**
2878 * Remove all listeners.
2879 */
2880 stopListening() {
2881 document.removeEventListener(Runner.events.KEYDOWN, this);
2882 document.removeEventListener(Runner.events.KEYUP, this);
2883
2884 if (this.touchController) {
2885 this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
2886 this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
2887 }
2888
2889 this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
2890 document.removeEventListener(Runner.events.POINTERDOWN, this);
2891 document.removeEventListener(Runner.events.POINTERUP, this);
2892
2893 if (this.isArcadeMode()) {
2894 window.removeEventListener(Runner.events.GAMEPADCONNECTED, this);
2895 }
2896 },
2897
2898 /**
2899 * Process keydown.
2900 * @param {Event} e
2901 */
2902 onKeyDown(e) {
2903 // Prevent native page scrolling whilst tapping on mobile.
2904 if (IS_MOBILE && this.playing) {
2905 e.preventDefault();
2906 }
2907
2908 if (this.isCanvasInView()) {
2909 if (!this.crashed && !this.paused) {
2910 if (Runner.keycodes.JUMP[e.keyCode] ||
2911 e.type === Runner.events.TOUCHSTART) {
2912 e.preventDefault();
2913 // Starting the game for the first time.
2914 if (!this.playing) {
2915 // Started by touch so create a touch controller.
2916 if (!this.touchController && e.type === Runner.events.TOUCHSTART) {
2917 this.createTouchController();
2918 }
2919 this.loadSounds();
2920 this.setPlayStatus(true);
2921 this.update();
2922 if (window.errorPageController) {
2923 errorPageController.trackEasterEgg();
2924 }
2925 }
2926 // Start jump.
2927 if (!this.tRex.jumping && !this.tRex.ducking) {
2928 this.playSound(this.soundFx.BUTTON_PRESS);
2929 this.tRex.startJump(this.currentSpeed);
2930 }
2931 } else if (this.playing && Runner.keycodes.DUCK[e.keyCode]) {
2932 e.preventDefault();
2933 if (this.tRex.jumping) {
2934 // Speed drop, activated only when jump key is not pressed.
2935 this.tRex.setSpeedDrop();
2936 } else if (!this.tRex.jumping && !this.tRex.ducking) {
2937 // Duck.
2938 this.tRex.setDuck(true);
2939 }
2940 }
2941 // iOS only triggers touchstart and no pointer events.
2942 } else if (
2943 IS_IOS && this.crashed && e.type === Runner.events.TOUCHSTART &&
2944 e.currentTarget === this.containerEl) {
2945 this.handleGameOverClicks(e);
2946 }
2947 }
2948 },
2949
2950 /**
2951 * Process key up.
2952 * @param {Event} e
2953 */
2954 onKeyUp(e) {
2955 const keyCode = String(e.keyCode);
2956 const isjumpKey = Runner.keycodes.JUMP[keyCode] ||
2957 e.type === Runner.events.TOUCHEND || e.type === Runner.events.POINTERUP;
2958
2959 if (this.isRunning() && isjumpKey) {
2960 this.tRex.endJump();
2961 } else if (Runner.keycodes.DUCK[keyCode]) {
2962 this.tRex.speedDrop = false;
2963 this.tRex.setDuck(false);
2964 } else if (this.crashed) {
2965 // Check that enough time has elapsed before allowing jump key to restart.
2966 const deltaTime = getTimeStamp() - this.time;
2967
2968 if (this.isCanvasInView() &&
2969 (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
2970 (deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
2971 Runner.keycodes.JUMP[keyCode]))) {
2972 this.handleGameOverClicks(e);
2973 }
2974 } else if (this.paused && isjumpKey) {
2975 // Reset the jump state
2976 this.tRex.reset();
2977 this.play();
2978 }
2979 },
2980
2981 /**
2982 * Process gamepad connected event.
2983 * @param {Event} e
2984 */
2985 onGamepadConnected(e) {
2986 if (!this.pollingGamepads) {
2987 this.pollGamepadState();
2988 }
2989 },
2990
2991 /**
2992 * rAF loop for gamepad polling.
2993 */
2994 pollGamepadState() {
2995 const gamepads = navigator.getGamepads();
2996 this.pollActiveGamepad(gamepads);
2997
2998 this.pollingGamepads = true;
2999 requestAnimationFrame(this.pollGamepadState.bind(this));
3000 },
3001
3002 /**
3003 * Polls for a gamepad with the jump button pressed. If one is found this
3004 * becomes the "active" gamepad and all others are ignored.
3005 * @param {!Array<Gamepad>} gamepads
3006 */
3007 pollForActiveGamepad(gamepads) {
3008 for (let i = 0; i < gamepads.length; ++i) {
3009 if (gamepads[i] && gamepads[i].buttons.length > 0 &&
3010 gamepads[i].buttons[0].pressed) {
3011 this.gamepadIndex = i;
3012 this.pollActiveGamepad(gamepads);
3013 return;
3014 }
3015 }
3016 },
3017
3018 /**
3019 * Polls the chosen gamepad for button presses and generates KeyboardEvents
3020 * to integrate with the rest of the game logic.
3021 * @param {!Array<Gamepad>} gamepads
3022 */
3023 pollActiveGamepad(gamepads) {
3024 if (this.gamepadIndex === undefined) {
3025 this.pollForActiveGamepad(gamepads);
3026 return;
3027 }
3028
3029 const gamepad = gamepads[this.gamepadIndex];
3030 if (!gamepad) {
3031 this.gamepadIndex = undefined;
3032 this.pollForActiveGamepad(gamepads);
3033 return;
3034 }
3035
3036 // The gamepad specification defines the typical mapping of physical buttons
3037 // to button indicies: https://w3c.github.io/gamepad/#remapping
3038 this.pollGamepadButton(gamepad, 0, 38); // Jump
3039 if (gamepad.buttons.length >= 2) {
3040 this.pollGamepadButton(gamepad, 1, 40); // Duck
3041 }
3042 if (gamepad.buttons.length >= 10) {
3043 this.pollGamepadButton(gamepad, 9, 13); // Restart
3044 }
3045
3046 this.previousGamepad = gamepad;
3047 },
3048
3049 /**
3050 * Generates a key event based on a gamepad button.
3051 * @param {!Gamepad} gamepad
3052 * @param {number} buttonIndex
3053 * @param {number} keyCode
3054 */
3055 pollGamepadButton(gamepad, buttonIndex, keyCode) {
3056 const state = gamepad.buttons[buttonIndex].pressed;
3057 let previousState = false;
3058 if (this.previousGamepad) {
3059 previousState = this.previousGamepad.buttons[buttonIndex].pressed;
3060 }
3061 // Generate key events on the rising and falling edge of a button press.
3062 if (state !== previousState) {
3063 const e = new KeyboardEvent(state ? Runner.events.KEYDOWN
3064 : Runner.events.KEYUP,
3065 { keyCode: keyCode });
3066 document.dispatchEvent(e);
3067 }
3068 },
3069
3070 /**
3071 * Handle interactions on the game over screen state.
3072 * A user is able to tap the high score twice to reset it.
3073 * @param {Event} e
3074 */
3075 handleGameOverClicks(e) {
3076 e.preventDefault();
3077 if (this.distanceMeter.hasClickedOnHighScore(e) && this.highestScore) {
3078 if (this.distanceMeter.isHighScoreFlashing()) {
3079 // Subsequent click, reset the high score.
3080 this.saveHighScore(0, true);
3081 this.distanceMeter.resetHighScore();
3082 } else {
3083 // First click, flash the high score.
3084 this.distanceMeter.startHighScoreFlashing();
3085 }
3086 } else {
3087 this.distanceMeter.cancelHighScoreFlashing();
3088 this.restart();
3089 }
3090 },
3091
3092 /**
3093 * Returns whether the event was a left click on canvas.
3094 * On Windows right click is registered as a click.
3095 * @param {Event} e
3096 * @return {boolean}
3097 */
3098 isLeftClickOnCanvas(e) {
3099 return e.button != null && e.button < 2 &&
3100 e.type === Runner.events.POINTERUP && e.target === this.canvas;
3101 },
3102
3103 /**
3104 * RequestAnimationFrame wrapper.
3105 */
3106 scheduleNextUpdate() {
3107 if (!this.updatePending) {
3108 this.updatePending = true;
3109 this.raqId = requestAnimationFrame(this.update.bind(this));
3110 }
3111 },
3112
3113 /**
3114 * Whether the game is running.
3115 * @return {boolean}
3116 */
3117 isRunning() {
3118 return !!this.raqId;
3119 },
3120
3121 /**
3122 * Set the initial high score as stored in the user's profile.
3123 * @param {number} highScore
3124 */
3125 initializeHighScore(highScore) {
3126 this.syncHighestScore = true;
3127 highScore = Math.ceil(highScore);
3128 if (highScore < this.highestScore) {
3129 if (window.errorPageController) {
3130 errorPageController.updateEasterEggHighScore(this.highestScore);
3131 }
3132 return;
3133 }
3134 this.highestScore = highScore;
3135 this.distanceMeter.setHighScore(this.highestScore);
3136 },
3137
3138 /**
3139 * Sets the current high score and saves to the profile if available.
3140 * @param {number} distanceRan Total distance ran.
3141 * @param {boolean=} opt_resetScore Whether to reset the score.
3142 */
3143 saveHighScore(distanceRan, opt_resetScore) {
3144 this.highestScore = Math.ceil(distanceRan);
3145 this.distanceMeter.setHighScore(this.highestScore);
3146
3147 // Store the new high score in the profile.
3148 if (this.syncHighestScore && window.errorPageController) {
3149 if (opt_resetScore) {
3150 errorPageController.resetEasterEggHighScore();
3151 } else {
3152 errorPageController.updateEasterEggHighScore(this.highestScore);
3153 }
3154 }
3155 },
3156
3157 /**
3158 * Game over state.
3159 */
3160 gameOver() {
3161 this.playSound(this.soundFx.HIT);
3162 vibrate(200);
3163
3164 this.stop();
3165 this.crashed = true;
3166 this.distanceMeter.achievement = false;
3167
3168 this.tRex.update(100, Trex.status.CRASHED);
3169
3170 // Game over panel.
3171 if (!this.gameOverPanel) {
3172 if (this.canvas) {
3173 this.gameOverPanel = new GameOverPanel(this.canvas,
3174 this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
3175 this.dimensions);
3176 }
3177 } else {
3178 this.gameOverPanel.draw();
3179 }
3180
3181 // Update the high score.
3182 if (this.distanceRan > this.highestScore) {
3183 this.saveHighScore(this.distanceRan);
3184 }
3185
3186 // Reset the time clock.
3187 this.time = getTimeStamp();
3188 },
3189
3190 stop() {
3191 this.setPlayStatus(false);
3192 this.paused = true;
3193 cancelAnimationFrame(this.raqId);
3194 this.raqId = 0;
3195 },
3196
3197 play() {
3198 if (!this.crashed) {
3199 this.setPlayStatus(true);
3200 this.paused = false;
3201 this.tRex.update(0, Trex.status.RUNNING);
3202 this.time = getTimeStamp();
3203 this.update();
3204 }
3205 },
3206
3207 restart() {
3208 if (!this.raqId) {
3209 this.playCount++;
3210 this.runningTime = 0;
3211 this.setPlayStatus(true);
3212 this.paused = false;
3213 this.crashed = false;
3214 this.distanceRan = 0;
3215 this.setSpeed(this.config.SPEED);
3216 this.time = getTimeStamp();
3217 this.containerEl.classList.remove(Runner.classes.CRASHED);
3218 this.clearCanvas();
3219 this.distanceMeter.reset();
3220 this.horizon.reset();
3221 this.tRex.reset();
3222 this.playSound(this.soundFx.BUTTON_PRESS);
3223 this.invert(true);
3224 this.bdayFlashTimer = null;
3225 this.update();
3226 }
3227 },
3228
3229 setPlayStatus(isPlaying) {
3230 if (this.touchController) {
3231 this.touchController.classList.toggle(HIDDEN_CLASS, !isPlaying);
3232 }
3233 this.playing = isPlaying;
3234 },
3235
3236 /**
3237 * Whether the game should go into arcade mode.
3238 * @return {boolean}
3239 */
3240 isArcadeMode() {
3241 return document.title === ARCADE_MODE_URL;
3242 },
3243
3244 /**
3245 * Hides offline messaging for a fullscreen game only experience.
3246 */
3247 setArcadeMode() {
3248 document.body.classList.add(Runner.classes.ARCADE_MODE);
3249 this.setArcadeModeContainerScale();
3250 },
3251
3252 /**
3253 * Sets the scaling for arcade mode.
3254 */
3255 setArcadeModeContainerScale() {
3256 const windowHeight = window.innerHeight;
3257 const scaleHeight = windowHeight / this.dimensions.HEIGHT;
3258 const scaleWidth = window.innerWidth / this.dimensions.WIDTH;
3259 const scale = Math.max(1, Math.min(scaleHeight, scaleWidth));
3260 const scaledCanvasHeight = this.dimensions.HEIGHT * scale;
3261 // Positions the game container at 10% of the available vertical window
3262 // height minus the game container height.
3263 const translateY = Math.ceil(Math.max(0, (windowHeight - scaledCanvasHeight -
3264 Runner.config.ARCADE_MODE_INITIAL_TOP_POSITION) *
3265 Runner.config.ARCADE_MODE_TOP_POSITION_PERCENT)) *
3266 window.devicePixelRatio;
3267 this.containerEl.style.transform = 'scale(' + scale + ') translateY(' +
3268 translateY + 'px)';
3269 },
3270
3271 /**
3272 * Pause the game if the tab is not in focus.
3273 */
3274 onVisibilityChange(e) {
3275 if (document.hidden || document.webkitHidden || e.type === 'blur' ||
3276 document.visibilityState !== 'visible') {
3277 this.stop();
3278 } else if (!this.crashed) {
3279 this.tRex.reset();
3280 this.play();
3281 }
3282 },
3283
3284 /**
3285 * Play a sound.
3286 * @param {AudioBuffer} soundBuffer
3287 */
3288 playSound(soundBuffer) {
3289 if (soundBuffer) {
3290 const sourceNode = this.audioContext.createBufferSource();
3291 sourceNode.buffer = soundBuffer;
3292 sourceNode.connect(this.audioContext.destination);
3293 sourceNode.start(0);
3294 }
3295 },
3296
3297 /**
3298 * Inverts the current page / canvas colors.
3299 * @param {boolean} reset Whether to reset colors.
3300 */
3301 invert(reset) {
3302 const htmlEl = document.firstElementChild;
3303
3304 if (reset) {
3305 htmlEl.classList.toggle(Runner.classes.INVERTED,
3306 false);
3307 this.invertTimer = 0;
3308 this.inverted = false;
3309 } else {
3310 this.inverted = htmlEl.classList.toggle(
3311 Runner.classes.INVERTED, this.invertTrigger);
3312 }
3313 }
3314};
3315
3316
3317/**
3318 * Updates the canvas size taking into
3319 * account the backing store pixel ratio and
3320 * the device pixel ratio.
3321 *
3322 * See article by Paul Lewis:
3323 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
3324 *
3325 * @param {HTMLCanvasElement} canvas
3326 * @param {number=} opt_width
3327 * @param {number=} opt_height
3328 * @return {boolean} Whether the canvas was scaled.
3329 */
3330Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
3331 const context =
3332 /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
3333
3334 // Query the various pixel ratios
3335 const devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
3336 /** @suppress {missingProperties} */
3337 const backingStoreRatio =
3338 Math.floor(context.webkitBackingStorePixelRatio) || 1;
3339 const ratio = devicePixelRatio / backingStoreRatio;
3340
3341 // Upscale the canvas if the two ratios don't match
3342 if (devicePixelRatio !== backingStoreRatio) {
3343 const oldWidth = opt_width || canvas.width;
3344 const oldHeight = opt_height || canvas.height;
3345
3346 canvas.width = oldWidth * ratio;
3347 canvas.height = oldHeight * ratio;
3348
3349 canvas.style.width = oldWidth + 'px';
3350 canvas.style.height = oldHeight + 'px';
3351
3352 // Scale the context to counter the fact that we've manually scaled
3353 // our canvas element.
3354 context.scale(ratio, ratio);
3355 return true;
3356 } else if (devicePixelRatio === 1) {
3357 // Reset the canvas width / height. Fixes scaling bug when the page is
3358 // zoomed and the devicePixelRatio changes accordingly.
3359 canvas.style.width = canvas.width + 'px';
3360 canvas.style.height = canvas.height + 'px';
3361 }
3362 return false;
3363};
3364
3365
3366/**
3367 * Get random number.
3368 * @param {number} min
3369 * @param {number} max
3370 */
3371function getRandomNum(min, max) {
3372 return Math.floor(Math.random() * (max - min + 1)) + min;
3373}
3374
3375
3376/**
3377 * Vibrate on mobile devices.
3378 * @param {number} duration Duration of the vibration in milliseconds.
3379 */
3380function vibrate(duration) {
3381 if (IS_MOBILE && window.navigator.vibrate) {
3382 window.navigator.vibrate(duration);
3383 }
3384}
3385
3386
3387/**
3388 * Create canvas element.
3389 * @param {Element} container Element to append canvas to.
3390 * @param {number} width
3391 * @param {number} height
3392 * @param {string=} opt_classname
3393 * @return {HTMLCanvasElement}
3394 */
3395function createCanvas(container, width, height, opt_classname) {
3396 const canvas =
3397 /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
3398 canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
3399 opt_classname : Runner.classes.CANVAS;
3400 canvas.width = width;
3401 canvas.height = height;
3402 container.appendChild(canvas);
3403
3404 return canvas;
3405}
3406
3407
3408/**
3409 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
3410 * @param {string} base64String
3411 */
3412function decodeBase64ToArrayBuffer(base64String) {
3413 const len = (base64String.length / 4) * 3;
3414 const str = atob(base64String);
3415 const arrayBuffer = new ArrayBuffer(len);
3416 const bytes = new Uint8Array(arrayBuffer);
3417
3418 for (let i = 0; i < len; i++) {
3419 bytes[i] = str.charCodeAt(i);
3420 }
3421 return bytes.buffer;
3422}
3423
3424
3425/**
3426 * Return the current timestamp.
3427 * @return {number}
3428 */
3429function getTimeStamp() {
3430 return IS_IOS ? new Date().getTime() : performance.now();
3431}
3432
3433
3434//******************************************************************************
3435
3436
3437/**
3438 * Game over panel.
3439 * @param {!HTMLCanvasElement} canvas
3440 * @param {Object} textImgPos
3441 * @param {Object} restartImgPos
3442 * @param {!Object} dimensions Canvas dimensions.
3443 * @constructor
3444 */
3445function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
3446 this.canvas = canvas;
3447 this.canvasCtx =
3448 /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
3449 this.canvasDimensions = dimensions;
3450 this.textImgPos = textImgPos;
3451 this.restartImgPos = restartImgPos;
3452 this.draw();
3453}
3454
3455
3456/**
3457 * Dimensions used in the panel.
3458 * @enum {number}
3459 */
3460GameOverPanel.dimensions = {
3461 TEXT_X: 0,
3462 TEXT_Y: 13,
3463 TEXT_WIDTH: 191,
3464 TEXT_HEIGHT: 11,
3465 RESTART_WIDTH: 36,
3466 RESTART_HEIGHT: 32
3467};
3468
3469
3470GameOverPanel.prototype = {
3471 /**
3472 * Update the panel dimensions.
3473 * @param {number} width New canvas width.
3474 * @param {number} opt_height Optional new canvas height.
3475 */
3476 updateDimensions(width, opt_height) {
3477 this.canvasDimensions.WIDTH = width;
3478 if (opt_height) {
3479 this.canvasDimensions.HEIGHT = opt_height;
3480 }
3481 },
3482
3483 /**
3484 * Draw the panel.
3485 */
3486 draw() {
3487 const dimensions = GameOverPanel.dimensions;
3488
3489 const centerX = this.canvasDimensions.WIDTH / 2;
3490
3491 // Game over text.
3492 let textSourceX = dimensions.TEXT_X;
3493 let textSourceY = dimensions.TEXT_Y;
3494 let textSourceWidth = dimensions.TEXT_WIDTH;
3495 let textSourceHeight = dimensions.TEXT_HEIGHT;
3496
3497 const textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
3498 const textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
3499 const textTargetWidth = dimensions.TEXT_WIDTH;
3500 const textTargetHeight = dimensions.TEXT_HEIGHT;
3501
3502 let restartSourceWidth = dimensions.RESTART_WIDTH;
3503 let restartSourceHeight = dimensions.RESTART_HEIGHT;
3504 const restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
3505 const restartTargetY = this.canvasDimensions.HEIGHT / 2;
3506
3507 if (IS_HIDPI) {
3508 textSourceY *= 2;
3509 textSourceX *= 2;
3510 textSourceWidth *= 2;
3511 textSourceHeight *= 2;
3512 restartSourceWidth *= 2;
3513 restartSourceHeight *= 2;
3514 }
3515
3516 textSourceX += this.textImgPos.x;
3517 textSourceY += this.textImgPos.y;
3518
3519 // Game over text from sprite.
3520 this.canvasCtx.drawImage(Runner.imageSprite,
3521 textSourceX, textSourceY, textSourceWidth, textSourceHeight,
3522 textTargetX, textTargetY, textTargetWidth, textTargetHeight);
3523
3524 // Restart button.
3525 this.canvasCtx.drawImage(Runner.imageSprite,
3526 this.restartImgPos.x, this.restartImgPos.y,
3527 restartSourceWidth, restartSourceHeight,
3528 restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
3529 dimensions.RESTART_HEIGHT);
3530 }
3531};
3532
3533
3534//******************************************************************************
3535
3536/**
3537 * Check for a collision.
3538 * @param {!Obstacle} obstacle
3539 * @param {!Trex} tRex T-rex object.
3540 * @param {CanvasRenderingContext2D=} opt_canvasCtx Optional canvas context for
3541 * drawing collision boxes.
3542 * @return {Array<CollisionBox>|undefined}
3543 */
3544function checkForCollision(obstacle, tRex, opt_canvasCtx) {
3545 const obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
3546
3547 // Adjustments are made to the bounding box as there is a 1 pixel white
3548 // border around the t-rex and obstacles.
3549 const tRexBox = new CollisionBox(
3550 tRex.xPos + 1,
3551 tRex.yPos + 1,
3552 tRex.config.WIDTH - 2,
3553 tRex.config.HEIGHT - 2);
3554
3555 const obstacleBox = new CollisionBox(
3556 obstacle.xPos + 1,
3557 obstacle.yPos + 1,
3558 obstacle.typeConfig.width * obstacle.size - 2,
3559 obstacle.typeConfig.height - 2);
3560
3561 // Debug outer box
3562 if (opt_canvasCtx) {
3563 drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
3564 }
3565
3566 // Simple outer bounds check.
3567 if (boxCompare(tRexBox, obstacleBox)) {
3568 const collisionBoxes = obstacle.collisionBoxes;
3569 const tRexCollisionBoxes = tRex.ducking ?
3570 Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
3571
3572 // Detailed axis aligned box check.
3573 for (let t = 0; t < tRexCollisionBoxes.length; t++) {
3574 for (let i = 0; i < collisionBoxes.length; i++) {
3575 // Adjust the box to actual positions.
3576 const adjTrexBox =
3577 createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
3578 const adjObstacleBox =
3579 createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
3580 const crashed = boxCompare(adjTrexBox, adjObstacleBox);
3581
3582 // Draw boxes for debug.
3583 if (opt_canvasCtx) {
3584 drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
3585 }
3586
3587 if (crashed) {
3588 return [adjTrexBox, adjObstacleBox];
3589 }
3590 }
3591 }
3592 }
3593}
3594
3595
3596/**
3597 * Adjust the collision box.
3598 * @param {!CollisionBox} box The original box.
3599 * @param {!CollisionBox} adjustment Adjustment box.
3600 * @return {CollisionBox} The adjusted collision box object.
3601 */
3602function createAdjustedCollisionBox(box, adjustment) {
3603 return new CollisionBox(
3604 box.x + adjustment.x,
3605 box.y + adjustment.y,
3606 box.width,
3607 box.height);
3608}
3609
3610
3611/**
3612 * Draw the collision boxes for debug.
3613 */
3614function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
3615 canvasCtx.save();
3616 canvasCtx.strokeStyle = '#f00';
3617 canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
3618
3619 canvasCtx.strokeStyle = '#0f0';
3620 canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
3621 obstacleBox.width, obstacleBox.height);
3622 canvasCtx.restore();
3623}
3624
3625
3626/**
3627 * Compare two collision boxes for a collision.
3628 * @param {CollisionBox} tRexBox
3629 * @param {CollisionBox} obstacleBox
3630 * @return {boolean} Whether the boxes intersected.
3631 */
3632function boxCompare(tRexBox, obstacleBox) {
3633 let crashed = false;
3634 const tRexBoxX = tRexBox.x;
3635 const tRexBoxY = tRexBox.y;
3636
3637 const obstacleBoxX = obstacleBox.x;
3638 const obstacleBoxY = obstacleBox.y;
3639
3640 // Axis-Aligned Bounding Box method.
3641 if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
3642 tRexBox.x + tRexBox.width > obstacleBoxX &&
3643 tRexBox.y < obstacleBox.y + obstacleBox.height &&
3644 tRexBox.height + tRexBox.y > obstacleBox.y) {
3645 crashed = true;
3646 }
3647
3648 return crashed;
3649}
3650
3651
3652//******************************************************************************
3653
3654/**
3655 * Collision box object.
3656 * @param {number} x X position.
3657 * @param {number} y Y Position.
3658 * @param {number} w Width.
3659 * @param {number} h Height.
3660 * @constructor
3661 */
3662function CollisionBox(x, y, w, h) {
3663 this.x = x;
3664 this.y = y;
3665 this.width = w;
3666 this.height = h;
3667}
3668
3669
3670//******************************************************************************
3671
3672/**
3673 * Obstacle.
3674 * @param {CanvasRenderingContext2D} canvasCtx
3675 * @param {ObstacleType} type
3676 * @param {Object} spriteImgPos Obstacle position in sprite.
3677 * @param {Object} dimensions
3678 * @param {number} gapCoefficient Mutipler in determining the gap.
3679 * @param {number} speed
3680 * @param {number=} opt_xOffset
3681 * @constructor
3682 */
3683function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
3684 gapCoefficient, speed, opt_xOffset) {
3685
3686 this.canvasCtx = canvasCtx;
3687 this.spritePos = spriteImgPos;
3688 this.typeConfig = type;
3689 this.gapCoefficient = gapCoefficient;
3690 this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
3691 this.dimensions = dimensions;
3692 this.remove = false;
3693 this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
3694 this.yPos = 0;
3695 this.width = 0;
3696 this.collisionBoxes = [];
3697 this.gap = 0;
3698 this.speedOffset = 0;
3699
3700 // For animated obstacles.
3701 this.currentFrame = 0;
3702 this.timer = 0;
3703
3704 this.init(speed);
3705}
3706
3707/**
3708 * Coefficient for calculating the maximum gap.
3709 * @const
3710 */
3711Obstacle.MAX_GAP_COEFFICIENT = 1.5;
3712
3713/**
3714 * Maximum obstacle grouping count.
3715 * @const
3716 */
3717Obstacle.MAX_OBSTACLE_LENGTH = 3;
3718
3719
3720Obstacle.prototype = {
3721 /**
3722 * Initialise the DOM for the obstacle.
3723 * @param {number} speed
3724 */
3725 init(speed) {
3726 this.cloneCollisionBoxes();
3727
3728 // Only allow sizing if we're at the right speed.
3729 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
3730 this.size = 1;
3731 }
3732
3733 this.width = this.typeConfig.width * this.size;
3734
3735 // Check if obstacle can be positioned at various heights.
3736 if (Array.isArray(this.typeConfig.yPos)) {
3737 const yPosConfig =
3738 IS_MOBILE ? this.typeConfig.yPosMobile : this.typeConfig.yPos;
3739 this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
3740 } else {
3741 this.yPos = this.typeConfig.yPos;
3742 }
3743
3744 this.draw();
3745
3746 // Make collision box adjustments,
3747 // Central box is adjusted to the size as one box.
3748 // ____ ______ ________
3749 // _| |-| _| |-| _| |-|
3750 // | |<->| | | |<--->| | | |<----->| |
3751 // | | 1 | | | | 2 | | | | 3 | |
3752 // |_|___|_| |_|_____|_| |_|_______|_|
3753 //
3754 if (this.size > 1) {
3755 this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
3756 this.collisionBoxes[2].width;
3757 this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
3758 }
3759
3760 // For obstacles that go at a different speed from the horizon.
3761 if (this.typeConfig.speedOffset) {
3762 this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
3763 -this.typeConfig.speedOffset;
3764 }
3765
3766 this.gap = this.getGap(this.gapCoefficient, speed);
3767 },
3768
3769 /**
3770 * Draw and crop based on size.
3771 */
3772 draw() {
3773 let sourceWidth = this.typeConfig.width;
3774 let sourceHeight = this.typeConfig.height;
3775
3776 if (IS_HIDPI) {
3777 sourceWidth = sourceWidth * 2;
3778 sourceHeight = sourceHeight * 2;
3779 }
3780
3781 // X position in sprite.
3782 let sourceX =
3783 (sourceWidth * this.size) * (0.5 * (this.size - 1)) + this.spritePos.x;
3784
3785 // Animation frames.
3786 if (this.currentFrame > 0) {
3787 sourceX += sourceWidth * this.currentFrame;
3788 }
3789
3790 this.canvasCtx.drawImage(
3791 Runner.imageSprite, sourceX, this.spritePos.y, sourceWidth * this.size,
3792 sourceHeight, this.xPos, this.yPos, this.typeConfig.width * this.size,
3793 this.typeConfig.height);
3794 },
3795
3796 /**
3797 * Obstacle frame update.
3798 * @param {number} deltaTime
3799 * @param {number} speed
3800 */
3801 update(deltaTime, speed) {
3802 if (!this.remove) {
3803 if (this.typeConfig.speedOffset) {
3804 speed += this.speedOffset;
3805 }
3806 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
3807
3808 // Update frame
3809 if (this.typeConfig.numFrames) {
3810 this.timer += deltaTime;
3811 if (this.timer >= this.typeConfig.frameRate) {
3812 this.currentFrame =
3813 this.currentFrame === this.typeConfig.numFrames - 1 ?
3814 0 :
3815 this.currentFrame + 1;
3816 this.timer = 0;
3817 }
3818 }
3819 this.draw();
3820
3821 if (!this.isVisible()) {
3822 this.remove = true;
3823 }
3824 }
3825 },
3826
3827 /**
3828 * Calculate a random gap size.
3829 * - Minimum gap gets wider as speed increses
3830 * @param {number} gapCoefficient
3831 * @param {number} speed
3832 * @return {number} The gap size.
3833 */
3834 getGap(gapCoefficient, speed) {
3835 const minGap = Math.round(
3836 this.width * speed + this.typeConfig.minGap * gapCoefficient);
3837 const maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
3838 return getRandomNum(minGap, maxGap);
3839 },
3840
3841 /**
3842 * Check if obstacle is visible.
3843 * @return {boolean} Whether the obstacle is in the game area.
3844 */
3845 isVisible() {
3846 return this.xPos + this.width > 0;
3847 },
3848
3849 /**
3850 * Make a copy of the collision boxes, since these will change based on
3851 * obstacle type and size.
3852 */
3853 cloneCollisionBoxes() {
3854 const collisionBoxes = this.typeConfig.collisionBoxes;
3855
3856 for (let i = collisionBoxes.length - 1; i >= 0; i--) {
3857 this.collisionBoxes[i] = new CollisionBox(
3858 collisionBoxes[i].x, collisionBoxes[i].y, collisionBoxes[i].width,
3859 collisionBoxes[i].height);
3860 }
3861 }
3862};
3863
3864/**
3865 * Obstacle definitions.
3866 * minGap: minimum pixel space betweeen obstacles.
3867 * multipleSpeed: Speed at which multiples are allowed.
3868 * speedOffset: speed faster / slower than the horizon.
3869 * minSpeed: Minimum speed which the obstacle can make an appearance.
3870 *
3871 * @typedef {{
3872 * type: string,
3873 * width: number,
3874 * height: number,
3875 * yPos: number,
3876 * multipleSpeed: number,
3877 * minGap: number,
3878 * minSpeed: number,
3879 * collisionBoxes: Array<CollisionBox>,
3880 * }}
3881 */
3882let ObstacleType;
3883
3884/** @type {Array<ObstacleType>} */
3885Obstacle.types = [
3886 {
3887 type: 'CACTUS_SMALL',
3888 width: 17,
3889 height: 35,
3890 yPos: 105,
3891 multipleSpeed: 4,
3892 minGap: 120,
3893 minSpeed: 0,
3894 collisionBoxes: [
3895 new CollisionBox(0, 7, 5, 27),
3896 new CollisionBox(4, 0, 6, 34),
3897 new CollisionBox(10, 4, 7, 14)
3898 ]
3899 },
3900 {
3901 type: 'CACTUS_LARGE',
3902 width: 25,
3903 height: 50,
3904 yPos: 90,
3905 multipleSpeed: 7,
3906 minGap: 120,
3907 minSpeed: 0,
3908 collisionBoxes: [
3909 new CollisionBox(0, 12, 7, 38),
3910 new CollisionBox(8, 0, 7, 49),
3911 new CollisionBox(13, 10, 10, 38)
3912 ]
3913 },
3914 {
3915 type: 'PTERODACTYL',
3916 width: 46,
3917 height: 40,
3918 yPos: [ 100, 75, 50 ], // Variable height.
3919 yPosMobile: [ 100, 50 ], // Variable height mobile.
3920 multipleSpeed: 999,
3921 minSpeed: 8.5,
3922 minGap: 150,
3923 collisionBoxes: [
3924 new CollisionBox(15, 15, 16, 5),
3925 new CollisionBox(18, 21, 24, 6),
3926 new CollisionBox(2, 14, 4, 3),
3927 new CollisionBox(6, 10, 4, 7),
3928 new CollisionBox(10, 8, 6, 9)
3929 ],
3930 numFrames: 2,
3931 frameRate: 1000/6,
3932 speedOffset: .8
3933 }
3934];
3935
3936
3937//******************************************************************************
3938/**
3939 * T-rex game character.
3940 * @param {HTMLCanvasElement} canvas
3941 * @param {Object} spritePos Positioning within image sprite.
3942 * @constructor
3943 */
3944function Trex(canvas, spritePos) {
3945 this.canvas = canvas;
3946 this.canvasCtx =
3947 /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
3948 this.spritePos = spritePos;
3949 this.xPos = 0;
3950 this.yPos = 0;
3951 this.xInitialPos = 0;
3952 // Position when on the ground.
3953 this.groundYPos = 0;
3954 this.currentFrame = 0;
3955 this.currentAnimFrames = [];
3956 this.blinkDelay = 0;
3957 this.blinkCount = 0;
3958 this.animStartTime = 0;
3959 this.timer = 0;
3960 this.msPerFrame = 1000 / FPS;
3961 this.config = Trex.config;
3962 // Current status.
3963 this.status = Trex.status.WAITING;
3964 this.jumping = false;
3965 this.ducking = false;
3966 this.jumpVelocity = 0;
3967 this.reachedMinHeight = false;
3968 this.speedDrop = false;
3969 this.jumpCount = 0;
3970 this.jumpspotX = 0;
3971
3972 this.init();
3973}
3974
3975
3976/**
3977 * T-rex player config.
3978 */
3979Trex.config = {
3980 DROP_VELOCITY: -5,
3981 GRAVITY: 0.6,
3982 HEIGHT: 47,
3983 HEIGHT_DUCK: 25,
3984 INIITAL_JUMP_VELOCITY: -10,
3985 INTRO_DURATION: 1500,
3986 MAX_JUMP_HEIGHT: 30,
3987 MIN_JUMP_HEIGHT: 30,
3988 SPEED_DROP_COEFFICIENT: 3,
3989 SPRITE_WIDTH: 262,
3990 START_X_POS: 50,
3991 WIDTH: 44,
3992 WIDTH_DUCK: 59
3993};
3994
3995
3996/**
3997 * Used in collision detection.
3998 * @enum {Array<CollisionBox>}
3999 */
4000Trex.collisionBoxes = {
4001 DUCKING: [
4002 new CollisionBox(1, 18, 55, 25)
4003 ],
4004 RUNNING: [
4005 new CollisionBox(22, 0, 17, 16),
4006 new CollisionBox(1, 18, 30, 9),
4007 new CollisionBox(10, 35, 14, 8),
4008 new CollisionBox(1, 24, 29, 5),
4009 new CollisionBox(5, 30, 21, 4),
4010 new CollisionBox(9, 34, 15, 4)
4011 ]
4012};
4013
4014
4015/**
4016 * Animation states.
4017 * @enum {string}
4018 */
4019Trex.status = {
4020 CRASHED: 'CRASHED',
4021 DUCKING: 'DUCKING',
4022 JUMPING: 'JUMPING',
4023 RUNNING: 'RUNNING',
4024 WAITING: 'WAITING'
4025};
4026
4027/**
4028 * Blinking coefficient.
4029 * @const
4030 */
4031Trex.BLINK_TIMING = 7000;
4032
4033
4034/**
4035 * Animation config for different states.
4036 * @enum {Object}
4037 */
4038Trex.animFrames = {
4039 WAITING: {
4040 frames: [44, 0],
4041 msPerFrame: 1000 / 3
4042 },
4043 RUNNING: {
4044 frames: [88, 132],
4045 msPerFrame: 1000 / 12
4046 },
4047 CRASHED: {
4048 frames: [220],
4049 msPerFrame: 1000 / 60
4050 },
4051 JUMPING: {
4052 frames: [0],
4053 msPerFrame: 1000 / 60
4054 },
4055 DUCKING: {
4056 frames: [264, 323],
4057 msPerFrame: 1000 / 8
4058 }
4059};
4060
4061
4062Trex.prototype = {
4063 /**
4064 * T-rex player initaliser.
4065 * Sets the t-rex to blink at random intervals.
4066 */
4067 init() {
4068 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
4069 Runner.config.BOTTOM_PAD;
4070 this.yPos = this.groundYPos;
4071 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
4072
4073 this.draw(0, 0);
4074 this.update(0, Trex.status.WAITING);
4075 },
4076
4077 /**
4078 * Setter for the jump velocity.
4079 * The approriate drop velocity is also set.
4080 * @param {number} setting
4081 */
4082 setJumpVelocity(setting) {
4083 this.config.INIITAL_JUMP_VELOCITY = -setting;
4084 this.config.DROP_VELOCITY = -setting / 2;
4085 },
4086
4087 /**
4088 * Set the animation status.
4089 * @param {!number} deltaTime
4090 * @param {Trex.status=} opt_status Optional status to switch to.
4091 */
4092 update(deltaTime, opt_status) {
4093 this.timer += deltaTime;
4094
4095 // Update the status.
4096 if (opt_status) {
4097 this.status = opt_status;
4098 this.currentFrame = 0;
4099 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
4100 this.currentAnimFrames = Trex.animFrames[opt_status].frames;
4101
4102 if (opt_status === Trex.status.WAITING) {
4103 this.animStartTime = getTimeStamp();
4104 this.setBlinkDelay();
4105 }
4106 }
4107
4108 // Game intro animation, T-rex moves in from the left.
4109 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
4110 this.xPos += Math.round((this.config.START_X_POS /
4111 this.config.INTRO_DURATION) * deltaTime);
4112 this.xInitialPos = this.xPos;
4113 }
4114
4115 if (this.status === Trex.status.WAITING) {
4116 this.blink(getTimeStamp());
4117 } else {
4118 this.draw(this.currentAnimFrames[this.currentFrame], 0);
4119 }
4120
4121 // Update the frame position.
4122 if (this.timer >= this.msPerFrame) {
4123 this.currentFrame = this.currentFrame ==
4124 this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
4125 this.timer = 0;
4126 }
4127
4128 // Speed drop becomes duck if the down key is still being pressed.
4129 if (this.speedDrop && this.yPos === this.groundYPos) {
4130 this.speedDrop = false;
4131 this.setDuck(true);
4132 }
4133 },
4134
4135 /**
4136 * Draw the t-rex to a particular position.
4137 * @param {number} x
4138 * @param {number} y
4139 */
4140 draw(x, y) {
4141 let sourceX = x;
4142 let sourceY = y;
4143 let sourceWidth = this.ducking && this.status !== Trex.status.CRASHED ?
4144 this.config.WIDTH_DUCK :
4145 this.config.WIDTH;
4146 let sourceHeight = this.config.HEIGHT;
4147 const outputHeight = sourceHeight;
4148
4149 if (IS_HIDPI) {
4150 sourceX *= 2;
4151 sourceY *= 2;
4152 sourceWidth *= 2;
4153 sourceHeight *= 2;
4154 }
4155
4156 // Adjustments for sprite sheet position.
4157 sourceX += this.spritePos.x;
4158 sourceY += this.spritePos.y;
4159
4160 // Ducking.
4161 if (this.ducking && this.status !== Trex.status.CRASHED) {
4162 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
4163 sourceWidth, sourceHeight,
4164 this.xPos, this.yPos,
4165 this.config.WIDTH_DUCK, outputHeight);
4166 } else {
4167 // Crashed whilst ducking. Trex is standing up so needs adjustment.
4168 if (this.ducking && this.status === Trex.status.CRASHED) {
4169 this.xPos++;
4170 }
4171 // Standing / running
4172 this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
4173 sourceWidth, sourceHeight,
4174 this.xPos, this.yPos,
4175 this.config.WIDTH, outputHeight);
4176 }
4177 this.canvasCtx.globalAlpha = 1;
4178 },
4179
4180 /**
4181 * Sets a random time for the blink to happen.
4182 */
4183 setBlinkDelay() {
4184 this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
4185 },
4186
4187 /**
4188 * Make t-rex blink at random intervals.
4189 * @param {number} time Current time in milliseconds.
4190 */
4191 blink(time) {
4192 const deltaTime = time - this.animStartTime;
4193
4194 if (deltaTime >= this.blinkDelay) {
4195 this.draw(this.currentAnimFrames[this.currentFrame], 0);
4196
4197 if (this.currentFrame === 1) {
4198 // Set new random delay to blink.
4199 this.setBlinkDelay();
4200 this.animStartTime = time;
4201 this.blinkCount++;
4202 }
4203 }
4204 },
4205
4206 /**
4207 * Initialise a jump.
4208 * @param {number} speed
4209 */
4210 startJump(speed) {
4211 if (!this.jumping) {
4212 this.update(0, Trex.status.JUMPING);
4213 // Tweak the jump velocity based on the speed.
4214 this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
4215 this.jumping = true;
4216 this.reachedMinHeight = false;
4217 this.speedDrop = false;
4218 }
4219 },
4220
4221 /**
4222 * Jump is complete, falling down.
4223 */
4224 endJump() {
4225 if (this.reachedMinHeight &&
4226 this.jumpVelocity < this.config.DROP_VELOCITY) {
4227 this.jumpVelocity = this.config.DROP_VELOCITY;
4228 }
4229 },
4230
4231 /**
4232 * Update frame for a jump.
4233 * @param {number} deltaTime
4234 */
4235 updateJump(deltaTime) {
4236 const msPerFrame = Trex.animFrames[this.status].msPerFrame;
4237 const framesElapsed = deltaTime / msPerFrame;
4238
4239 // Speed drop makes Trex fall faster.
4240 if (this.speedDrop) {
4241 this.yPos += Math.round(this.jumpVelocity *
4242 this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
4243 } else {
4244 this.yPos += Math.round(this.jumpVelocity * framesElapsed);
4245 }
4246
4247 this.jumpVelocity += this.config.GRAVITY * framesElapsed;
4248
4249 // Minimum height has been reached.
4250 if (this.yPos < this.minJumpHeight || this.speedDrop) {
4251 this.reachedMinHeight = true;
4252 }
4253
4254 // Reached max height
4255 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
4256 this.endJump();
4257 }
4258
4259 // Back down at ground level. Jump completed.
4260 if (this.yPos > this.groundYPos) {
4261 this.reset();
4262 this.jumpCount++;
4263 }
4264 },
4265
4266 /**
4267 * Set the speed drop. Immediately cancels the current jump.
4268 */
4269 setSpeedDrop() {
4270 this.speedDrop = true;
4271 this.jumpVelocity = 1;
4272 },
4273
4274 /**
4275 * @param {boolean} isDucking
4276 */
4277 setDuck(isDucking) {
4278 if (isDucking && this.status !== Trex.status.DUCKING) {
4279 this.update(0, Trex.status.DUCKING);
4280 this.ducking = true;
4281 } else if (this.status === Trex.status.DUCKING) {
4282 this.update(0, Trex.status.RUNNING);
4283 this.ducking = false;
4284 }
4285 },
4286
4287 /**
4288 * Reset the t-rex to running at start of game.
4289 */
4290 reset() {
4291 this.xPos = this.xInitialPos;
4292 this.yPos = this.groundYPos;
4293 this.jumpVelocity = 0;
4294 this.jumping = false;
4295 this.ducking = false;
4296 this.update(0, Trex.status.RUNNING);
4297 this.midair = false;
4298 this.speedDrop = false;
4299 this.jumpCount = 0;
4300 }
4301};
4302
4303
4304//******************************************************************************
4305
4306/**
4307 * Handles displaying the distance meter.
4308 * @param {!HTMLCanvasElement} canvas
4309 * @param {Object} spritePos Image position in sprite.
4310 * @param {number} canvasWidth
4311 * @constructor
4312 */
4313function DistanceMeter(canvas, spritePos, canvasWidth) {
4314 this.canvas = canvas;
4315 this.canvasCtx =
4316 /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
4317 this.image = Runner.imageSprite;
4318 this.spritePos = spritePos;
4319 this.x = 0;
4320 this.y = 5;
4321
4322 this.currentDistance = 0;
4323 this.maxScore = 0;
4324 this.highScore = '0';
4325 this.container = null;
4326
4327 this.digits = [];
4328 this.achievement = false;
4329 this.defaultString = '';
4330 this.flashTimer = 0;
4331 this.flashIterations = 0;
4332 this.invertTrigger = false;
4333 this.flashingRafId = null;
4334 this.highScoreBounds = {};
4335 this.highScoreFlashing = false;
4336
4337 this.config = DistanceMeter.config;
4338 this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
4339 this.init(canvasWidth);
4340}
4341
4342
4343/**
4344 * @enum {number}
4345 */
4346DistanceMeter.dimensions = {
4347 WIDTH: 10,
4348 HEIGHT: 13,
4349 DEST_WIDTH: 11
4350};
4351
4352
4353/**
4354 * Y positioning of the digits in the sprite sheet.
4355 * X position is always 0.
4356 * @type {Array<number>}
4357 */
4358DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
4359
4360
4361/**
4362 * Distance meter config.
4363 * @enum {number}
4364 */
4365DistanceMeter.config = {
4366 // Number of digits.
4367 MAX_DISTANCE_UNITS: 5,
4368
4369 // Distance that causes achievement animation.
4370 ACHIEVEMENT_DISTANCE: 100,
4371
4372 // Used for conversion from pixel distance to a scaled unit.
4373 COEFFICIENT: 0.025,
4374
4375 // Flash duration in milliseconds.
4376 FLASH_DURATION: 1000 / 4,
4377
4378 // Flash iterations for achievement animation.
4379 FLASH_ITERATIONS: 3,
4380
4381 // Padding around the high score hit area.
4382 HIGH_SCORE_HIT_AREA_PADDING: 4
4383};
4384
4385
4386DistanceMeter.prototype = {
4387 /**
4388 * Initialise the distance meter to '00000'.
4389 * @param {number} width Canvas width in px.
4390 */
4391 init(width) {
4392 let maxDistanceStr = '';
4393
4394 this.calcXPos(width);
4395 this.maxScore = this.maxScoreUnits;
4396 for (let i = 0; i < this.maxScoreUnits; i++) {
4397 this.draw(i, 0);
4398 this.defaultString += '0';
4399 maxDistanceStr += '9';
4400 }
4401
4402 this.maxScore = parseInt(maxDistanceStr, 10);
4403 },
4404
4405 /**
4406 * Calculate the xPos in the canvas.
4407 * @param {number} canvasWidth
4408 */
4409 calcXPos(canvasWidth) {
4410 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
4411 (this.maxScoreUnits + 1));
4412 },
4413
4414 /**
4415 * Draw a digit to canvas.
4416 * @param {number} digitPos Position of the digit.
4417 * @param {number} value Digit value 0-9.
4418 * @param {boolean=} opt_highScore Whether drawing the high score.
4419 */
4420 draw(digitPos, value, opt_highScore) {
4421 let sourceWidth = DistanceMeter.dimensions.WIDTH;
4422 let sourceHeight = DistanceMeter.dimensions.HEIGHT;
4423 let sourceX = DistanceMeter.dimensions.WIDTH * value;
4424 let sourceY = 0;
4425
4426 const targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
4427 const targetY = this.y;
4428 const targetWidth = DistanceMeter.dimensions.WIDTH;
4429 const targetHeight = DistanceMeter.dimensions.HEIGHT;
4430
4431 // For high DPI we 2x source values.
4432 if (IS_HIDPI) {
4433 sourceWidth *= 2;
4434 sourceHeight *= 2;
4435 sourceX *= 2;
4436 }
4437
4438 sourceX += this.spritePos.x;
4439 sourceY += this.spritePos.y;
4440
4441 this.canvasCtx.save();
4442
4443 if (opt_highScore) {
4444 // Left of the current score.
4445 const highScoreX = this.x - (this.maxScoreUnits * 2) *
4446 DistanceMeter.dimensions.WIDTH;
4447 this.canvasCtx.translate(highScoreX, this.y);
4448 } else {
4449 this.canvasCtx.translate(this.x, this.y);
4450 }
4451
4452 this.canvasCtx.drawImage(this.image, sourceX, sourceY,
4453 sourceWidth, sourceHeight,
4454 targetX, targetY,
4455 targetWidth, targetHeight
4456 );
4457
4458 this.canvasCtx.restore();
4459 },
4460
4461 /**
4462 * Covert pixel distance to a 'real' distance.
4463 * @param {number} distance Pixel distance ran.
4464 * @return {number} The 'real' distance ran.
4465 */
4466 getActualDistance(distance) {
4467 return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
4468 },
4469
4470 /**
4471 * Update the distance meter.
4472 * @param {number} distance
4473 * @param {number} deltaTime
4474 * @return {boolean} Whether the acheivement sound fx should be played.
4475 */
4476 update(deltaTime, distance) {
4477 let paint = true;
4478 let playSound = false;
4479
4480 if (!this.achievement) {
4481 distance = this.getActualDistance(distance);
4482 // Score has gone beyond the initial digit count.
4483 if (distance > this.maxScore && this.maxScoreUnits ==
4484 this.config.MAX_DISTANCE_UNITS) {
4485 this.maxScoreUnits++;
4486 this.maxScore = parseInt(this.maxScore + '9', 10);
4487 } else {
4488 this.distance = 0;
4489 }
4490
4491 if (distance > 0) {
4492 // Achievement unlocked.
4493 if (distance % this.config.ACHIEVEMENT_DISTANCE === 0) {
4494 // Flash score and play sound.
4495 this.achievement = true;
4496 this.flashTimer = 0;
4497 playSound = true;
4498 }
4499
4500 // Create a string representation of the distance with leading 0.
4501 const distanceStr = (this.defaultString +
4502 distance).substr(-this.maxScoreUnits);
4503 this.digits = distanceStr.split('');
4504 } else {
4505 this.digits = this.defaultString.split('');
4506 }
4507 } else {
4508 // Control flashing of the score on reaching acheivement.
4509 if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
4510 this.flashTimer += deltaTime;
4511
4512 if (this.flashTimer < this.config.FLASH_DURATION) {
4513 paint = false;
4514 } else if (this.flashTimer >
4515 this.config.FLASH_DURATION * 2) {
4516 this.flashTimer = 0;
4517 this.flashIterations++;
4518 }
4519 } else {
4520 this.achievement = false;
4521 this.flashIterations = 0;
4522 this.flashTimer = 0;
4523 }
4524 }
4525
4526 // Draw the digits if not flashing.
4527 if (paint) {
4528 for (let i = this.digits.length - 1; i >= 0; i--) {
4529 this.draw(i, parseInt(this.digits[i], 10));
4530 }
4531 }
4532
4533 this.drawHighScore();
4534 return playSound;
4535 },
4536
4537 /**
4538 * Draw the high score.
4539 */
4540 drawHighScore() {
4541 this.canvasCtx.save();
4542 this.canvasCtx.globalAlpha = .8;
4543 for (let i = this.highScore.length - 1; i >= 0; i--) {
4544 this.draw(i, parseInt(this.highScore[i], 10), true);
4545 }
4546 this.canvasCtx.restore();
4547 },
4548
4549 /**
4550 * Set the highscore as a array string.
4551 * Position of char in the sprite: H - 10, I - 11.
4552 * @param {number} distance Distance ran in pixels.
4553 */
4554 setHighScore(distance) {
4555 distance = this.getActualDistance(distance);
4556 const highScoreStr = (this.defaultString +
4557 distance).substr(-this.maxScoreUnits);
4558
4559 this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
4560 },
4561
4562
4563 /**
4564 * Whether a clicked is in the high score area.
4565 * @param {Event} e Event object.
4566 * @return {boolean} Whether the click was in the high score bounds.
4567 */
4568 hasClickedOnHighScore(e) {
4569 let x = 0;
4570 let y = 0;
4571
4572 if (e.touches) {
4573 // Bounds for touch differ from pointer.
4574 const canvasBounds = this.canvas.getBoundingClientRect();
4575 x = e.touches[0].clientX - canvasBounds.left;
4576 y = e.touches[0].clientY - canvasBounds.top;
4577 } else {
4578 x = e.offsetX;
4579 y = e.offsetY;
4580 }
4581
4582 this.highScoreBounds = this.getHighScoreBounds();
4583 return x >= this.highScoreBounds.x && x <=
4584 this.highScoreBounds.x + this.highScoreBounds.width &&
4585 y >= this.highScoreBounds.y && y <=
4586 this.highScoreBounds.y + this.highScoreBounds.height;
4587 },
4588
4589 /**
4590 * Get the bounding box for the high score.
4591 * @return {Object} Object with x, y, width and height properties.
4592 */
4593 getHighScoreBounds() {
4594 return {
4595 x: (this.x - (this.maxScoreUnits * 2) *
4596 DistanceMeter.dimensions.WIDTH) -
4597 DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
4598 y: this.y,
4599 width: DistanceMeter.dimensions.WIDTH * (this.highScore.length + 1) +
4600 DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING,
4601 height: DistanceMeter.dimensions.HEIGHT +
4602 (DistanceMeter.config.HIGH_SCORE_HIT_AREA_PADDING * 2)
4603 };
4604 },
4605
4606 /**
4607 * Animate flashing the high score to indicate ready for resetting.
4608 * The flashing stops following this.config.FLASH_ITERATIONS x 2 flashes.
4609 */
4610 flashHighScore() {
4611 const now = getTimeStamp();
4612 const deltaTime = now - (this.frameTimeStamp || now);
4613 let paint = true;
4614 this.frameTimeStamp = now;
4615
4616 // Reached the max number of flashes.
4617 if (this.flashIterations > this.config.FLASH_ITERATIONS * 2) {
4618 this.cancelHighScoreFlashing();
4619 return;
4620 }
4621
4622 this.flashTimer += deltaTime;
4623
4624 if (this.flashTimer < this.config.FLASH_DURATION) {
4625 paint = false;
4626 } else if (this.flashTimer > this.config.FLASH_DURATION * 2) {
4627 this.flashTimer = 0;
4628 this.flashIterations++;
4629 }
4630
4631 if (paint) {
4632 this.drawHighScore();
4633 } else {
4634 this.clearHighScoreBounds();
4635 }
4636 // Frame update.
4637 this.flashingRafId =
4638 requestAnimationFrame(this.flashHighScore.bind(this));
4639 },
4640
4641 /**
4642 * Draw empty rectangle over high score.
4643 */
4644 clearHighScoreBounds() {
4645 this.canvasCtx.save();
4646 this.canvasCtx.fillStyle = '#fff';
4647 this.canvasCtx.rect(this.highScoreBounds.x, this.highScoreBounds.y,
4648 this.highScoreBounds.width, this.highScoreBounds.height);
4649 this.canvasCtx.fill();
4650 this.canvasCtx.restore();
4651 },
4652
4653 /**
4654 * Starts the flashing of the high score.
4655 */
4656 startHighScoreFlashing() {
4657 this.highScoreFlashing = true;
4658 this.flashHighScore();
4659 },
4660
4661 /**
4662 * Whether high score is flashing.
4663 * @return {boolean}
4664 */
4665 isHighScoreFlashing() {
4666 return this.highScoreFlashing;
4667 },
4668
4669 /**
4670 * Stop flashing the high score.
4671 */
4672 cancelHighScoreFlashing() {
4673 if (this.flashingRafId) {
4674 cancelAnimationFrame(this.flashingRafId);
4675 }
4676 this.flashIterations = 0;
4677 this.flashTimer = 0;
4678 this.highScoreFlashing = false;
4679 this.clearHighScoreBounds();
4680 this.drawHighScore();
4681 },
4682
4683 /**
4684 * Clear the high score.
4685 */
4686 resetHighScore() {
4687 this.setHighScore(0);
4688 this.cancelHighScoreFlashing();
4689 },
4690
4691 /**
4692 * Reset the distance meter back to '00000'.
4693 */
4694 reset() {
4695 this.update(0, 0);
4696 this.achievement = false;
4697 }
4698};
4699
4700
4701//******************************************************************************
4702
4703/**
4704 * Cloud background item.
4705 * Similar to an obstacle object but without collision boxes.
4706 * @param {HTMLCanvasElement} canvas Canvas element.
4707 * @param {Object} spritePos Position of image in sprite.
4708 * @param {number} containerWidth
4709 * @constructor
4710 */
4711function Cloud(canvas, spritePos, containerWidth) {
4712 this.canvas = canvas;
4713 this.canvasCtx =
4714 /** @type {CanvasRenderingContext2D} */ (this.canvas.getContext('2d'));
4715 this.spritePos = spritePos;
4716 this.containerWidth = containerWidth;
4717 this.xPos = containerWidth;
4718 this.yPos = 0;
4719 this.remove = false;
4720 this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
4721 Cloud.config.MAX_CLOUD_GAP);
4722
4723 this.init();
4724}
4725
4726
4727/**
4728 * Cloud object config.
4729 * @enum {number}
4730 */
4731Cloud.config = {
4732 HEIGHT: 14,
4733 MAX_CLOUD_GAP: 400,
4734 MAX_SKY_LEVEL: 30,
4735 MIN_CLOUD_GAP: 100,
4736 MIN_SKY_LEVEL: 71,
4737 WIDTH: 46
4738};
4739
4740
4741Cloud.prototype = {
4742 /**
4743 * Initialise the cloud. Sets the Cloud height.
4744 */
4745 init() {
4746 this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
4747 Cloud.config.MIN_SKY_LEVEL);
4748 this.draw();
4749 },
4750
4751 /**
4752 * Draw the cloud.
4753 */
4754 draw() {
4755 this.canvasCtx.save();
4756 let sourceWidth = Cloud.config.WIDTH;
4757 let sourceHeight = Cloud.config.HEIGHT;
4758 const outputWidth = sourceWidth;
4759 const outputHeight = sourceHeight;
4760 if (IS_HIDPI) {
4761 sourceWidth = sourceWidth * 2;
4762 sourceHeight = sourceHeight * 2;
4763 }
4764
4765 this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
4766 this.spritePos.y,
4767 sourceWidth, sourceHeight,
4768 this.xPos, this.yPos,
4769 outputWidth, outputHeight);
4770
4771 this.canvasCtx.restore();
4772 },
4773
4774 /**
4775 * Update the cloud position.
4776 * @param {number} speed
4777 */
4778 update(speed) {
4779 if (!this.remove) {
4780 this.xPos -= Math.ceil(speed);
4781 this.draw();
4782
4783 // Mark as removeable if no longer in the canvas.
4784 if (!this.isVisible()) {
4785 this.remove = true;
4786 }
4787 }
4788 },
4789
4790 /**
4791 * Check if the cloud is visible on the stage.
4792 * @return {boolean}
4793 */
4794 isVisible() {
4795 return this.xPos + Cloud.config.WIDTH > 0;
4796 }
4797};
4798
4799
4800//******************************************************************************
4801
4802/**
4803 * Nightmode shows a moon and stars on the horizon.
4804 * @param {HTMLCanvasElement} canvas
4805 * @param {number} spritePos
4806 * @param {number} containerWidth
4807 * @constructor
4808 */
4809function NightMode(canvas, spritePos, containerWidth) {
4810 this.spritePos = spritePos;
4811 this.canvas = canvas;
4812 this.canvasCtx =
4813 /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
4814 this.xPos = containerWidth - 50;
4815 this.yPos = 30;
4816 this.currentPhase = 0;
4817 this.opacity = 0;
4818 this.containerWidth = containerWidth;
4819 this.stars = [];
4820 this.drawStars = false;
4821 this.placeStars();
4822}
4823
4824/**
4825 * @enum {number}
4826 */
4827NightMode.config = {
4828 FADE_SPEED: 0.035,
4829 HEIGHT: 40,
4830 MOON_SPEED: 0.25,
4831 NUM_STARS: 2,
4832 STAR_SIZE: 9,
4833 STAR_SPEED: 0.3,
4834 STAR_MAX_Y: 70,
4835 WIDTH: 20
4836};
4837
4838NightMode.phases = [140, 120, 100, 60, 40, 20, 0];
4839
4840NightMode.prototype = {
4841 /**
4842 * Update moving moon, changing phases.
4843 * @param {boolean} activated Whether night mode is activated.
4844 */
4845 update(activated) {
4846 // Moon phase.
4847 if (activated && this.opacity === 0) {
4848 this.currentPhase++;
4849
4850 if (this.currentPhase >= NightMode.phases.length) {
4851 this.currentPhase = 0;
4852 }
4853 }
4854
4855 // Fade in / out.
4856 if (activated && (this.opacity < 1 || this.opacity === 0)) {
4857 this.opacity += NightMode.config.FADE_SPEED;
4858 } else if (this.opacity > 0) {
4859 this.opacity -= NightMode.config.FADE_SPEED;
4860 }
4861
4862 // Set moon positioning.
4863 if (this.opacity > 0) {
4864 this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);
4865
4866 // Update stars.
4867 if (this.drawStars) {
4868 for (let i = 0; i < NightMode.config.NUM_STARS; i++) {
4869 this.stars[i].x =
4870 this.updateXPos(this.stars[i].x, NightMode.config.STAR_SPEED);
4871 }
4872 }
4873 this.draw();
4874 } else {
4875 this.opacity = 0;
4876 this.placeStars();
4877 }
4878 this.drawStars = true;
4879 },
4880
4881 updateXPos(currentPos, speed) {
4882 if (currentPos < -NightMode.config.WIDTH) {
4883 currentPos = this.containerWidth;
4884 } else {
4885 currentPos -= speed;
4886 }
4887 return currentPos;
4888 },
4889
4890 draw() {
4891 let moonSourceWidth = this.currentPhase === 3 ? NightMode.config.WIDTH * 2 :
4892 NightMode.config.WIDTH;
4893 let moonSourceHeight = NightMode.config.HEIGHT;
4894 let moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];
4895 const moonOutputWidth = moonSourceWidth;
4896 let starSize = NightMode.config.STAR_SIZE;
4897 let starSourceX = Runner.spriteDefinition.LDPI.STAR.x;
4898
4899 if (IS_HIDPI) {
4900 moonSourceWidth *= 2;
4901 moonSourceHeight *= 2;
4902 moonSourceX = this.spritePos.x +
4903 (NightMode.phases[this.currentPhase] * 2);
4904 starSize *= 2;
4905 starSourceX = Runner.spriteDefinition.HDPI.STAR.x;
4906 }
4907
4908 this.canvasCtx.save();
4909 this.canvasCtx.globalAlpha = this.opacity;
4910
4911 // Stars.
4912 if (this.drawStars) {
4913 for (let i = 0; i < NightMode.config.NUM_STARS; i++) {
4914 this.canvasCtx.drawImage(Runner.imageSprite,
4915 starSourceX, this.stars[i].sourceY, starSize, starSize,
4916 Math.round(this.stars[i].x), this.stars[i].y,
4917 NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE);
4918 }
4919 }
4920
4921 // Moon.
4922 this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX,
4923 this.spritePos.y, moonSourceWidth, moonSourceHeight,
4924 Math.round(this.xPos), this.yPos,
4925 moonOutputWidth, NightMode.config.HEIGHT);
4926
4927 this.canvasCtx.globalAlpha = 1;
4928 this.canvasCtx.restore();
4929 },
4930
4931 // Do star placement.
4932 placeStars() {
4933 const segmentSize = Math.round(this.containerWidth /
4934 NightMode.config.NUM_STARS);
4935
4936 for (let i = 0; i < NightMode.config.NUM_STARS; i++) {
4937 this.stars[i] = {};
4938 this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));
4939 this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);
4940
4941 if (IS_HIDPI) {
4942 this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y +
4943 NightMode.config.STAR_SIZE * 2 * i;
4944 } else {
4945 this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +
4946 NightMode.config.STAR_SIZE * i;
4947 }
4948 }
4949 },
4950
4951 reset() {
4952 this.currentPhase = 0;
4953 this.opacity = 0;
4954 this.update(false);
4955 }
4956
4957};
4958
4959
4960//******************************************************************************
4961
4962/**
4963 * Horizon Line.
4964 * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
4965 * @param {HTMLCanvasElement} canvas
4966 * @param {Object} spritePos Horizon position in sprite.
4967 * @constructor
4968 */
4969function HorizonLine(canvas, spritePos) {
4970 this.spritePos = spritePos;
4971 this.canvas = canvas;
4972 this.canvasCtx =
4973 /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
4974 this.sourceDimensions = {};
4975 this.dimensions = HorizonLine.dimensions;
4976 this.sourceXPos = [this.spritePos.x, this.spritePos.x +
4977 this.dimensions.WIDTH];
4978 this.xPos = [];
4979 this.yPos = 0;
4980 this.bumpThreshold = 0.5;
4981
4982 this.setSourceDimensions();
4983 this.draw();
4984}
4985
4986
4987/**
4988 * Horizon line dimensions.
4989 * @enum {number}
4990 */
4991HorizonLine.dimensions = {
4992 WIDTH: 600,
4993 HEIGHT: 12,
4994 YPOS: 127
4995};
4996
4997
4998HorizonLine.prototype = {
4999 /**
5000 * Set the source dimensions of the horizon line.
5001 */
5002 setSourceDimensions() {
5003 for (const dimension in HorizonLine.dimensions) {
5004 if (IS_HIDPI) {
5005 if (dimension !== 'YPOS') {
5006 this.sourceDimensions[dimension] =
5007 HorizonLine.dimensions[dimension] * 2;
5008 }
5009 } else {
5010 this.sourceDimensions[dimension] =
5011 HorizonLine.dimensions[dimension];
5012 }
5013 this.dimensions[dimension] = HorizonLine.dimensions[dimension];
5014 }
5015
5016 this.xPos = [0, HorizonLine.dimensions.WIDTH];
5017 this.yPos = HorizonLine.dimensions.YPOS;
5018 },
5019
5020 /**
5021 * Return the crop x position of a type.
5022 */
5023 getRandomType() {
5024 return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
5025 },
5026
5027 /**
5028 * Draw the horizon line.
5029 */
5030 draw() {
5031 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
5032 this.spritePos.y,
5033 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
5034 this.xPos[0], this.yPos,
5035 this.dimensions.WIDTH, this.dimensions.HEIGHT);
5036
5037 this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
5038 this.spritePos.y,
5039 this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
5040 this.xPos[1], this.yPos,
5041 this.dimensions.WIDTH, this.dimensions.HEIGHT);
5042 },
5043
5044 /**
5045 * Update the x position of an indivdual piece of the line.
5046 * @param {number} pos Line position.
5047 * @param {number} increment
5048 */
5049 updateXPos(pos, increment) {
5050 const line1 = pos;
5051 const line2 = pos === 0 ? 1 : 0;
5052
5053 this.xPos[line1] -= increment;
5054 this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
5055
5056 if (this.xPos[line1] <= -this.dimensions.WIDTH) {
5057 this.xPos[line1] += this.dimensions.WIDTH * 2;
5058 this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
5059 this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
5060 }
5061 },
5062
5063 /**
5064 * Update the horizon line.
5065 * @param {number} deltaTime
5066 * @param {number} speed
5067 */
5068 update(deltaTime, speed) {
5069 const increment = Math.floor(speed * (FPS / 1000) * deltaTime);
5070
5071 if (this.xPos[0] <= 0) {
5072 this.updateXPos(0, increment);
5073 } else {
5074 this.updateXPos(1, increment);
5075 }
5076 this.draw();
5077 },
5078
5079 /**
5080 * Reset horizon to the starting position.
5081 */
5082 reset() {
5083 this.xPos[0] = 0;
5084 this.xPos[1] = HorizonLine.dimensions.WIDTH;
5085 }
5086};
5087
5088
5089//******************************************************************************
5090
5091/**
5092 * Horizon background class.
5093 * @param {HTMLCanvasElement} canvas
5094 * @param {Object} spritePos Sprite positioning.
5095 * @param {Object} dimensions Canvas dimensions.
5096 * @param {number} gapCoefficient
5097 * @constructor
5098 */
5099function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
5100 this.canvas = canvas;
5101 this.canvasCtx =
5102 /** @type {CanvasRenderingContext2D} */ (this.canvas.getContext('2d'));
5103 this.config = Horizon.config;
5104 this.dimensions = dimensions;
5105 this.gapCoefficient = gapCoefficient;
5106 this.obstacles = [];
5107 this.obstacleHistory = [];
5108 this.horizonOffsets = [0, 0];
5109 this.cloudFrequency = this.config.CLOUD_FREQUENCY;
5110 this.spritePos = spritePos;
5111 this.nightMode = null;
5112
5113 // Cloud
5114 this.clouds = [];
5115 this.cloudSpeed = this.config.BG_CLOUD_SPEED;
5116
5117 // Horizon
5118 this.horizonLine = null;
5119 this.init();
5120}
5121
5122
5123/**
5124 * Horizon config.
5125 * @enum {number}
5126 */
5127Horizon.config = {
5128 BG_CLOUD_SPEED: 0.2,
5129 BUMPY_THRESHOLD: .3,
5130 CLOUD_FREQUENCY: .5,
5131 HORIZON_HEIGHT: 16,
5132 MAX_CLOUDS: 6
5133};
5134
5135
5136Horizon.prototype = {
5137 /**
5138 * Initialise the horizon. Just add the line and a cloud. No obstacles.
5139 */
5140 init() {
5141 this.addCloud();
5142 this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
5143 this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
5144 this.dimensions.WIDTH);
5145 },
5146
5147 /**
5148 * @param {number} deltaTime
5149 * @param {number} currentSpeed
5150 * @param {boolean} updateObstacles Used as an override to prevent
5151 * the obstacles from being updated / added. This happens in the
5152 * ease in section.
5153 * @param {boolean} showNightMode Night mode activated.
5154 */
5155 update(deltaTime, currentSpeed, updateObstacles, showNightMode) {
5156 this.runningTime += deltaTime;
5157 this.horizonLine.update(deltaTime, currentSpeed);
5158 this.nightMode.update(showNightMode);
5159 this.updateClouds(deltaTime, currentSpeed);
5160
5161 if (updateObstacles) {
5162 this.updateObstacles(deltaTime, currentSpeed);
5163 }
5164 },
5165
5166 /**
5167 * Update the cloud positions.
5168 * @param {number} deltaTime
5169 * @param {number} speed
5170 */
5171 updateClouds(deltaTime, speed) {
5172 const cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
5173 const numClouds = this.clouds.length;
5174
5175 if (numClouds) {
5176 for (let i = numClouds - 1; i >= 0; i--) {
5177 this.clouds[i].update(cloudSpeed);
5178 }
5179
5180 const lastCloud = this.clouds[numClouds - 1];
5181
5182 // Check for adding a new cloud.
5183 if (numClouds < this.config.MAX_CLOUDS &&
5184 (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
5185 this.cloudFrequency > Math.random()) {
5186 this.addCloud();
5187 }
5188
5189 // Remove expired clouds.
5190 this.clouds = this.clouds.filter(function(obj) {
5191 return !obj.remove;
5192 });
5193 } else {
5194 this.addCloud();
5195 }
5196 },
5197
5198 /**
5199 * Update the obstacle positions.
5200 * @param {number} deltaTime
5201 * @param {number} currentSpeed
5202 */
5203 updateObstacles(deltaTime, currentSpeed) {
5204 // Obstacles, move to Horizon layer.
5205 const updatedObstacles = this.obstacles.slice(0);
5206
5207 for (let i = 0; i < this.obstacles.length; i++) {
5208 const obstacle = this.obstacles[i];
5209 obstacle.update(deltaTime, currentSpeed);
5210
5211 // Clean up existing obstacles.
5212 if (obstacle.remove) {
5213 updatedObstacles.shift();
5214 }
5215 }
5216 this.obstacles = updatedObstacles;
5217
5218 if (this.obstacles.length > 0) {
5219 const lastObstacle = this.obstacles[this.obstacles.length - 1];
5220
5221 if (lastObstacle && !lastObstacle.followingObstacleCreated &&
5222 lastObstacle.isVisible() &&
5223 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
5224 this.dimensions.WIDTH) {
5225 this.addNewObstacle(currentSpeed);
5226 lastObstacle.followingObstacleCreated = true;
5227 }
5228 } else {
5229 // Create new obstacles.
5230 this.addNewObstacle(currentSpeed);
5231 }
5232 },
5233
5234 removeFirstObstacle() {
5235 this.obstacles.shift();
5236 },
5237
5238 /**
5239 * Add a new obstacle.
5240 * @param {number} currentSpeed
5241 */
5242 addNewObstacle(currentSpeed) {
5243 const obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
5244 const obstacleType = Obstacle.types[obstacleTypeIndex];
5245
5246 // Check for multiples of the same type of obstacle.
5247 // Also check obstacle is available at current speed.
5248 if (this.duplicateObstacleCheck(obstacleType.type) ||
5249 currentSpeed < obstacleType.minSpeed) {
5250 this.addNewObstacle(currentSpeed);
5251 } else {
5252 const obstacleSpritePos = this.spritePos[obstacleType.type];
5253
5254 this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
5255 obstacleSpritePos, this.dimensions,
5256 this.gapCoefficient, currentSpeed, obstacleType.width));
5257
5258 this.obstacleHistory.unshift(obstacleType.type);
5259
5260 if (this.obstacleHistory.length > 1) {
5261 this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
5262 }
5263 }
5264 },
5265
5266 /**
5267 * Returns whether the previous two obstacles are the same as the next one.
5268 * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
5269 * @return {boolean}
5270 */
5271 duplicateObstacleCheck(nextObstacleType) {
5272 let duplicateCount = 0;
5273
5274 for (let i = 0; i < this.obstacleHistory.length; i++) {
5275 duplicateCount =
5276 this.obstacleHistory[i] === nextObstacleType ? duplicateCount + 1 : 0;
5277 }
5278 return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
5279 },
5280
5281 /**
5282 * Reset the horizon layer.
5283 * Remove existing obstacles and reposition the horizon line.
5284 */
5285 reset() {
5286 this.obstacles = [];
5287 this.horizonLine.reset();
5288 this.nightMode.reset();
5289 },
5290
5291 /**
5292 * Update the canvas width and scaling.
5293 * @param {number} width Canvas width.
5294 * @param {number} height Canvas height.
5295 */
5296 resize(width, height) {
5297 this.canvas.width = width;
5298 this.canvas.height = height;
5299 },
5300
5301 /**
5302 * Add a new cloud to the horizon.
5303 */
5304 addCloud() {
5305 this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
5306 this.dimensions.WIDTH));
5307 }
5308};
5309</script>
5310</head>
5311<body id="t" style="font-family: 'Segoe UI', Tahoma, sans-serif; font-size: 75%" jstcache="0" class="neterror">
5312 <div id="main-frame-error" class="interstitial-wrapper" jstcache="0">
5313 <div id="main-content" jstcache="0">
5314 <div class="icon icon-generic" jseval="updateIconClass(this.classList, iconClass)" alt="" jstcache="1"></div>
5315 <div id="main-message" jstcache="0">
5316 <h1 jstcache="0">
5317 <span jsselect="heading" jsvalues=".innerHTML:msg" jstcache="10">This site can’t be reached</span>
5318 <a id="error-information-button" class="hidden" onclick="toggleErrorInformationPopup();" jstcache="0"></a>
5319 </h1>
5320 <p jsselect="summary" jsvalues=".innerHTML:msg" jstcache="2"><strong jscontent="hostName" jstcache="23">brontoforum.us</strong>’s server IP address could not be found.</p>
5321 <!--The suggestion list and error code are normally presented inline,
5322 in which case error-information-popup-* divs have no effect. When
5323 error-information-popup-container has the use-popup-container class, this
5324 information is provided in a popup instead.-->
5325 <div id="error-information-popup-container" jstcache="0">
5326 <div id="error-information-popup" jstcache="0">
5327 <div id="error-information-popup-box" jstcache="0">
5328 <div id="error-information-popup-content" jstcache="0">
5329 <div id="suggestions-list" style="" jsdisplay="(suggestionsSummaryList && suggestionsSummaryList.length)" jstcache="17">
5330 <p jsvalues=".innerHTML:suggestionsSummaryListHeader" jstcache="19"></p>
5331 <ul jsvalues=".className:suggestionsSummaryList.length == 1 ? 'single-suggestion' : ''" jstcache="20" class="single-suggestion">
5332 <li jsselect="suggestionsSummaryList" jsvalues=".innerHTML:summary" jstcache="22" jsinstance="*0"><a href="javascript:diagnoseErrors()" id="diagnose-link" jstcache="0">Try running Windows Network Diagnostics</a>.</li>
5333 </ul>
5334 </div>
5335 <div class="error-code" jscontent="errorCode" jstcache="18">DNS_PROBE_FINISHED_NXDOMAIN</div>
5336 <p id="error-information-popup-close" jstcache="0">
5337 <a class="link-button" jscontent="closeDescriptionPopup" onclick="toggleErrorInformationPopup();" jstcache="21">null</a>
5338 </p>
5339 </div>
5340 </div>
5341 </div>
5342 </div>
5343 <div id="diagnose-frame" class="hidden" jstcache="0"></div>
5344 <div id="download-links-wrapper" class="hidden" jstcache="0">
5345 <div id="download-link-wrapper" jstcache="0">
5346 <a id="download-link" class="link-button" onclick="downloadButtonClick()" jsselect="downloadButton" jscontent="msg" jsvalues=".disabledText:disabledMsg" jstcache="7" style="display: none;">
5347 </a>
5348 </div>
5349 <div id="download-link-clicked-wrapper" class="hidden" jstcache="0">
5350 <div id="download-link-clicked" class="link-button" jsselect="downloadButton" jscontent="disabledMsg" jstcache="12" style="display: none;">
5351 </div>
5352 </div>
5353 </div>
5354 <div id="save-page-for-later-button" class="hidden" jstcache="0">
5355 <a class="link-button" onclick="savePageLaterClick()" jsselect="savePageLater" jscontent="savePageMsg" jstcache="11" style="display: none;">
5356 </a>
5357 </div>
5358 <div id="cancel-save-page-button" class="hidden" onclick="cancelSavePageClick()" jsselect="savePageLater" jsvalues=".innerHTML:cancelMsg" jstcache="5" style="display: none;">
5359 </div>
5360 <div id="offline-content-list" class="list-hidden" hidden="" jstcache="0">
5361 <div id="offline-content-list-visibility-card" onclick="toggleOfflineContentListVisibility(true)" jstcache="0">
5362 <div id="offline-content-list-title" jsselect="offlineContentList" jscontent="title" jstcache="13" style="display: none;">
5363 </div>
5364 <div jstcache="0">
5365 <div id="offline-content-list-show-text" jsselect="offlineContentList" jscontent="showText" jstcache="15" style="display: none;">
5366 </div>
5367 <div id="offline-content-list-hide-text" jsselect="offlineContentList" jscontent="hideText" jstcache="16" style="display: none;">
5368 </div>
5369 </div>
5370 </div>
5371 <div id="offline-content-suggestions" jstcache="0"></div>
5372 <div id="offline-content-list-action" jstcache="0">
5373 <a class="link-button" onclick="launchDownloadsPage()" jsselect="offlineContentList" jscontent="actionText" jstcache="14" style="display: none;">
5374 </a>
5375 </div>
5376 </div>
5377 </div>
5378 </div>
5379 <div id="buttons" class="nav-wrapper suggested-left" jstcache="0">
5380 <div id="control-buttons" hidden="" jstcache="0">
5381 <button id="reload-button" class="blue-button text-button" onclick="trackClick(this.trackingId);
5382 reloadButtonClick(this.url);" jsselect="reloadButton" jsvalues=".url:reloadUrl; .trackingId:reloadTrackingId" jscontent="msg" jstcache="6" style="display: none;"></button>
5383 <button id="download-button" class="blue-button text-button" onclick="downloadButtonClick()" jsselect="downloadButton" jscontent="msg" jsvalues=".disabledText:disabledMsg" jstcache="7" style="display: none;">
5384 </button>
5385 </div>
5386 <button id="details-button" class="secondary-button 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>
5387 </div>
5388 <div id="details" class="hidden" jstcache="0">
5389 <div class="suggestions" jsselect="suggestionsDetails" jstcache="4" jsinstance="*0" style="display: none;">
5390 <div class="suggestion-header" jsvalues=".innerHTML:header" jstcache="8"></div>
5391 <div class="suggestion-body" jsvalues=".innerHTML:body" jstcache="9"></div>
5392 </div>
5393 </div>
5394 </div>
5395 <div id="sub-frame-error" jstcache="0">
5396 <!-- Show details when hovering over the icon, in case the details are
5397 hidden because they're too large. -->
5398 <div class="icon icon-generic" jseval="updateIconClass(this.classList, iconClass)" jstcache="1"></div>
5399 <div id="sub-frame-error-details" jsselect="summary" jsvalues=".innerHTML:msg" jstcache="2"><strong jscontent="hostName" jstcache="23">brontoforum.us</strong>’s server IP address could not be found.</div>
5400 </div>
5401
5402 <div id="offline-resources" jstcache="0">
5403 <img id="offline-resources-1x" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABMUAAABBCAMAAADmHuOOAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDozNDhmYjcyNS1lZDRkLTRkNGEtOGU4ZS0zZDZlNWJiNWMyNGQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6N0UwMzg1RUMwMDJCMTFFNkI1NTVBNjZDNEFGRENEQTkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6N0UwMzg1RUIwMDJCMTFFNkI1NTVBNjZDNEFGRENEQTkiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0MjA5Yjk0Yi00MTA4LTRmZjUtOTVmNi02MWU3Y2E5YWU1MzUiIHN0UmVmOmRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo4MGM3MDBjYi00ODhiLTExNzktYjg3YS1jODZiYTFiOGU1ODgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz73qFqWAAADAFBMVEVYWFjPb2/Dw8P86elISEj+/v762tp0c3PT09NoaGiK2KGXDg774+OU26rcKSmr47y8vLz19fW96cqMNDT++Pj/UlLy+/WgHBzhNTXJ7dTstrY0NDSZ3a3U8dzr6+vIyMilpaX+d3f5lZXB6s751NREwGn1w8PN9P/x8fHKysrt7e39/f1Kwm+ul5f99fWsS0tQMzOqqqrn19f2y8vVR0eNeHj/g4OysrLyu7vlxMTkOTnum5vhS0vGKirzsrLsi4tqMzPk9um15sRxSkqx5cGUk5OLi4vjWlr+bGz2/Pij4LV50pRRxHSD1ZzY8uDxrKw6Ojri4uJLw2+MUlKbm5v98fHiU1PFMzPl5eXd9OTeOjrd3d3RMjLmQkLspKTshITsk5NcyX3xoqLz29v16enkKSnla2vYVVXXiopjy4OkNTX+zMzlNTX4Tk7hY2PbycnY2Ni2NDT39/ec3rDpdXX5/fqCgoJtzot10ZH10dHpNDTra2vXnJzsfHzdMTH24uLVq6u5JCT/+/sqKirOzs7vqKitYmLaubn8w8Pb9//t+fHEp6fHtLSqICDg9eficHDd0tLFm5tz0I/i+f/5/fy3oqJHwWzq+O6458bkgIDienrEgYHgRUVAv2ZZx3oqMzNPUFDqODjo6OjzODhdYGD8/v779PS7sbHYMjLw4uJIwm3/Xl7niYnirq69Wlr77u77/vxVxnfOw8MVJSXkoaHoZmbKHx/oXFzsRER/YWHgQEDoUlKl4bhCwGhvz4yZgYHQ79mgoKDomZn9rKzYNDRozYaP2aUwLy+g37P/u7vn9+zBRkbo+v8+vmTjj4/nS0t91JczOztAMjL28fETHBzlRkb/oqLfLy98fX1AQUH/tbXl4ODTPT3g2tpRRETb8+L5+vqur6+Oj4+Hh4fJzMy66Mj5+flJwm23t7fnMjL6+vr7+/u05sNMw2+u5L7Jycnu6Oinra1fyX+/v7/wVVXxW1vo6+vPy8v8+vrLyMjK0NDd9OOXl5feNDRMw3D///8fPj6GAAABAHRSTlP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AU/cHJQAAKM1JREFUeNrsnQtcE1e6wNOYCaAYiWCMjgKthAZiKYi7UAWE+kAE5FF5GLAqKq00SyGCVaxYQLddW7S1XdBqMVK0bl/ottaK1Uqvitfutkrbxdx2l+sLaeLqXdl7b393TeaeM5PHzCQ5mfASNN+vNeHMN2fOTOb85/u+850zPCJp7BQncoros2C/exTKmxjhFrc8GFKdxflux8L8OFfrN6J6KJxb9VC61Dxim8zoRE72F8QAxt56yw0ytzwYFAuetZ6rrnfBTK4cCysoC+bc7/ywATu34CF0qXtPMe9gb85HefNRq/zurXtwmlhSEuZ6gZNKD0nKbQvLb627MMWeNrkBs78h2f6GOesujHXpNiyfk5w8drg/JnT8uXO75dR3z4jAzfDTP2ru3Ah/8EUc0HrFsjU0IqJbQX5cpr4BRZ+7VxaJCYIfCIouB94GRbkRc+dGwX2Jkta5rXkEoYiKiFIT4suB5C78wEDxAJyF36YVWcJsbhhr3rTpl1kbOakeWL9+U+QPXAmiCN7IGTZhkZFc7Su/lSuyvBOD7weKEUHbIzke5K1HGXIPutkamSyZDiVspEw28hC9/1+QyUbT95DskMkWMOqQjFmTQP+7847MaMOr/anwii2wbQG1YY0dwJIbztnZMNLuHii2kXtMGuYU86gBwlPDr4FtNTXzwOfldFim9SQIL/ilRhNq1tQAAvWQZQLIom4cfGvLBfc1WVbjRRAhOLlvBkG0wi9tUUSXtqbNn8jDqV2aar7/cSCsFe+vphfHZAu56GZFFk/fXsnJGjsQuT3nmZYYTsCp3rSieUXiKxzbS/gVFGZxhOOIT6cXZ8cE3QcUCyPiYsK4HeRPTIr9bj8H3L/iyln4vYKR9rOj7dvgaTxJ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" jstcache="0">
5404 <img id="offline-resources-2x" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAACYkAAACCCAMAAAA+cCW9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDozNDhmYjcyNS1lZDRkLTRkNGEtOGU4ZS0zZDZlNWJiNWMyNGQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REVEMDNFMjcwMDJBMTFFNkI1NTVBNjZDNEFGRENEQTkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REVEMDNFMjYwMDJBMTFFNkI1NTVBNjZDNEFGRENEQTkiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0MjA5Yjk0Yi00MTA4LTRmZjUtOTVmNi02MWU3Y2E5YWU1MzUiIHN0UmVmOmRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo4MGM3MDBjYi00ODhiLTExNzktYjg3YS1jODZiYTFiOGU1ODgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7nkeIzAAADAFBMVEX86uqFhIT8/PzoqanDw8O15sRpaWnZSEjgNTX75OR50pTptbXmysrc8+Pt7e31ycn4NDRrMjLh+f/l5eX5mJh5eXlUMjLz+/bHKyujo6Pq1taXGRmK2KH51tbaFxf/UlI0NDT6+vrqeXmjHh7zw8P73d3p6en/iIj98fH++Piub2+T26lRxXTzu7usrKzbV1fUMjLK7dW5JSWpISHaZmaB1ZrrhIT99fX39/f12dnwoqLtlJTe3t6qTEzVdnbW9v/YiYnGNDTA6s3z0tIyMjK2NDT19fXxra3JyclCQkK0goLti4vtm5uTk5P5zc3iOjrdOjpszoniXFybnJzN9P/W1tblamr0/f//eHinMjLiY2Pz8/NdXV3gTk5jyoLNzc06OTnx8PAWMjLcMzPR0dHkNjZOTk6UZWXy8vK9vb3mQkLi4eH/aGjSmpq5ubnKrKzZqKiDMzOEBARycnL//PyXMzPk9unrOTmt473zs7NUVFSys7OYBQXdKyvGcnLmKCjsNDQkJSXZ2dnYMjL5/v3AZ2dvAwPKiIjiVFT46OglMzPcIyPr4eHhcnLq+O7bNDTqbW3V8d36w8ORTEzmjIyj4LXayMjpc3P34OBNw3HZvr7+u7vigoLER0flfHz7q6vQLi6/ra2/np787u7gkpKHIyNAMjL1TEyg37OVKCj/s7PoW1vE69DsRkauCQn89PT/XV0SFBRdyX73/fm/v79Vxne/WVnjmpqLERHoYmLhMDBrS0vfQUEyTU3JtLT/oKDQICC+ERHUOzvM7tfoVVU1MjLgRUWptLT/VlZcAQGhFRWHkpLf9OYzAACa3a9YQ0P7/v+qGxvv5+flSEizvb1Yx3n9/f348vL7+PjP79lwz4zc3NzoMjJ00JBozYdHR0fn9+zoTk5KXFz5+/svLi7////+/v7//f3+//7+///p7+/OxcXd4uLw9fWn4rnU0ND4HR3yUFC3trbsWFjj09ONjY1PamrDubmjrKzO0tLQ1dWXl5ekkJDeNDRMw3D///9tlcWXAAABAHRSTlP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AU/cHJQAASMxJREFUeNrsnQ9AFNe972FEHEiySLb8CyK7gAQqIILiCsQEHZbdiEo0kBBAWJelCEkaIogazVUwiX+qq2lStTWGWy1Jqqlp/BOl3NtcE7yp11rTXLcxGk1bdo33T957ucm777103Td/9s/M7szs/F1m4Xzb4O4w85szZw5zPvM7v/M7Ea7xpMv3PurVg4+5gICAgICAgIAUrQj0vzsSSBEX89ijFN1rB/cXCAgIaByp+3B+dWlOuJjNnVFWt28GuGuyVTAgMeWRmB+IoSh2G9xgICAgoPGknJczhjMiwsVsTPWfnU1p4K7JVsGAxBRGYgEg9uijD6L/vxeMUgIBAQGNI71stsaFj9ky56ZSiU12H86qLt0O7hsgMaEk1h2RVVZ6WIYrufzgowwCrrFwVtKbj+TlPfJqUrgfEYorB5JHpiWNRmNbQq3f5tUJkahM5A1t6H7RBb4t7QXYJmNbpP+xJuzYhNXub7W4JVzebcQvIhuxwxuJk6yOpMhzZlNKo7ERLZ+BfGBCG6olnl3w/VPwj2rKmYmLSFCH2y2JcTpnhI/ZpbDzZalt5sSVDO+Lkdpq9+GXq0tjwu6+ARIT4BPLXbpvU5n0V3Lvo4y61zExbuaJV1fmrbzE1nPvnvlu3o11bNVxz9wreZ/PZjvLsc9PXZl7jzjGuIQW9KsTXHY9doVoYKeOcTWuzCP4KxTnAOKiFI1b0ZTNkcRGL4mt1nhV68Et36ZGE+XgRorBFN9+5JOkGD0biYNJ5jAVUMqByugBKlOjZxNBXy7iC6mY3rMkkE0pXsfjJh1ZsODIpPtcJc4mxZuNeGbB6ddeO73gLtc2p1mG2ohz9udLX8VLS4b37A+P+wZITOToZFz/2VsSX4jjURbdOzG8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" jstcache="0">
5405 <template id="audio-resources" jstcache="0"></template>
5406 </div>
5407
5408
5409<script jstcache="0">// Copyright (c) 2012 The Chromium Authors. All rights reserved.
5410// Use of this source code is governed by a BSD-style license that can be
5411// found in the LICENSE file.
5412
5413/**
5414 * @fileoverview This file defines a singleton which provides access to all data
5415 * that is available as soon as the page's resources are loaded (before DOM
5416 * content has finished loading). This data includes both localized strings and
5417 * any data that is important to have ready from a very early stage (e.g. things
5418 * that must be displayed right away).
5419 *
5420 * Note that loadTimeData is not guaranteed to be consistent between page
5421 * refreshes (https://crbug.com/740629) and should not contain values that might
5422 * change if the page is re-opened later.
5423 */
5424
5425// #import {assert} from './assert.m.js';
5426// #import {parseHtmlSubset} from './parse_html_subset.m.js';
5427
5428/**
5429 * @typedef {{
5430 * substitutions: (Array<string>|undefined),
5431 * attrs: (Object<function(Node, string):boolean>|undefined),
5432 * tags: (Array<string>|undefined),
5433 * }}
5434 */
5435/* #export */ let SanitizeInnerHtmlOpts;
5436
5437// eslint-disable-next-line no-var
5438/* #export */ /** @type {!LoadTimeData} */ var loadTimeData;
5439
5440// Expose this type globally as a temporary work around until
5441// https://github.com/google/closure-compiler/issues/544 is fixed.
5442/** @constructor */
5443function LoadTimeData(){}
5444
5445(function() {
5446 'use strict';
5447
5448 LoadTimeData.prototype = {
5449 /**
5450 * Sets the backing object.
5451 *
5452 * Note that there is no getter for |data_| to discourage abuse of the form:
5453 *
5454 * var value = loadTimeData.data()['key'];
5455 *
5456 * @param {Object} value The de-serialized page data.
5457 */
5458 set data(value) {
5459 expect(!this.data_, 'Re-setting data.');
5460 this.data_ = value;
5461 },
5462
5463 /**
5464 * Returns a JsEvalContext for |data_|.
5465 * @returns {JsEvalContext}
5466 */
5467 createJsEvalContext() {
5468 return new JsEvalContext(this.data_);
5469 },
5470
5471 /**
5472 * @param {string} id An ID of a value that might exist.
5473 * @return {boolean} True if |id| is a key in the dictionary.
5474 */
5475 valueExists(id) {
5476 return id in this.data_;
5477 },
5478
5479 /**
5480 * Fetches a value, expecting that it exists.
5481 * @param {string} id The key that identifies the desired value.
5482 * @return {*} The corresponding value.
5483 */
5484 getValue(id) {
5485 expect(this.data_, 'No data. Did you remember to include strings.js?');
5486 const value = this.data_[id];
5487 expect(typeof value !== 'undefined', 'Could not find value for ' + id);
5488 return value;
5489 },
5490
5491 /**
5492 * As above, but also makes sure that the value is a string.
5493 * @param {string} id The key that identifies the desired string.
5494 * @return {string} The corresponding string value.
5495 */
5496 getString(id) {
5497 const value = this.getValue(id);
5498 expectIsType(id, value, 'string');
5499 return /** @type {string} */ (value);
5500 },
5501
5502 /**
5503 * Returns a formatted localized string where $1 to $9 are replaced by the
5504 * second to the tenth argument.
5505 * @param {string} id The ID of the string we want.
5506 * @param {...(string|number)} var_args The extra values to include in the
5507 * formatted output.
5508 * @return {string} The formatted string.
5509 */
5510 getStringF(id, var_args) {
5511 const value = this.getString(id);
5512 if (!value) {
5513 return '';
5514 }
5515
5516 const args = Array.prototype.slice.call(arguments);
5517 args[0] = value;
5518 return this.substituteString.apply(this, args);
5519 },
5520
5521 /**
5522 * Make a string safe for use with with Polymer bindings that are
5523 * inner-h-t-m-l (or other innerHTML use).
5524 * @param {string} rawString The unsanitized string.
5525 * @param {SanitizeInnerHtmlOpts=} opts Optional additional allowed tags and
5526 * attributes.
5527 * @return {string}
5528 */
5529 sanitizeInnerHtml(rawString, opts) {
5530 opts = opts || {};
5531 return parseHtmlSubset('<b>' + rawString + '</b>', opts.tags, opts.attrs)
5532 .firstChild.innerHTML;
5533 },
5534
5535 /**
5536 * Returns a formatted localized string where $1 to $9 are replaced by the
5537 * second to the tenth argument. Any standalone $ signs must be escaped as
5538 * $$.
5539 * @param {string} label The label to substitute through.
5540 * This is not an resource ID.
5541 * @param {...(string|number)} var_args The extra values to include in the
5542 * formatted output.
5543 * @return {string} The formatted string.
5544 */
5545 substituteString(label, var_args) {
5546 const varArgs = arguments;
5547 return label.replace(/\$(.|$|\n)/g, function(m) {
5548 assert(m.match(/\$[$1-9]/), 'Unescaped $ found in localized string.');
5549 return m === '$$' ? '$' : varArgs[m[1]];
5550 });
5551 },
5552
5553 /**
5554 * Returns a formatted string where $1 to $9 are replaced by the second to
5555 * tenth argument, split apart into a list of pieces describing how the
5556 * substitution was performed. Any standalone $ signs must be escaped as $$.
5557 * @param {string} label A localized string to substitute through.
5558 * This is not an resource ID.
5559 * @param {...(string|number)} var_args The extra values to include in the
5560 * formatted output.
5561 * @return {!Array<!{value: string, arg: (null|string)}>} The formatted
5562 * string pieces.
5563 */
5564 getSubstitutedStringPieces(label, var_args) {
5565 const varArgs = arguments;
5566 // Split the string by separately matching all occurrences of $1-9 and of
5567 // non $1-9 pieces.
5568 const pieces = (label.match(/(\$[1-9])|(([^$]|\$([^1-9]|$))+)/g) ||
5569 []).map(function(p) {
5570 // Pieces that are not $1-9 should be returned after replacing $$
5571 // with $.
5572 if (!p.match(/^\$[1-9]$/)) {
5573 assert(
5574 (p.match(/\$/g) || []).length % 2 === 0,
5575 'Unescaped $ found in localized string.');
5576 return {value: p.replace(/\$\$/g, '$'), arg: null};
5577 }
5578
5579 // Otherwise, return the substitution value.
5580 return {value: varArgs[p[1]], arg: p};
5581 });
5582
5583 return pieces;
5584 },
5585
5586 /**
5587 * As above, but also makes sure that the value is a boolean.
5588 * @param {string} id The key that identifies the desired boolean.
5589 * @return {boolean} The corresponding boolean value.
5590 */
5591 getBoolean(id) {
5592 const value = this.getValue(id);
5593 expectIsType(id, value, 'boolean');
5594 return /** @type {boolean} */ (value);
5595 },
5596
5597 /**
5598 * As above, but also makes sure that the value is an integer.
5599 * @param {string} id The key that identifies the desired number.
5600 * @return {number} The corresponding number value.
5601 */
5602 getInteger(id) {
5603 const value = this.getValue(id);
5604 expectIsType(id, value, 'number');
5605 expect(value === Math.floor(value), 'Number isn\'t integer: ' + value);
5606 return /** @type {number} */ (value);
5607 },
5608
5609 /**
5610 * Override values in loadTimeData with the values found in |replacements|.
5611 * @param {Object} replacements The dictionary object of keys to replace.
5612 */
5613 overrideValues(replacements) {
5614 expect(
5615 typeof replacements === 'object',
5616 'Replacements must be a dictionary object.');
5617 for (const key in replacements) {
5618 this.data_[key] = replacements[key];
5619 }
5620 }
5621 };
5622
5623 /**
5624 * Checks condition, displays error message if expectation fails.
5625 * @param {*} condition The condition to check for truthiness.
5626 * @param {string} message The message to display if the check fails.
5627 */
5628 function expect(condition, message) {
5629 if (!condition) {
5630 console.error(
5631 'Unexpected condition on ' + document.location.href + ': ' + message);
5632 }
5633 }
5634
5635 /**
5636 * Checks that the given value has the given type.
5637 * @param {string} id The id of the value (only used for error message).
5638 * @param {*} value The value to check the type on.
5639 * @param {string} type The type we expect |value| to be.
5640 */
5641 function expectIsType(id, value, type) {
5642 expect(
5643 typeof value === type, '[' + value + '] (' + id + ') is not a ' + type);
5644 }
5645
5646 expect(!loadTimeData, 'should only include this file once');
5647 loadTimeData = new LoadTimeData;
5648
5649 // Expose |loadTimeData| directly on |window|. This is only necessary by the
5650 // auto-generated load_time_data.m.js, since within a JS module the scope is
5651 // local.
5652 window.loadTimeData = loadTimeData;
5653})();
5654</script><script jstcache="0">loadTimeData.data = {"a11yenhanced":"","details":"Details","errorCode":"DNS_PROBE_POSSIBLE","fontfamily":"'Segoe UI', Tahoma, sans-serif","fontsize":"75%","heading":{"hostName":"brontoforum.us","msg":"This site can’t be reached"},"hideDetails":"Hide details","iconClass":"icon-generic","language":"en","suggestionsDetails":[],"suggestionsSummaryList":[{"summary":"\u003Ca href=\"javascript:diagnoseErrors()\" id=\"diagnose-link\">Try running Windows Network Diagnostics\u003C/a>."}],"summary":{"failedUrl":"https://brontoforum.us/posting.php?mode=reply&f=10&t=57","hostName":"brontoforum.us","msg":"\u003Cstrong jscontent=\"hostName\">\u003C/strong>’s \u003Cabbr id=\"dnsDefinition\">DNS address\u003C/abbr> could not be found. Diagnosing the problem."},"textdirection":"ltr","title":"brontoforum.us"};</script><script jstcache="0">// Copyright (c) 2012 The Chromium Authors. All rights reserved.
5655// Use of this source code is governed by a BSD-style license that can be
5656// found in the LICENSE file.
5657
5658// This file serves as a proxy to bring the included js file from /third_party
5659// into its correct location under the resources directory tree, whence it is
5660// delivered via a chrome://resources URL. See ../webui_resources.grd.
5661
5662// Note: this <include> is not behind a single-line comment because the first
5663// line of the file is source code (so the first line would be skipped) instead
5664// of a licence header.
5665// clang-format off
5666(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}}
5667function 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]];
5668function 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]=
5669h;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():[]}
5670L.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);
5671d=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)};
5672L.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,""+
5673h))}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)}
5674function 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))};
5675})()
5676</script><script jstcache="0">var tp = document.getElementById('t');jstProcess(loadTimeData.createJsEvalContext(), tp);</script></body></html>