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