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