需求
通过前端页面实现操作后台数据库:实现增删改查
目录结构
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)

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()

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'),)

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 ]
/static/ 下 各文件 (略)


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: 'Segoe UI',Arial,'Microsoft Yahei',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 && suggestionsSummaryList.length)" jstcache="6"> 4101 <p jsvalues=".innerHTML:suggestionsSummaryListHeader" jstcache="13">请试试以下办法:</p> 4102 <ul jsvalues=".className:suggestionsSummaryList.length == 1 ? 'single-suggestion' : ''" 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 && suggestionsDetails.length > 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 菜单 >“<span jscontent="settingsTitle" jstcache="17">设置</span>”>“<span jscontent="advancedTitle" jstcache="18">显示高级设置…</span>”>“<span jscontent="proxyTitle" jstcache="19">更改代理服务器设置…</span>”>“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,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" jstcache="0"> 4144 <img id="offline-resources-2x" src="data:image/png;base64,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" 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>

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>

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>