module07-1-后台主机管理

需求


通过前端页面实现操作后台数据库:实现增删改查

目录结构


 

host_manage
├ cmdb  # 名为cmdb的app目录
| ├ migtation  
| ├ __init__.py
| ├ admin.py
| ├ apps.py
| ├ orm_base.py  # 数据库操作引擎 
| ├ tests.py
| └ views.py
├ host_manage  
| ├ __init__.py
| ├ settngs.py
| ├ ulrs.py
| └ wsgi.py
├ static  # 静态文件总目录,以下分类存放各种静态文件的目录  
| ├ css  
| ├ fonts
| ├ i
| ├ img
| └ js
├ templates  # 模板文件目录  
| ├ detail.html  
| ├ error.html
| ├ home.html
| └ login.html
└ manage.py

  

 

代码


 

 1 #! /usr/bin/env python3
 2 # -*- utf-8 -*-
 3 # Author:Jailly
 4 
 5 from sqlalchemy import create_engine
 6 from sqlalchemy.ext.declarative import declarative_base
 7 from sqlalchemy import Column, Integer, String, Enum, Date
 8 
 9 db_user = 'jailly'
10 db_pwd = '123456'
11 db_host = '192.168.1.76'
12 # db_host = '192.168.18.149'
13 
14 engine = create_engine('mysql+pymysql://%s:%s@%s:3306/cmdb?charset=utf8'%(db_user,db_pwd,db_host), encoding='utf-8')
15 Base = declarative_base()
16 
17 
18 class Host(Base):
19     __tablename__ = 'host'
20     id = Column(Integer, primary_key=True)
21     hostname = Column(String(32), nullable=False)
22     ip = Column(String(16), nullable=False)
23     group = Column(String(32), nullable=False)
24     data_center = Column(String(32))
25     status = Column(Enum('0', '1'))
26     update_time = Column(Date)
27     update_staff = Column(String(32))
28     comments = Column(String(255))
29 
30     def __repr__(self):
31         return 'id -> %s,hostname -> %s ' % (self.id, self.hostname)
32 
33 
34 class User(Base):
35     __tablename__ = 'user'
36     id = Column(Integer, primary_key=True)
37     user = Column(String(32))
38     password = Column(String(32))
39 
40 
41 Base.metadata.create_all(bind=engine)
42 
43 # from sqlalchemy.orm import sessionmaker
44 # s = sessionmaker(engine)()
45 # obj = s.query(Host).filter(Host.id==100).first()
46 # print(obj.update_time)
cmdb/orm_base.py
  1 from django.shortcuts import render, redirect
  2 from sqlalchemy.orm import sessionmaker
  3 from sqlalchemy import and_
  4 from cmdb import orm_base
  5 import time
  6 
  7 SessionClass = sessionmaker(bind=orm_base.engine)
  8 session = SessionClass()
  9 
 10 
 11 def login(request):
 12     error_flag = ''
 13 
 14     if request.method == 'POST':
 15         user = request.POST.get('user', None)
 16         pwd = request.POST.get('pwd', None)
 17 
 18         if session.query(orm_base.User).filter(and_(orm_base.User.user == user, orm_base.User.password == pwd)).all():
 19             return redirect('home.html?user=%s' % user)
 20         else:
 21             error_flag = 1
 22 
 23     return render(request, 'login.html', {'error_flag': error_flag})
 24 
 25 
 26 def home(request):
 27     user = ''
 28     if request.method == 'GET':
 29         user = request.GET.get('user', '').strip()
 30 
 31     else:
 32         user = request.POST.get('user',None)
 33         action = request.POST.get('action','').strip()
 34         id = request.POST.get('item-id','').strip()
 35         ip = request.POST.get('item-ip','').strip()
 36         hostname = request.POST.get('item-hostname','').strip()
 37         group = request.POST.get('item-group','').strip()
 38         data_center = request.POST.get('item-data-center','').strip()
 39         status = request.POST.get('item-status','').strip()
 40         update_time = time.strftime('%Y-%m-%d',time.localtime())
 41         update_staff = user
 42         comments = request.POST.get('item-comments','').strip()
 43 
 44         # 1 -> 修改
 45         if action == '1':
 46             host = session.query(orm_base.Host).filter(orm_base.Host.id == id).first()
 47             host.ip = ip
 48             host.hostname = hostname
 49             host.group = group
 50             host.data_center = data_center
 51             host.status = status
 52             host.update_time = update_time
 53             host.update_staff = update_staff
 54             host.comments = comments
 55 
 56         # 0 -> 添加
 57         elif action == '0':
 58             host = orm_base.Host(
 59                 ip = ip,
 60                 hostname = hostname,
 61                 group = group,
 62                 data_center = data_center,
 63                 status = status,
 64                 update_time = update_time,
 65                 update_staff = update_staff,
 66                 comments = comments
 67             )
 68 
 69             session.add(host)
 70 
 71         # 2 -> 删除
 72         elif action == '2':
 73             host = session.query(orm_base.Host).filter(orm_base.Host.id == id).first()
 74             # 传入不存在的id,返回None,所以需判断对象是否为None
 75             if host:
 76                 session.delete(host)
 77 
 78         session.commit()
 79 
 80     if not user:
 81         print('user ->',user)
 82         return redirect('/error')
 83 
 84     host_list = session.query(orm_base.Host).filter().all()
 85 
 86     return render(request, 'home.html', {'user': user, 'host_list': host_list})
 87 
 88 
 89 def detail(request):
 90     host=''
 91     if request.method == 'GET':
 92         host_id = int(request.GET.get('host_id',None))
 93         host = session.query(orm_base.Host).filter(orm_base.Host.id==host_id).first()
 94     
 95     if not host:
 96         return redirect('/error')
 97     
 98     return render(request,'detail.html',{'host':host})
 99 
