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