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