100 
101 def error(request):
102     return render(request,'error.html')
103 
104 
105  
106     # 递归地捕获异常...
107     # def x():
108     #     for k, v in request.__dict__.items():
109     #         print(k, '-->', end=' ')
110     #
111     #         try:
112     #             print(v)
113     #         except:
114     #             try:
115     #                 print(repr(v))
116     #             except:
117     #                 try:
118     #                     print(str(v))
119     #                 except:
120     #                     continue
121     #
122     # def y():
123     #     try:
124     #         x()
125     #     except:
126     #         y()
127     #
128     # y()
cmdb/views.py
  1 """
  2 Django settings for host_manage project.
  3 
  4 Generated by 'django-admin startproject' using Django 1.11.6.
  5 
  6 For more information on this file, see
  7 https://docs.djangoproject.com/en/1.11/topics/settings/
  8 
  9 For the full list of settings and their values, see
 10 https://docs.djangoproject.com/en/1.11/ref/settings/
 11 """
 12 
 13 import os
 14 
 15 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 16 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 17 
 18 
 19 # Quick-start development settings - unsuitable for production
 20 # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
 21 
 22 # SECURITY WARNING: keep the secret key used in production secret!
 23 SECRET_KEY = 'os7ex@-bxs#mu3apj)psoz9k%ykh97p8d$g^8nq7t7a4oc86o!'
 24 
 25 # SECURITY WARNING: don't run with debug turned on in production!
 26 DEBUG = True
 27 
 28 ALLOWED_HOSTS = []
 29 
 30 
 31 # Application definition
 32 
 33 INSTALLED_APPS = [
 34     'django.contrib.admin',
 35     'django.contrib.auth',
 36     'django.contrib.contenttypes',
 37     'django.contrib.sessions',
 38     'django.contrib.messages',
 39     'django.contrib.staticfiles',
 40 ]
 41 
 42 MIDDLEWARE = [
 43     'django.middleware.security.SecurityMiddleware',
 44     'django.contrib.sessions.middleware.SessionMiddleware',
 45     'django.middleware.common.CommonMiddleware',
 46     # 'django.middleware.csrf.CsrfViewMiddleware',
 47     'django.contrib.auth.middleware.AuthenticationMiddleware',
 48     'django.contrib.messages.middleware.MessageMiddleware',
 49     'django.middleware.clickjacking.XFrameOptionsMiddleware',
 50 ]
 51 
 52 ROOT_URLCONF = 'host_manage.urls'
 53 
 54 TEMPLATES = [
 55     {
 56         'BACKEND': 'django.template.backends.django.DjangoTemplates',
 57         'DIRS': [os.path.join(BASE_DIR,'templates'),],
 58         'APP_DIRS': True,
 59         'OPTIONS': {
 60             'context_processors': [
 61                 'django.template.context_processors.debug',
 62                 'django.template.context_processors.request',
 63                 'django.contrib.auth.context_processors.auth',
 64                 'django.contrib.messages.context_processors.messages',
 65             ],
 66         },
 67     },
 68 ]
 69 
 70 WSGI_APPLICATION = 'host_manage.wsgi.application'
 71 
 72 
 73 # Database
 74 # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
 75 
 76 DATABASES = {
 77     'default': {
 78         'ENGINE': 'django.db.backends.sqlite3',
 79         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
 80     }
 81 }
 82 
 83 
 84 # Password validation
 85 # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
 86 
 87 AUTH_PASSWORD_VALIDATORS = [
 88     {
 89         'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
 90     },
 91     {
 92         'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
 93     },
 94     {
 95         'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
 96     },
 97     {
 98         'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
 99     },
100 ]
101 
102 
103 # Internationalization
104 # https://docs.djangoproject.com/en/1.11/topics/i18n/
105 
106 LANGUAGE_CODE = 'en-us'
107 
108 TIME_ZONE = 'UTC'
109 
110 USE_I18N = True
111 
112 USE_L10N = True
113 
114 USE_TZ = True
115 
116 
117 # Static files (CSS, JavaScript, Images)
118 # https://docs.djangoproject.com/en/1.11/howto/static-files/
119 
120 STATIC_URL = '/static/'
121 
122 STATICFILES_DIRS = (os.path.join(BASE_DIR,'static'),)
123 
124 TEMPLATE_DIRS = (os.path.join(BASE_DIR,  'templates'),)
host_manage/settings.py
 1 """host_manage URL Configuration
 2 
 3 The `urlpatterns` list routes URLs to views. For more information please see:
 4     https://docs.djangoproject.com/en/1.11/topics/http/urls/
 5 Examples:
 6 Function views
 7     1. Add an import:  from my_app import views
 8     2. Add a URL to urlpatterns:  url(r'^$', views.home, name='home')
 9 Class-based views
10     1. Add an import:  from other_app.views import Home
11     2. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
12 Including another URLconf
13     1. Import the include() function: from django.conf.urls import url, include
14     2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
15 """
16 from django.conf.urls import url
17 from django.contrib import admin
18 
19 from cmdb import views
20 
21 urlpatterns = [
22     url(r'^admin/', admin.site.urls),
23     url(r'^$',views.login),
24     url(r'^login',views.login),
25     url(r'^home',views.home),
26     url(r'^detail',views.detail),
27     url(r'^error',views.error)
28 ]
host_manage/urls.py

/static/ 下 各文件 (略)

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

 

 

 

 
posted @ 2017-11-01 14:59  jailly  阅读(5259)  评论(0编辑  收藏  举报