From bbf7c910aa4ac5c9ca54c664cfa5e2acab8b31c7 Mon Sep 17 00:00:00 2001 From: QuiteAFancyEmerald Date: Tue, 19 Jan 2021 22:25:52 +0000 Subject: [PATCH] Added Games --- views/archive/dino/.gitignore | 45 + views/archive/dino/README.md | 16 + views/archive/dino/dino.css | 549 ++++ views/archive/dino/dino.js | 2774 +++++++++++++++++ views/archive/dino/dino.min.css | 2 + views/archive/dino/dino.min.js | 65 + .../default_100_percent/100-disabled.png | Bin 0 -> 382 bytes .../default_100_percent/100-error-offline.png | Bin 0 -> 196 bytes .../100-offline-sprite.png | Bin 0 -> 2433 bytes .../default_200_percent/200-disabled.png | Bin 0 -> 479 bytes .../default_200_percent/200-error-offline.png | Bin 0 -> 269 bytes .../200-offline-sprite.png | Bin 0 -> 3056 bytes views/archive/dino/index.html | 26 + views/archive/dino/sounds/button-press.mp3 | Bin 0 -> 5178 bytes views/archive/dino/sounds/hit.mp3 | Bin 0 -> 7212 bytes views/archive/dino/sounds/score-reached.mp3 | Bin 0 -> 9250 bytes views/pages/nav/games5.html | 8 + 17 files changed, 3485 insertions(+) create mode 100644 views/archive/dino/.gitignore create mode 100644 views/archive/dino/README.md create mode 100644 views/archive/dino/dino.css create mode 100644 views/archive/dino/dino.js create mode 100644 views/archive/dino/dino.min.css create mode 100644 views/archive/dino/dino.min.js create mode 100644 views/archive/dino/images/default_100_percent/100-disabled.png create mode 100644 views/archive/dino/images/default_100_percent/100-error-offline.png create mode 100644 views/archive/dino/images/default_100_percent/100-offline-sprite.png create mode 100644 views/archive/dino/images/default_200_percent/200-disabled.png create mode 100644 views/archive/dino/images/default_200_percent/200-error-offline.png create mode 100644 views/archive/dino/images/default_200_percent/200-offline-sprite.png create mode 100644 views/archive/dino/index.html create mode 100644 views/archive/dino/sounds/button-press.mp3 create mode 100644 views/archive/dino/sounds/hit.mp3 create mode 100644 views/archive/dino/sounds/score-reached.mp3 diff --git a/views/archive/dino/.gitignore b/views/archive/dino/.gitignore new file mode 100644 index 00000000..b671a384 --- /dev/null +++ b/views/archive/dino/.gitignore @@ -0,0 +1,45 @@ +# Log file +*.log + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar +*.7z + +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# macOS +.DS_Store + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/views/archive/dino/README.md b/views/archive/dino/README.md new file mode 100644 index 00000000..e24a81bd --- /dev/null +++ b/views/archive/dino/README.md @@ -0,0 +1,16 @@ +# T-Rex Runner + +T-Rex Runner is an Easter Egg in Google Chrome which appears when you suddenly lose internet access. [Here](https://www.blog.google/products/chrome/chrome-dino/) is the origin of this game. + +The dino game is extracted from the Chromium [source code](https://cs.chromium.org/chromium/src/components/neterror/resources/offline.js) in commit `24d8c44` + +## Controls + +* Press the space bar to jump and to start the game +* Use the down arrow key to duck + +## How to play + +You can click [here](https://congerh.github.io/dino/) to play online. + +You can also play the origin Chrome dino game without turning off your network connections. Just open `chrome://dino` in your web browser, and you’ll be taken to an "arcade mode" where you can practice in a full-window environment. diff --git a/views/archive/dino/dino.css b/views/archive/dino/dino.css new file mode 100644 index 00000000..ae954777 --- /dev/null +++ b/views/archive/dino/dino.css @@ -0,0 +1,549 @@ +/* Copyright 2017 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * Extract source code from chromium by congerh. */ + +body { + --google-blue-600: rgb(26, 115, 232); + --google-blue-700: rgb(25, 103, 210); + --google-gray-50: rgb(248, 249, 250); + --google-gray-500: rgb(154, 160, 166); + --google-gray-600: rgb(128, 134, 139); + --google-gray-700: rgb(95, 99, 105); + background-color: #fff; + color: var(--google-gray-700); + word-wrap: break-word; +} + +html { + -webkit-text-size-adjust: 100%; + font-size: 125%; +} + +.icon { + background-repeat: no-repeat; + background-size: 100%; +} + +/* Copyright 2014 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +.bad-clock button, +.captive-portal button, +.main-frame-blocked button, +.neterror button, +.offline button, +.pdf button, +.ssl button, +.safe-browsing-billing button { + background: var(--google-blue-600); +} + +.error-code { + color: #646464; + font-size: .86667em; + text-transform: uppercase; + margin-top: 12px; +} + +h1 { + color: var(--google-gray-900); + font-size: 1.6em; + font-weight: normal; + line-height: 1.25em; + margin-bottom: 16px; +} + +h2 { + font-size: 1.2em; + font-weight: normal; +} + +.icon { + height: 72px; + margin: 0 0 40px; + width: 72px; +} + +.interstitial-wrapper { + box-sizing: border-box; + font-size: 1em; + line-height: 1.6em; + margin: 14vh auto 0; + max-width: 600px; + width: 100%; +} + +#main-message > p { + display: inline; +} + +@media (max-width: 700px) { + .interstitial-wrapper { + padding: 0 10%; + } +} + +@media (max-width: 420px) { + .interstitial-wrapper { + padding: 0 5%; + } +} + +/** + * Mobile specific styling. + * Navigation buttons are anchored to the bottom of the screen. + * Details message replaces the top content in its own scrollable area. + */ + +/* Fixed nav. */ +@media (min-width: 240px) and (max-width: 420px) and (min-height: 401px), + (min-width: 421px) and (min-height: 240px) and (max-height: 560px) { + body .nav-wrapper { + background: #fff; + bottom: 0; + box-shadow: 0 -22px 40px #fff; + left: 0; + margin: 0 auto; + max-width: 736px; + padding-left: 24px; + padding-right: 24px; + position: fixed; + right: 0; + width: 100%; + z-index: 2; + } + + .interstitial-wrapper { + max-width: 736px; + } + + #details, + #main-content { + padding-bottom: 40px; + } +} + +@media (max-width: 420px) and (orientation: portrait), + (max-height: 560px) { + body { + margin: 0 auto; + } + + #details.hidden, + #main-content.hidden { + display: block; + height: 0; + opacity: 0; + overflow: hidden; + padding-bottom: 0; + transition: none; + } + + h1 { + font-size: 1.5em; + margin-bottom: 8px; + } + + .icon { + margin-bottom: 5.69vh; + } + + .interstitial-wrapper { + box-sizing: border-box; + margin: 7vh auto 12px; + padding: 0 24px; + position: relative; + } + + .interstitial-wrapper p { + font-size: .95em; + line-height: 1.61em; + margin-top: 8px; + } + + #main-content { + margin: 0; + transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1); + } +} + +@media (min-width: 421px) and (min-height: 500px) and (max-height: 560px) { + .interstitial-wrapper { + margin-top: 10vh; + } +} + +@media (min-height: 400px) and (orientation:portrait) { + .interstitial-wrapper { + margin-bottom: 145px; + } +} + + +@media (min-height: 500px) and (max-height: 650px) and (max-width: 414px) and + (orientation: portrait) { + .interstitial-wrapper { + margin-top: 7vh; + } +} + +@media (min-height: 650px) and (max-width: 414px) and (orientation: portrait) { + .interstitial-wrapper { + margin-top: 10vh; + } +} + +/* Small mobile screens. No fixed nav. */ +@media (max-height: 400px) and (orientation: portrait), + (max-height: 239px) and (orientation: landscape), + (max-width: 419px) and (max-height: 399px) { + .interstitial-wrapper { + display: flex; + flex-direction: column; + margin-bottom: 0; + } + + #main-content { + flex: 1 1 auto; + order: 0; + } +} + +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +/* Don't use the main frame div when the error is in a subframe. */ +html[subframe] #main-frame-error { + display: none; +} + +/* Don't use the subframe error div when the error is in a main frame. */ +html:not([subframe]) #sub-frame-error { + display: none; +} + +h1 { + margin-top: 0; + word-wrap: break-word; +} + +h1 span { + font-weight: 500; +} + +h2 { + color: #666; + font-size: 1.2em; + font-weight: normal; + margin: 10px 0; +} + +a { + color: rgb(17, 85, 204); + text-decoration: none; +} + +.icon { + -webkit-user-select: none; + display: inline-block; +} + +.icon-offline { + content: -webkit-image-set( + url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAGxJREFUeF7tyMEJwkAQRuFf5ipMKxYQiJ3Z2nSwrWwBA0+DQZcdxEOueaePp9+dQZFB7GpUcURSVU66yVNFj6LFICatThZB6r/ko/pbRpUgilY0Cbw5sNmb9txGXUKyuH7eV25x39DtJXUNPQGJtWFV+BT/QAAAAABJRU5ErkJggg==) 1x, + url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQBAMAAAAVaP+LAAAAGFBMVEUAAABTU1NNTU1TU1NPT09SUlJSUlJTU1O8B7DEAAAAB3RSTlMAoArVKvVgBuEdKgAAAJ1JREFUeF7t1TEOwyAMQNG0Q6/UE+RMXD9d/tC6womIFSL9P+MnAYOXeTIzMzMzMzMzaz8J9Ri6HoITmuHXhISE8nEh9yxDh55aCEUoTGbbQwjqHwIkRAEiIaG0+0AA9VBMaE89Rogeoww936MQrWdBr4GN/z0IAdQ6nQ/FIpRXDwHcA+JIJcQowQAlFUA0MfQpXLlVQfkzR4igS6ENjknm/wiaGhsAAAAASUVORK5CYII=) 2x); + position: relative; +} + +.icon-disabled { + content: -webkit-image-set( + 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, + url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOAAAACQAQMAAAArwfVjAAAABlBMVEVMaXFTU1OXUj8tAAAAAXRSTlMAQObYZgAAAYdJREFUeF7F1EFqwzAUBNARAmVj0FZe5QoBH6BX+dn4GlY2PYNzGx/A0CvkCIJuvIraKJKbgBvzf2g62weDGD7CYggpfFReis4J0ey9EGFIiEQQojFSlA9kSIiqd0KkFjKsewgRbStEN19mxUPTtmW9HQ/h6tyqNQ8NlSMZdzyE6qkoE0trVYGFm0n1WYeBhduzwbwBC7voS+vIxfeMjeaiLxsMMtQNwMPtuew+DjzcTHk8YMfDknEcIUOtf2lVfgVH3K4Xv5PRYAXRVMtItIJ3rfaCIVn9DsTH2NxisAVRex2Hh3hX+/mRUR08bAwPEYsI51ZxWH4Q0SpicQRXeyEaIug48FEdegARfMz/tADVsRciwTAxW308ehmC2gLraC+YCbV3QoTZexa+zegAEW5PhhgYfmbvJgcRqngGByOSXdFJcLk2JeDPEN0kxe1JhIt5FiFA+w+ItMELsUyPF2IaJ4aILqb4FbxPwhImwj6JauKgDUCYaxmYIsd4KXdMjIC9ItB5Bn4BNRwsG0XM2nwAAAAASUVORK5CYII=) 2x); + width: 112px; +} + +.error-code { + display: block; + font-size: .8em; +} + +.hidden { + display: none; +} + +#suggestions-list p { + margin-block-end: 0; +} + +#suggestions-list ul { + margin-top: 0; +} + +.snackbar { + background: #323232; + border-radius: 2px; + bottom: 24px; + box-sizing: border-box; + color: #fff; + font-size: .87em; + left: 24px; + max-width: 568px; + min-width: 288px; + opacity: 0; + padding: 16px 24px 12px; + position: fixed; + transform: translateY(90px); + will-change: opacity, transform; + z-index: 999; +} + +.snackbar-show { + -webkit-animation: + show-snackbar .25s cubic-bezier(0.0, 0.0, 0.2, 1) forwards, + hide-snackbar .25s cubic-bezier(0.4, 0.0, 1, 1) forwards 5s; +} + +@-webkit-keyframes show-snackbar { + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@-webkit-keyframes hide-snackbar { + 0% { + opacity: 1; + transform: translateY(0); + } + 100% { + opacity: 0; + transform: translateY(90px); + } +} + +/* Decrease padding at low sizes. */ +@media (max-width: 640px), (max-height: 640px) { + h1 { + margin: 0 0 15px; + } + #content-top { + margin: 15px; + } +} + +/* Don't allow overflow when in a subframe. */ +html[subframe] body { + overflow: hidden; +} + +#sub-frame-error { + -webkit-align-items: center; + background-color: #DDD; + display: -webkit-flex; + -webkit-flex-flow: column; + height: 100%; + -webkit-justify-content: center; + left: 0; + position: absolute; + text-align: center; + top: 0; + transition: background-color .2s ease-in-out; + width: 100%; +} + +#sub-frame-error:hover { + background-color: #EEE; +} + +#sub-frame-error .icon-generic { + margin: 0 0 16px; +} + +#sub-frame-error-details { + margin: 0 10px; + text-align: center; + visibility: hidden; +} + +/* Show details only when hovering. */ +#sub-frame-error:hover #sub-frame-error-details { + visibility: visible; +} + +/* If the iframe is too small, always hide the error code. */ +/* TODO(mmenke): See if overflow: no-display works better, once supported. */ +@media (max-width: 200px), (max-height: 95px) { + #sub-frame-error-details { + display: none; + } +} + +/* Adjust icon for small embedded frames in apps. */ +@media (max-height: 100px) { + #sub-frame-error .icon-generic { + height: auto; + margin: 0; + padding-top: 0; + width: 25px; + } +} + +/* Offline page */ +.offline { + transition: -webkit-filter 1.5s cubic-bezier(0.65, 0.05, 0.36, 1), + background-color 1.5s cubic-bezier(0.65, 0.05, 0.36, 1); + will-change: -webkit-filter, background-color; +} + +.offline #main-message > p { + display: none; +} + +.offline.inverted { + -webkit-filter: invert(100%); + background-color: #000; +} + +.offline .interstitial-wrapper { + color: #2b2b2b; + font-size: 1em; + line-height: 1.55; + margin: 0 auto; + max-width: 600px; + padding-top: 100px; + width: 100%; +} + +.offline .runner-container { + direction: ltr; + height: 150px; + max-width: 600px; + overflow: hidden; + position: absolute; + top: 35px; + width: 44px; +} + +.offline .runner-canvas { + height: 150px; + max-width: 600px; + opacity: 1; + overflow: hidden; + position: absolute; + top: 0; + z-index: 10; +} + +.offline .controller { + background: rgba(247,247,247, .1); + height: 100vh; + left: 0; + position: absolute; + top: 0; + width: 100vw; + z-index: 9; +} + +#offline-resources { + display: none; +} + +@media (max-width: 420px) { + .snackbar { + left: 0; + bottom: 0; + width: 100%; + border-radius: 0; + } +} + +@media (max-height: 350px) { + h1 { + margin: 0 0 15px; + } + + .icon-offline { + margin: 0 0 10px; + } + + .interstitial-wrapper { + margin-top: 5%; + } +} + +@media (min-width: 420px) and (max-width: 736px) and + (min-height: 240px) and (max-height: 420px) and + (orientation:landscape) { + .interstitial-wrapper { + margin-bottom: 100px; + } +} + +@media (max-width: 360px) and (max-height: 480px) { + .offline .interstitial-wrapper { + padding-top: 60px; + } + + .offline .runner-container { + top: 8px; + } +} + +@media (min-height: 240px) and (orientation: landscape) { + .offline .interstitial-wrapper { + margin-bottom: 90px; + } + + .icon-offline { + margin-bottom: 20px; + } +} + +@media (max-height: 320px) and (orientation: landscape) { + .icon-offline { + margin-bottom: 0; + } + + .offline .runner-container { + top: 10px; + } +} + +@media (max-width: 240px) { + .interstitial-wrapper { + overflow: inherit; + padding: 0 8px; + } +} + +.arcade-mode, +.arcade-mode .runner-container, +.arcade-mode .runner-canvas { + image-rendering: pixelated; + max-width: 100%; + overflow: hidden; +} + +.arcade-mode #buttons, +.arcade-mode #main-content { + opacity: 0; + overflow: hidden; +} + +.arcade-mode .interstitial-wrapper { + height: 100vh; + max-width: 100%; + overflow: hidden; +} + +.arcade-mode .runner-container { + left: 0; + margin: auto; + right: 0; + transform-origin: top center; + transition: transform 250ms cubic-bezier(0.4, 0.0, 1, 1) .4s; + z-index: 2; +} diff --git a/views/archive/dino/dino.js b/views/archive/dino/dino.js new file mode 100644 index 00000000..14180334 --- /dev/null +++ b/views/archive/dino/dino.js @@ -0,0 +1,2774 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Extract source code from Chromium by congerh. +(function() { +'use strict'; +/** + * T-Rex runner. + * @param {string} outerContainerId Outer containing element id. + * @param {Object} opt_config + * @constructor + * @export + */ +function Runner(outerContainerId, opt_config) { + // Singleton + if (Runner.instance_) { + return Runner.instance_; + } + Runner.instance_ = this; + + this.outerContainerEl = document.querySelector(outerContainerId); + this.containerEl = null; + this.snackbarEl = null; + // A div to intercept touch events. Only set while (playing && useTouch). + this.touchController = null; + + this.config = opt_config || Runner.config; + // Logical dimensions of the container. + this.dimensions = Runner.defaultDimensions; + + this.canvas = null; + this.canvasCtx = null; + + this.tRex = null; + + this.distanceMeter = null; + this.distanceRan = 0; + + this.highestScore = 0; + + this.time = 0; + this.runningTime = 0; + this.msPerFrame = 1000 / FPS; + this.currentSpeed = this.config.SPEED; + + this.obstacles = []; + + this.activated = false; // Whether the easter egg has been activated. + this.playing = false; // Whether the game is currently in play state. + this.crashed = false; + this.paused = false; + this.inverted = false; + this.invertTimer = 0; + this.resizeTimerId_ = null; + + this.playCount = 0; + + // Sound FX. + this.audioBuffer = null; + this.soundFx = {}; + + // Global web audio context for playing sounds. + this.audioContext = null; + + // Images. + this.images = {}; + this.imagesLoaded = 0; + + if (this.isDisabled()) { + this.setupDisabledRunner(); + } else { + this.loadImages(); + } +} +window['Runner'] = Runner; + + +/** + * Default game width. + * @const + */ +var DEFAULT_WIDTH = 600; + +/** + * Frames per second. + * @const + */ +var FPS = 60; + +/** @const */ +var IS_HIDPI = window.devicePixelRatio > 1; + +/** @const */ +var IS_IOS = /iPad|iPhone|iPod/.test(window.navigator.platform); + +/** @const */ +var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS; + +/** @const */ +var ARCADE_MODE_URL = 'chrome://dino/'; + +/** + * Default game configuration. + * @enum {number} + */ +Runner.config = { + ACCELERATION: 0.001, + BG_CLOUD_SPEED: 0.2, + BOTTOM_PAD: 10, + // Scroll Y threshold at which the game can be activated. + CANVAS_IN_VIEW_OFFSET: -10, + CLEAR_TIME: 3000, + CLOUD_FREQUENCY: 0.5, + GAMEOVER_CLEAR_TIME: 750, + GAP_COEFFICIENT: 0.6, + GRAVITY: 0.6, + INITIAL_JUMP_VELOCITY: 12, + INVERT_FADE_DURATION: 12000, + INVERT_DISTANCE: 700, + MAX_BLINK_COUNT: 3, + MAX_CLOUDS: 6, + MAX_OBSTACLE_LENGTH: 3, + MAX_OBSTACLE_DUPLICATION: 2, + MAX_SPEED: 13, + MIN_JUMP_HEIGHT: 35, + MOBILE_SPEED_COEFFICIENT: 1.2, + RESOURCE_TEMPLATE_ID: 'audio-resources', + SPEED: 6, + SPEED_DROP_COEFFICIENT: 3, + ARCADE_MODE_INITIAL_TOP_POSITION: 35, + ARCADE_MODE_TOP_POSITION_PERCENT: 0.1 +}; + + +/** + * Default dimensions. + * @enum {string} + */ +Runner.defaultDimensions = { + WIDTH: DEFAULT_WIDTH, + HEIGHT: 150 +}; + + +/** + * CSS class names. + * @enum {string} + */ +Runner.classes = { + ARCADE_MODE: 'arcade-mode', + CANVAS: 'runner-canvas', + CONTAINER: 'runner-container', + CRASHED: 'crashed', + ICON: 'icon-offline', + INVERTED: 'inverted', + SNACKBAR: 'snackbar', + SNACKBAR_SHOW: 'snackbar-show', + TOUCH_CONTROLLER: 'controller' +}; + + +/** + * Sprite definition layout of the spritesheet. + * @enum {Object} + */ +Runner.spriteDefinition = { + LDPI: { + CACTUS_LARGE: {x: 332, y: 2}, + CACTUS_SMALL: {x: 228, y: 2}, + CLOUD: {x: 86, y: 2}, + HORIZON: {x: 2, y: 54}, + MOON: {x: 484, y: 2}, + PTERODACTYL: {x: 134, y: 2}, + RESTART: {x: 2, y: 2}, + TEXT_SPRITE: {x: 655, y: 2}, + TREX: {x: 848, y: 2}, + STAR: {x: 645, y: 2} + }, + HDPI: { + CACTUS_LARGE: {x: 652, y: 2}, + CACTUS_SMALL: {x: 446, y: 2}, + CLOUD: {x: 166, y: 2}, + HORIZON: {x: 2, y: 104}, + MOON: {x: 954, y: 2}, + PTERODACTYL: {x: 260, y: 2}, + RESTART: {x: 2, y: 2}, + TEXT_SPRITE: {x: 1294, y: 2}, + TREX: {x: 1678, y: 2}, + STAR: {x: 1276, y: 2} + } +}; + + +/** + * Sound FX. Reference to the ID of the audio tag on interstitial page. + * @enum {string} + */ +Runner.sounds = { + BUTTON_PRESS: 'offline-sound-press', + HIT: 'offline-sound-hit', + SCORE: 'offline-sound-reached' +}; + + +/** + * Key code mapping. + * @enum {Object} + */ +Runner.keycodes = { + JUMP: {'38': 1, '32': 1}, // Up, spacebar + DUCK: {'40': 1}, // Down + RESTART: {'13': 1} // Enter +}; + + +/** + * Runner event names. + * @enum {string} + */ +Runner.events = { + ANIM_END: 'webkitAnimationEnd', + CLICK: 'click', + KEYDOWN: 'keydown', + KEYUP: 'keyup', + POINTERDOWN: 'pointerdown', + POINTERUP: 'pointerup', + RESIZE: 'resize', + TOUCHEND: 'touchend', + TOUCHSTART: 'touchstart', + VISIBILITY: 'visibilitychange', + BLUR: 'blur', + FOCUS: 'focus', + LOAD: 'load' +}; + +Runner.prototype = { + /** + * Whether the easter egg has been disabled. CrOS enterprise enrolled devices. + * @return {boolean} + */ + isDisabled: function() { + // return loadTimeData && loadTimeData.valueExists('disabledEasterEgg'); + return false; + }, + + /** + * For disabled instances, set up a snackbar with the disabled message. + */ + setupDisabledRunner: function() { + this.containerEl = document.createElement('div'); + this.containerEl.className = Runner.classes.SNACKBAR; + this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg'); + this.outerContainerEl.appendChild(this.containerEl); + + // Show notification when the activation key is pressed. + document.addEventListener(Runner.events.KEYDOWN, function(e) { + if (Runner.keycodes.JUMP[e.keyCode]) { + this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW); + document.querySelector('.icon').classList.add('icon-disabled'); + } + }.bind(this)); + }, + + /** + * Setting individual settings for debugging. + * @param {string} setting + * @param {*} value + */ + updateConfigSetting: function(setting, value) { + if (setting in this.config && value != undefined) { + this.config[setting] = value; + + switch (setting) { + case 'GRAVITY': + case 'MIN_JUMP_HEIGHT': + case 'SPEED_DROP_COEFFICIENT': + this.tRex.config[setting] = value; + break; + case 'INITIAL_JUMP_VELOCITY': + this.tRex.setJumpVelocity(value); + break; + case 'SPEED': + this.setSpeed(value); + break; + } + } + }, + + /** + * Cache the appropriate image sprite from the page and get the sprite sheet + * definition. + */ + loadImages: function() { + if (IS_HIDPI) { + Runner.imageSprite = document.getElementById('offline-resources-2x'); + this.spriteDef = Runner.spriteDefinition.HDPI; + } else { + Runner.imageSprite = document.getElementById('offline-resources-1x'); + this.spriteDef = Runner.spriteDefinition.LDPI; + } + + if (Runner.imageSprite.complete) { + this.init(); + } else { + // If the images are not yet loaded, add a listener. + Runner.imageSprite.addEventListener(Runner.events.LOAD, + this.init.bind(this)); + } + }, + + /** + * Load and decode base 64 encoded sounds. + */ + loadSounds: function() { + if (!IS_IOS) { + this.audioContext = new AudioContext(); + + var resourceTemplate = + document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content; + + for (var sound in Runner.sounds) { + var soundSrc = + resourceTemplate.getElementById(Runner.sounds[sound]).src; + soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1); + var buffer = decodeBase64ToArrayBuffer(soundSrc); + + // Async, so no guarantee of order in array. + this.audioContext.decodeAudioData(buffer, function(index, audioData) { + this.soundFx[index] = audioData; + }.bind(this, sound)); + } + } + }, + + /** + * Sets the game speed. Adjust the speed accordingly if on a smaller screen. + * @param {number} opt_speed + */ + setSpeed: function(opt_speed) { + var speed = opt_speed || this.currentSpeed; + + // Reduce the speed on smaller mobile screens. + if (this.dimensions.WIDTH < DEFAULT_WIDTH) { + var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH * + this.config.MOBILE_SPEED_COEFFICIENT; + this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed; + } else if (opt_speed) { + this.currentSpeed = opt_speed; + } + }, + + /** + * Game initialiser. + */ + init: function() { + // Hide the static icon. + document.querySelector('.' + Runner.classes.ICON).style.visibility = + 'hidden'; + + this.adjustDimensions(); + this.setSpeed(); + + this.containerEl = document.createElement('div'); + this.containerEl.className = Runner.classes.CONTAINER; + + // Player canvas container. + this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH, + this.dimensions.HEIGHT, Runner.classes.PLAYER); + + this.canvasCtx = this.canvas.getContext('2d'); + this.canvasCtx.fillStyle = '#f7f7f7'; + this.canvasCtx.fill(); + Runner.updateCanvasScaling(this.canvas); + + // Horizon contains clouds, obstacles and the ground. + this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, + this.config.GAP_COEFFICIENT); + + // Distance meter + this.distanceMeter = new DistanceMeter(this.canvas, + this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH); + + // Draw t-rex + this.tRex = new Trex(this.canvas, this.spriteDef.TREX); + + this.outerContainerEl.appendChild(this.containerEl); + + this.startListening(); + this.update(); + + window.addEventListener(Runner.events.RESIZE, + this.debounceResize.bind(this)); + }, + + /** + * Create the touch controller. A div that covers whole screen. + */ + createTouchController: function() { + this.touchController = document.createElement('div'); + this.touchController.className = Runner.classes.TOUCH_CONTROLLER; + this.touchController.addEventListener(Runner.events.TOUCHSTART, this); + this.touchController.addEventListener(Runner.events.TOUCHEND, this); + this.outerContainerEl.appendChild(this.touchController); + }, + + /** + * Debounce the resize event. + */ + debounceResize: function() { + if (!this.resizeTimerId_) { + this.resizeTimerId_ = + setInterval(this.adjustDimensions.bind(this), 250); + } + }, + + /** + * Adjust game space dimensions on resize. + */ + adjustDimensions: function() { + clearInterval(this.resizeTimerId_); + this.resizeTimerId_ = null; + + var boxStyles = window.getComputedStyle(this.outerContainerEl); + var padding = Number(boxStyles.paddingLeft.substr(0, + boxStyles.paddingLeft.length - 2)); + + this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2; + if (this.isArcadeMode()) { + this.dimensions.WIDTH = Math.min(DEFAULT_WIDTH, this.dimensions.WIDTH); + if (this.activated) { + this.setArcadeModeContainerScale(); + } + } + + // Redraw the elements back onto the canvas. + if (this.canvas) { + this.canvas.width = this.dimensions.WIDTH; + this.canvas.height = this.dimensions.HEIGHT; + + Runner.updateCanvasScaling(this.canvas); + + this.distanceMeter.calcXPos(this.dimensions.WIDTH); + this.clearCanvas(); + this.horizon.update(0, 0, true); + this.tRex.update(0); + + // Outer container and distance meter. + if (this.playing || this.crashed || this.paused) { + this.containerEl.style.width = this.dimensions.WIDTH + 'px'; + this.containerEl.style.height = this.dimensions.HEIGHT + 'px'; + this.distanceMeter.update(0, Math.ceil(this.distanceRan)); + this.stop(); + } else { + this.tRex.draw(0, 0); + } + + // Game over panel. + if (this.crashed && this.gameOverPanel) { + this.gameOverPanel.updateDimensions(this.dimensions.WIDTH); + this.gameOverPanel.draw(); + } + } + }, + + /** + * Play the game intro. + * Canvas container width expands out to the full width. + */ + playIntro: function() { + if (!this.activated && !this.crashed) { + this.playingIntro = true; + this.tRex.playingIntro = true; + + // CSS animation definition. + var keyframes = '@-webkit-keyframes intro { ' + + 'from { width:' + Trex.config.WIDTH + 'px }' + + 'to { width: ' + this.dimensions.WIDTH + 'px }' + + '}'; + document.styleSheets[0].insertRule(keyframes, 0); + + this.containerEl.addEventListener(Runner.events.ANIM_END, + this.startGame.bind(this)); + + this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both'; + this.containerEl.style.width = this.dimensions.WIDTH + 'px'; + + this.setPlayStatus(true); + this.activated = true; + } else if (this.crashed) { + this.restart(); + } + }, + + + /** + * Update the game status to started. + */ + startGame: function() { + if (this.isArcadeMode()) { + this.setArcadeMode(); + } + this.runningTime = 0; + this.playingIntro = false; + this.tRex.playingIntro = false; + this.containerEl.style.webkitAnimation = ''; + this.playCount++; + + // Handle tabbing off the page. Pause the current game. + document.addEventListener(Runner.events.VISIBILITY, + this.onVisibilityChange.bind(this)); + + window.addEventListener(Runner.events.BLUR, + this.onVisibilityChange.bind(this)); + + window.addEventListener(Runner.events.FOCUS, + this.onVisibilityChange.bind(this)); + }, + + clearCanvas: function() { + this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH, + this.dimensions.HEIGHT); + }, + + /** + * Checks whether the canvas area is in the viewport of the browser + * through the current scroll position. + * @return boolean. + */ + isCanvasInView: function() { + return this.containerEl.getBoundingClientRect().top > + Runner.config.CANVAS_IN_VIEW_OFFSET; + }, + + /** + * Update the game frame and schedules the next one. + */ + update: function() { + this.updatePending = false; + + var now = getTimeStamp(); + var deltaTime = now - (this.time || now); + + this.time = now; + + if (this.playing) { + this.clearCanvas(); + + if (this.tRex.jumping) { + this.tRex.updateJump(deltaTime); + } + + this.runningTime += deltaTime; + var hasObstacles = this.runningTime > this.config.CLEAR_TIME; + + // First jump triggers the intro. + if (this.tRex.jumpCount == 1 && !this.playingIntro) { + this.playIntro(); + } + + // The horizon doesn't move until the intro is over. + if (this.playingIntro) { + this.horizon.update(0, this.currentSpeed, hasObstacles); + } else { + deltaTime = !this.activated ? 0 : deltaTime; + this.horizon.update(deltaTime, this.currentSpeed, hasObstacles, + this.inverted); + } + + // Check for collisions. + var collision = hasObstacles && + checkForCollision(this.horizon.obstacles[0], this.tRex); + + if (!collision) { + this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame; + + if (this.currentSpeed < this.config.MAX_SPEED) { + this.currentSpeed += this.config.ACCELERATION; + } + } else { + this.gameOver(); + } + + var playAchievementSound = this.distanceMeter.update(deltaTime, + Math.ceil(this.distanceRan)); + + if (playAchievementSound) { + this.playSound(this.soundFx.SCORE); + } + + // Night mode. + if (this.invertTimer > this.config.INVERT_FADE_DURATION) { + this.invertTimer = 0; + this.invertTrigger = false; + this.invert(); + } else if (this.invertTimer) { + this.invertTimer += deltaTime; + } else { + var actualDistance = + this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan)); + + if (actualDistance > 0) { + this.invertTrigger = !(actualDistance % + this.config.INVERT_DISTANCE); + + if (this.invertTrigger && this.invertTimer === 0) { + this.invertTimer += deltaTime; + this.invert(); + } + } + } + } + + if (this.playing || (!this.activated && + this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) { + this.tRex.update(deltaTime); + this.scheduleNextUpdate(); + } + }, + + /** + * Event handler. + */ + handleEvent: function(e) { + return (function(evtType, events) { + switch (evtType) { + case events.KEYDOWN: + case events.TOUCHSTART: + case events.POINTERDOWN: + this.onKeyDown(e); + break; + case events.KEYUP: + case events.TOUCHEND: + case events.POINTERUP: + this.onKeyUp(e); + break; + } + }.bind(this))(e.type, Runner.events); + }, + + /** + * Bind relevant key / mouse / touch listeners. + */ + startListening: function() { + // Keys. + document.addEventListener(Runner.events.KEYDOWN, this); + document.addEventListener(Runner.events.KEYUP, this); + + // Touch / pointer. + this.containerEl.addEventListener(Runner.events.TOUCHSTART, this); + document.addEventListener(Runner.events.POINTERDOWN, this); + document.addEventListener(Runner.events.POINTERUP, this); + }, + + /** + * Remove all listeners. + */ + stopListening: function() { + document.removeEventListener(Runner.events.KEYDOWN, this); + document.removeEventListener(Runner.events.KEYUP, this); + + if (this.touchController) { + this.touchController.removeEventListener(Runner.events.TOUCHSTART, this); + this.touchController.removeEventListener(Runner.events.TOUCHEND, this); + } + + this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this); + document.removeEventListener(Runner.events.POINTERDOWN, this); + document.removeEventListener(Runner.events.POINTERUP, this); + }, + + /** + * Process keydown. + * @param {Event} e + */ + onKeyDown: function(e) { + // Prevent native page scrolling whilst tapping on mobile. + if (IS_MOBILE && this.playing) { + e.preventDefault(); + } + + if (this.isCanvasInView()) { + if (!this.crashed && !this.paused) { + if (Runner.keycodes.JUMP[e.keyCode] || + e.type == Runner.events.TOUCHSTART) { + e.preventDefault(); + // Starting the game for the first time. + if (!this.playing) { + // Started by touch so create a touch controller. + if (!this.touchController && e.type == Runner.events.TOUCHSTART) { + this.createTouchController(); + } + this.loadSounds(); + this.setPlayStatus(true); + this.update(); + if (window.errorPageController) { + errorPageController.trackEasterEgg(); + } + } + // Start jump. + if (!this.tRex.jumping && !this.tRex.ducking) { + this.playSound(this.soundFx.BUTTON_PRESS); + this.tRex.startJump(this.currentSpeed); + } + } else if (this.playing && Runner.keycodes.DUCK[e.keyCode]) { + e.preventDefault(); + if (this.tRex.jumping) { + // Speed drop, activated only when jump key is not pressed. + this.tRex.setSpeedDrop(); + } else if (!this.tRex.jumping && !this.tRex.ducking) { + // Duck. + this.tRex.setDuck(true); + } + } + } else if (this.crashed && e.type == Runner.events.TOUCHSTART && + e.currentTarget == this.containerEl) { + this.restart(); + } + } + }, + + + /** + * Process key up. + * @param {Event} e + */ + onKeyUp: function(e) { + var keyCode = String(e.keyCode); + var isjumpKey = Runner.keycodes.JUMP[keyCode] || + e.type == Runner.events.TOUCHEND || + e.type == Runner.events.POINTERUP; + + if (this.isRunning() && isjumpKey) { + this.tRex.endJump(); + } else if (Runner.keycodes.DUCK[keyCode]) { + this.tRex.speedDrop = false; + this.tRex.setDuck(false); + } else if (this.crashed) { + // Check that enough time has elapsed before allowing jump key to restart. + var deltaTime = getTimeStamp() - this.time; + + if (this.isCanvasInView() && + (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) || + (deltaTime >= this.config.GAMEOVER_CLEAR_TIME && + Runner.keycodes.JUMP[keyCode]))) { + this.restart(); + } + } else if (this.paused && isjumpKey) { + // Reset the jump state + this.tRex.reset(); + this.play(); + } + }, + + /** + * Returns whether the event was a left click on canvas. + * On Windows right click is registered as a click. + * @param {Event} e + * @return {boolean} + */ + isLeftClickOnCanvas: function(e) { + return e.button != null && e.button < 2 && + e.type == Runner.events.POINTERUP && e.target == this.canvas; + }, + + /** + * RequestAnimationFrame wrapper. + */ + scheduleNextUpdate: function() { + if (!this.updatePending) { + this.updatePending = true; + this.raqId = requestAnimationFrame(this.update.bind(this)); + } + }, + + /** + * Whether the game is running. + * @return {boolean} + */ + isRunning: function() { + return !!this.raqId; + }, + + /** + * Game over state. + */ + gameOver: function() { + this.playSound(this.soundFx.HIT); + vibrate(200); + + this.stop(); + this.crashed = true; + this.distanceMeter.achievement = false; + + this.tRex.update(100, Trex.status.CRASHED); + + // Game over panel. + if (!this.gameOverPanel) { + this.gameOverPanel = new GameOverPanel(this.canvas, + this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART, + this.dimensions); + } else { + this.gameOverPanel.draw(); + } + + // Update the high score. + if (this.distanceRan > this.highestScore) { + this.highestScore = Math.ceil(this.distanceRan); + this.distanceMeter.setHighScore(this.highestScore); + } + + // Reset the time clock. + this.time = getTimeStamp(); + }, + + stop: function() { + this.setPlayStatus(false); + this.paused = true; + cancelAnimationFrame(this.raqId); + this.raqId = 0; + }, + + play: function() { + if (!this.crashed) { + this.setPlayStatus(true); + this.paused = false; + this.tRex.update(0, Trex.status.RUNNING); + this.time = getTimeStamp(); + this.update(); + } + }, + + restart: function() { + if (!this.raqId) { + this.playCount++; + this.runningTime = 0; + this.setPlayStatus(true); + this.paused = false; + this.crashed = false; + this.distanceRan = 0; + this.setSpeed(this.config.SPEED); + this.time = getTimeStamp(); + this.containerEl.classList.remove(Runner.classes.CRASHED); + this.clearCanvas(); + this.distanceMeter.reset(this.highestScore); + this.horizon.reset(); + this.tRex.reset(); + this.playSound(this.soundFx.BUTTON_PRESS); + this.invert(true); + this.bdayFlashTimer = null; + this.update(); + } + }, + + setPlayStatus: function(isPlaying) { + if (this.touchController) + this.touchController.classList.toggle(HIDDEN_CLASS, !isPlaying); + this.playing = isPlaying; + }, + + /** + * Whether the game should go into arcade mode. + * @return {boolean} + */ + isArcadeMode: function() { + return document.title == ARCADE_MODE_URL; + }, + + /** + * Hides offline messaging for a fullscreen game only experience. + */ + setArcadeMode: function() { + document.body.classList.add(Runner.classes.ARCADE_MODE); + this.setArcadeModeContainerScale(); + }, + + /** + * Sets the scaling for arcade mode. + */ + setArcadeModeContainerScale: function() { + var windowHeight = window.innerHeight; + var scaleHeight = windowHeight / this.dimensions.HEIGHT; + var scaleWidth = window.innerWidth / this.dimensions.WIDTH; + var scale = Math.max(1, Math.min(scaleHeight, scaleWidth)); + var scaledCanvasHeight = this.dimensions.HEIGHT * scale; + // Positions the game container at 10% of the available vertical window + // height minus the game container height. + var translateY = Math.ceil(Math.max(0, (windowHeight - scaledCanvasHeight - + Runner.config.ARCADE_MODE_INITIAL_TOP_POSITION) * + Runner.config.ARCADE_MODE_TOP_POSITION_PERCENT)) * + window.devicePixelRatio; + this.containerEl.style.transform = 'scale(' + scale + ') translateY(' + + translateY + 'px)'; + }, + + /** + * Pause the game if the tab is not in focus. + */ + onVisibilityChange: function(e) { + if (document.hidden || document.webkitHidden || e.type == 'blur' || + document.visibilityState != 'visible') { + this.stop(); + } else if (!this.crashed) { + this.tRex.reset(); + this.play(); + } + }, + + /** + * Play a sound. + * @param {SoundBuffer} soundBuffer + */ + playSound: function(soundBuffer) { + if (soundBuffer) { + var sourceNode = this.audioContext.createBufferSource(); + sourceNode.buffer = soundBuffer; + sourceNode.connect(this.audioContext.destination); + sourceNode.start(0); + } + }, + + /** + * Inverts the current page / canvas colors. + * @param {boolean} Whether to reset colors. + */ + invert: function(reset) { + if (reset) { + document.body.classList.toggle(Runner.classes.INVERTED, false); + this.invertTimer = 0; + this.inverted = false; + } else { + this.inverted = document.body.classList.toggle(Runner.classes.INVERTED, + this.invertTrigger); + } + } +}; + + +/** + * Updates the canvas size taking into + * account the backing store pixel ratio and + * the device pixel ratio. + * + * See article by Paul Lewis: + * http://www.html5rocks.com/en/tutorials/canvas/hidpi/ + * + * @param {HTMLCanvasElement} canvas + * @param {number} opt_width + * @param {number} opt_height + * @return {boolean} Whether the canvas was scaled. + */ +Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) { + var context = canvas.getContext('2d'); + + // Query the various pixel ratios + var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1; + var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1; + var ratio = devicePixelRatio / backingStoreRatio; + + // Upscale the canvas if the two ratios don't match + if (devicePixelRatio !== backingStoreRatio) { + var oldWidth = opt_width || canvas.width; + var oldHeight = opt_height || canvas.height; + + canvas.width = oldWidth * ratio; + canvas.height = oldHeight * ratio; + + canvas.style.width = oldWidth + 'px'; + canvas.style.height = oldHeight + 'px'; + + // Scale the context to counter the fact that we've manually scaled + // our canvas element. + context.scale(ratio, ratio); + return true; + } else if (devicePixelRatio == 1) { + // Reset the canvas width / height. Fixes scaling bug when the page is + // zoomed and the devicePixelRatio changes accordingly. + canvas.style.width = canvas.width + 'px'; + canvas.style.height = canvas.height + 'px'; + } + return false; +}; + + +/** + * Get random number. + * @param {number} min + * @param {number} max + * @param {number} + */ +function getRandomNum(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + + +/** + * Vibrate on mobile devices. + * @param {number} duration Duration of the vibration in milliseconds. + */ +function vibrate(duration) { + if (IS_MOBILE && window.navigator.vibrate) { + window.navigator.vibrate(duration); + } +} + + +/** + * Create canvas element. + * @param {HTMLElement} container Element to append canvas to. + * @param {number} width + * @param {number} height + * @param {string} opt_classname + * @return {HTMLCanvasElement} + */ +function createCanvas(container, width, height, opt_classname) { + var canvas = document.createElement('canvas'); + canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' + + opt_classname : Runner.classes.CANVAS; + canvas.width = width; + canvas.height = height; + container.appendChild(canvas); + + return canvas; +} + + +/** + * Decodes the base 64 audio to ArrayBuffer used by Web Audio. + * @param {string} base64String + */ +function decodeBase64ToArrayBuffer(base64String) { + var len = (base64String.length / 4) * 3; + var str = atob(base64String); + var arrayBuffer = new ArrayBuffer(len); + var bytes = new Uint8Array(arrayBuffer); + + for (var i = 0; i < len; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes.buffer; +} + + +/** + * Return the current timestamp. + * @return {number} + */ +function getTimeStamp() { + return IS_IOS ? new Date().getTime() : performance.now(); +} + + +//****************************************************************************** + + +/** + * Game over panel. + * @param {!HTMLCanvasElement} canvas + * @param {Object} textImgPos + * @param {Object} restartImgPos + * @param {!Object} dimensions Canvas dimensions. + * @constructor + */ +function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) { + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.canvasDimensions = dimensions; + this.textImgPos = textImgPos; + this.restartImgPos = restartImgPos; + this.draw(); +}; + + +/** + * Dimensions used in the panel. + * @enum {number} + */ +GameOverPanel.dimensions = { + TEXT_X: 0, + TEXT_Y: 13, + TEXT_WIDTH: 191, + TEXT_HEIGHT: 11, + RESTART_WIDTH: 36, + RESTART_HEIGHT: 32 +}; + + +GameOverPanel.prototype = { + /** + * Update the panel dimensions. + * @param {number} width New canvas width. + * @param {number} opt_height Optional new canvas height. + */ + updateDimensions: function(width, opt_height) { + this.canvasDimensions.WIDTH = width; + if (opt_height) { + this.canvasDimensions.HEIGHT = opt_height; + } + }, + + /** + * Draw the panel. + */ + draw: function() { + var dimensions = GameOverPanel.dimensions; + + var centerX = this.canvasDimensions.WIDTH / 2; + + // Game over text. + var textSourceX = dimensions.TEXT_X; + var textSourceY = dimensions.TEXT_Y; + var textSourceWidth = dimensions.TEXT_WIDTH; + var textSourceHeight = dimensions.TEXT_HEIGHT; + + var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2)); + var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3); + var textTargetWidth = dimensions.TEXT_WIDTH; + var textTargetHeight = dimensions.TEXT_HEIGHT; + + var restartSourceWidth = dimensions.RESTART_WIDTH; + var restartSourceHeight = dimensions.RESTART_HEIGHT; + var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2); + var restartTargetY = this.canvasDimensions.HEIGHT / 2; + + if (IS_HIDPI) { + textSourceY *= 2; + textSourceX *= 2; + textSourceWidth *= 2; + textSourceHeight *= 2; + restartSourceWidth *= 2; + restartSourceHeight *= 2; + } + + textSourceX += this.textImgPos.x; + textSourceY += this.textImgPos.y; + + // Game over text from sprite. + this.canvasCtx.drawImage(Runner.imageSprite, + textSourceX, textSourceY, textSourceWidth, textSourceHeight, + textTargetX, textTargetY, textTargetWidth, textTargetHeight); + + // Restart button. + this.canvasCtx.drawImage(Runner.imageSprite, + this.restartImgPos.x, this.restartImgPos.y, + restartSourceWidth, restartSourceHeight, + restartTargetX, restartTargetY, dimensions.RESTART_WIDTH, + dimensions.RESTART_HEIGHT); + } +}; + + +//****************************************************************************** + +/** + * Check for a collision. + * @param {!Obstacle} obstacle + * @param {!Trex} tRex T-rex object. + * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing + * collision boxes. + * @return {Array} + */ +function checkForCollision(obstacle, tRex, opt_canvasCtx) { + var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos; + + // Adjustments are made to the bounding box as there is a 1 pixel white + // border around the t-rex and obstacles. + var tRexBox = new CollisionBox( + tRex.xPos + 1, + tRex.yPos + 1, + tRex.config.WIDTH - 2, + tRex.config.HEIGHT - 2); + + var obstacleBox = new CollisionBox( + obstacle.xPos + 1, + obstacle.yPos + 1, + obstacle.typeConfig.width * obstacle.size - 2, + obstacle.typeConfig.height - 2); + + // Debug outer box + if (opt_canvasCtx) { + drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox); + } + + // Simple outer bounds check. + if (boxCompare(tRexBox, obstacleBox)) { + var collisionBoxes = obstacle.collisionBoxes; + var tRexCollisionBoxes = tRex.ducking ? + Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING; + + // Detailed axis aligned box check. + for (var t = 0; t < tRexCollisionBoxes.length; t++) { + for (var i = 0; i < collisionBoxes.length; i++) { + // Adjust the box to actual positions. + var adjTrexBox = + createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox); + var adjObstacleBox = + createAdjustedCollisionBox(collisionBoxes[i], obstacleBox); + var crashed = boxCompare(adjTrexBox, adjObstacleBox); + + // Draw boxes for debug. + if (opt_canvasCtx) { + drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox); + } + + if (crashed) { + return [adjTrexBox, adjObstacleBox]; + } + } + } + } + return false; +}; + + +/** + * Adjust the collision box. + * @param {!CollisionBox} box The original box. + * @param {!CollisionBox} adjustment Adjustment box. + * @return {CollisionBox} The adjusted collision box object. + */ +function createAdjustedCollisionBox(box, adjustment) { + return new CollisionBox( + box.x + adjustment.x, + box.y + adjustment.y, + box.width, + box.height); +}; + + +/** + * Draw the collision boxes for debug. + */ +function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) { + canvasCtx.save(); + canvasCtx.strokeStyle = '#f00'; + canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height); + + canvasCtx.strokeStyle = '#0f0'; + canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y, + obstacleBox.width, obstacleBox.height); + canvasCtx.restore(); +}; + + +/** + * Compare two collision boxes for a collision. + * @param {CollisionBox} tRexBox + * @param {CollisionBox} obstacleBox + * @return {boolean} Whether the boxes intersected. + */ +function boxCompare(tRexBox, obstacleBox) { + var crashed = false; + var tRexBoxX = tRexBox.x; + var tRexBoxY = tRexBox.y; + + var obstacleBoxX = obstacleBox.x; + var obstacleBoxY = obstacleBox.y; + + // Axis-Aligned Bounding Box method. + if (tRexBox.x < obstacleBoxX + obstacleBox.width && + tRexBox.x + tRexBox.width > obstacleBoxX && + tRexBox.y < obstacleBox.y + obstacleBox.height && + tRexBox.height + tRexBox.y > obstacleBox.y) { + crashed = true; + } + + return crashed; +}; + + +//****************************************************************************** + +/** + * Collision box object. + * @param {number} x X position. + * @param {number} y Y Position. + * @param {number} w Width. + * @param {number} h Height. + */ +function CollisionBox(x, y, w, h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; +}; + + +//****************************************************************************** + +/** + * Obstacle. + * @param {HTMLCanvasCtx} canvasCtx + * @param {Obstacle.type} type + * @param {Object} spritePos Obstacle position in sprite. + * @param {Object} dimensions + * @param {number} gapCoefficient Mutipler in determining the gap. + * @param {number} speed + * @param {number} opt_xOffset + */ +function Obstacle(canvasCtx, type, spriteImgPos, dimensions, + gapCoefficient, speed, opt_xOffset) { + + this.canvasCtx = canvasCtx; + this.spritePos = spriteImgPos; + this.typeConfig = type; + this.gapCoefficient = gapCoefficient; + this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH); + this.dimensions = dimensions; + this.remove = false; + this.xPos = dimensions.WIDTH + (opt_xOffset || 0); + this.yPos = 0; + this.width = 0; + this.collisionBoxes = []; + this.gap = 0; + this.speedOffset = 0; + + // For animated obstacles. + this.currentFrame = 0; + this.timer = 0; + + this.init(speed); +}; + +/** + * Coefficient for calculating the maximum gap. + * @const + */ +Obstacle.MAX_GAP_COEFFICIENT = 1.5; + +/** + * Maximum obstacle grouping count. + * @const + */ +Obstacle.MAX_OBSTACLE_LENGTH = 3, + + +Obstacle.prototype = { + /** + * Initialise the DOM for the obstacle. + * @param {number} speed + */ + init: function(speed) { + this.cloneCollisionBoxes(); + + // Only allow sizing if we're at the right speed. + if (this.size > 1 && this.typeConfig.multipleSpeed > speed) { + this.size = 1; + } + + this.width = this.typeConfig.width * this.size; + + // Check if obstacle can be positioned at various heights. + if (Array.isArray(this.typeConfig.yPos)) { + var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile : + this.typeConfig.yPos; + this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)]; + } else { + this.yPos = this.typeConfig.yPos; + } + + this.draw(); + + // Make collision box adjustments, + // Central box is adjusted to the size as one box. + // ____ ______ ________ + // _| |-| _| |-| _| |-| + // | |<->| | | |<--->| | | |<----->| | + // | | 1 | | | | 2 | | | | 3 | | + // |_|___|_| |_|_____|_| |_|_______|_| + // + if (this.size > 1) { + this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width - + this.collisionBoxes[2].width; + this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width; + } + + // For obstacles that go at a different speed from the horizon. + if (this.typeConfig.speedOffset) { + this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset : + -this.typeConfig.speedOffset; + } + + this.gap = this.getGap(this.gapCoefficient, speed); + }, + + /** + * Draw and crop based on size. + */ + draw: function() { + var sourceWidth = this.typeConfig.width; + var sourceHeight = this.typeConfig.height; + + if (IS_HIDPI) { + sourceWidth = sourceWidth * 2; + sourceHeight = sourceHeight * 2; + } + + // X position in sprite. + var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) + + this.spritePos.x; + + // Animation frames. + if (this.currentFrame > 0) { + sourceX += sourceWidth * this.currentFrame; + } + + this.canvasCtx.drawImage(Runner.imageSprite, + sourceX, this.spritePos.y, + sourceWidth * this.size, sourceHeight, + this.xPos, this.yPos, + this.typeConfig.width * this.size, this.typeConfig.height); + }, + + /** + * Obstacle frame update. + * @param {number} deltaTime + * @param {number} speed + */ + update: function(deltaTime, speed) { + if (!this.remove) { + if (this.typeConfig.speedOffset) { + speed += this.speedOffset; + } + this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime); + + // Update frame + if (this.typeConfig.numFrames) { + this.timer += deltaTime; + if (this.timer >= this.typeConfig.frameRate) { + this.currentFrame = + this.currentFrame == this.typeConfig.numFrames - 1 ? + 0 : this.currentFrame + 1; + this.timer = 0; + } + } + this.draw(); + + if (!this.isVisible()) { + this.remove = true; + } + } + }, + + /** + * Calculate a random gap size. + * - Minimum gap gets wider as speed increses + * @param {number} gapCoefficient + * @param {number} speed + * @return {number} The gap size. + */ + getGap: function(gapCoefficient, speed) { + var minGap = Math.round(this.width * speed + + this.typeConfig.minGap * gapCoefficient); + var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT); + return getRandomNum(minGap, maxGap); + }, + + /** + * Check if obstacle is visible. + * @return {boolean} Whether the obstacle is in the game area. + */ + isVisible: function() { + return this.xPos + this.width > 0; + }, + + /** + * Make a copy of the collision boxes, since these will change based on + * obstacle type and size. + */ + cloneCollisionBoxes: function() { + var collisionBoxes = this.typeConfig.collisionBoxes; + + for (var i = collisionBoxes.length - 1; i >= 0; i--) { + this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x, + collisionBoxes[i].y, collisionBoxes[i].width, + collisionBoxes[i].height); + } + } +}; + + +/** + * Obstacle definitions. + * minGap: minimum pixel space betweeen obstacles. + * multipleSpeed: Speed at which multiples are allowed. + * speedOffset: speed faster / slower than the horizon. + * minSpeed: Minimum speed which the obstacle can make an appearance. + */ +Obstacle.types = [ + { + type: 'CACTUS_SMALL', + width: 17, + height: 35, + yPos: 105, + multipleSpeed: 4, + minGap: 120, + minSpeed: 0, + collisionBoxes: [ + new CollisionBox(0, 7, 5, 27), + new CollisionBox(4, 0, 6, 34), + new CollisionBox(10, 4, 7, 14) + ] + }, + { + type: 'CACTUS_LARGE', + width: 25, + height: 50, + yPos: 90, + multipleSpeed: 7, + minGap: 120, + minSpeed: 0, + collisionBoxes: [ + new CollisionBox(0, 12, 7, 38), + new CollisionBox(8, 0, 7, 49), + new CollisionBox(13, 10, 10, 38) + ] + }, + { + type: 'PTERODACTYL', + width: 46, + height: 40, + yPos: [ 100, 75, 50 ], // Variable height. + yPosMobile: [ 100, 50 ], // Variable height mobile. + multipleSpeed: 999, + minSpeed: 8.5, + minGap: 150, + collisionBoxes: [ + new CollisionBox(15, 15, 16, 5), + new CollisionBox(18, 21, 24, 6), + new CollisionBox(2, 14, 4, 3), + new CollisionBox(6, 10, 4, 7), + new CollisionBox(10, 8, 6, 9) + ], + numFrames: 2, + frameRate: 1000/6, + speedOffset: .8 + } +]; + + +//****************************************************************************** +/** + * T-rex game character. + * @param {HTMLCanvas} canvas + * @param {Object} spritePos Positioning within image sprite. + * @constructor + */ +function Trex(canvas, spritePos) { + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.spritePos = spritePos; + this.xPos = 0; + this.yPos = 0; + // Position when on the ground. + this.groundYPos = 0; + this.currentFrame = 0; + this.currentAnimFrames = []; + this.blinkDelay = 0; + this.blinkCount = 0; + this.animStartTime = 0; + this.timer = 0; + this.msPerFrame = 1000 / FPS; + this.config = Trex.config; + // Current status. + this.status = Trex.status.WAITING; + + this.jumping = false; + this.ducking = false; + this.jumpVelocity = 0; + this.reachedMinHeight = false; + this.speedDrop = false; + this.jumpCount = 0; + this.jumpspotX = 0; + + this.init(); +}; + + +/** + * T-rex player config. + * @enum {number} + */ +Trex.config = { + DROP_VELOCITY: -5, + GRAVITY: 0.6, + HEIGHT: 47, + HEIGHT_DUCK: 25, + INIITAL_JUMP_VELOCITY: -10, + INTRO_DURATION: 1500, + MAX_JUMP_HEIGHT: 30, + MIN_JUMP_HEIGHT: 30, + SPEED_DROP_COEFFICIENT: 3, + SPRITE_WIDTH: 262, + START_X_POS: 50, + WIDTH: 44, + WIDTH_DUCK: 59 +}; + + +/** + * Used in collision detection. + * @type {Array} + */ +Trex.collisionBoxes = { + DUCKING: [ + new CollisionBox(1, 18, 55, 25) + ], + RUNNING: [ + new CollisionBox(22, 0, 17, 16), + new CollisionBox(1, 18, 30, 9), + new CollisionBox(10, 35, 14, 8), + new CollisionBox(1, 24, 29, 5), + new CollisionBox(5, 30, 21, 4), + new CollisionBox(9, 34, 15, 4) + ] +}; + + +/** + * Animation states. + * @enum {string} + */ +Trex.status = { + CRASHED: 'CRASHED', + DUCKING: 'DUCKING', + JUMPING: 'JUMPING', + RUNNING: 'RUNNING', + WAITING: 'WAITING' +}; + +/** + * Blinking coefficient. + * @const + */ +Trex.BLINK_TIMING = 7000; + + +/** + * Animation config for different states. + * @enum {Object} + */ +Trex.animFrames = { + WAITING: { + frames: [44, 0], + msPerFrame: 1000 / 3 + }, + RUNNING: { + frames: [88, 132], + msPerFrame: 1000 / 12 + }, + CRASHED: { + frames: [220], + msPerFrame: 1000 / 60 + }, + JUMPING: { + frames: [0], + msPerFrame: 1000 / 60 + }, + DUCKING: { + frames: [264, 323], + msPerFrame: 1000 / 8 + } +}; + + +Trex.prototype = { + /** + * T-rex player initaliser. + * Sets the t-rex to blink at random intervals. + */ + init: function() { + this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT - + Runner.config.BOTTOM_PAD; + this.yPos = this.groundYPos; + this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; + + this.draw(0, 0); + this.update(0, Trex.status.WAITING); + }, + + /** + * Setter for the jump velocity. + * The approriate drop velocity is also set. + */ + setJumpVelocity: function(setting) { + this.config.INIITAL_JUMP_VELOCITY = -setting; + this.config.DROP_VELOCITY = -setting / 2; + }, + + /** + * Set the animation status. + * @param {!number} deltaTime + * @param {Trex.status} status Optional status to switch to. + */ + update: function(deltaTime, opt_status) { + this.timer += deltaTime; + + // Update the status. + if (opt_status) { + this.status = opt_status; + this.currentFrame = 0; + this.msPerFrame = Trex.animFrames[opt_status].msPerFrame; + this.currentAnimFrames = Trex.animFrames[opt_status].frames; + + if (opt_status == Trex.status.WAITING) { + this.animStartTime = getTimeStamp(); + this.setBlinkDelay(); + } + } + + // Game intro animation, T-rex moves in from the left. + if (this.playingIntro && this.xPos < this.config.START_X_POS) { + this.xPos += Math.round((this.config.START_X_POS / + this.config.INTRO_DURATION) * deltaTime); + } + + if (this.status == Trex.status.WAITING) { + this.blink(getTimeStamp()); + } else { + this.draw(this.currentAnimFrames[this.currentFrame], 0); + } + + // Update the frame position. + if (this.timer >= this.msPerFrame) { + this.currentFrame = this.currentFrame == + this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1; + this.timer = 0; + } + + // Speed drop becomes duck if the down key is still being pressed. + if (this.speedDrop && this.yPos == this.groundYPos) { + this.speedDrop = false; + this.setDuck(true); + } + }, + + /** + * Draw the t-rex to a particular position. + * @param {number} x + * @param {number} y + */ + draw: function(x, y) { + var sourceX = x; + var sourceY = y; + var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ? + this.config.WIDTH_DUCK : this.config.WIDTH; + var sourceHeight = this.config.HEIGHT; + var outputHeight = sourceHeight; + + if (IS_HIDPI) { + sourceX *= 2; + sourceY *= 2; + sourceWidth *= 2; + sourceHeight *= 2; + } + + // Adjustments for sprite sheet position. + sourceX += this.spritePos.x; + sourceY += this.spritePos.y; + + // Ducking. + if (this.ducking && this.status != Trex.status.CRASHED) { + this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY, + sourceWidth, sourceHeight, + this.xPos, this.yPos, + this.config.WIDTH_DUCK, outputHeight); + } else { + // Crashed whilst ducking. Trex is standing up so needs adjustment. + if (this.ducking && this.status == Trex.status.CRASHED) { + this.xPos++; + } + // Standing / running + this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY, + sourceWidth, sourceHeight, + this.xPos, this.yPos, + this.config.WIDTH, outputHeight); + } + this.canvasCtx.globalAlpha = 1; + }, + + /** + * Sets a random time for the blink to happen. + */ + setBlinkDelay: function() { + this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING); + }, + + /** + * Make t-rex blink at random intervals. + * @param {number} time Current time in milliseconds. + */ + blink: function(time) { + var deltaTime = time - this.animStartTime; + + if (deltaTime >= this.blinkDelay) { + this.draw(this.currentAnimFrames[this.currentFrame], 0); + + if (this.currentFrame == 1) { + // Set new random delay to blink. + this.setBlinkDelay(); + this.animStartTime = time; + this.blinkCount++; + } + } + }, + + /** + * Initialise a jump. + * @param {number} speed + */ + startJump: function(speed) { + if (!this.jumping) { + this.update(0, Trex.status.JUMPING); + // Tweak the jump velocity based on the speed. + this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10); + this.jumping = true; + this.reachedMinHeight = false; + this.speedDrop = false; + } + }, + + /** + * Jump is complete, falling down. + */ + endJump: function() { + if (this.reachedMinHeight && + this.jumpVelocity < this.config.DROP_VELOCITY) { + this.jumpVelocity = this.config.DROP_VELOCITY; + } + }, + + /** + * Update frame for a jump. + * @param {number} deltaTime + * @param {number} speed + */ + updateJump: function(deltaTime, speed) { + var msPerFrame = Trex.animFrames[this.status].msPerFrame; + var framesElapsed = deltaTime / msPerFrame; + + // Speed drop makes Trex fall faster. + if (this.speedDrop) { + this.yPos += Math.round(this.jumpVelocity * + this.config.SPEED_DROP_COEFFICIENT * framesElapsed); + } else { + this.yPos += Math.round(this.jumpVelocity * framesElapsed); + } + + this.jumpVelocity += this.config.GRAVITY * framesElapsed; + + // Minimum height has been reached. + if (this.yPos < this.minJumpHeight || this.speedDrop) { + this.reachedMinHeight = true; + } + + // Reached max height + if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) { + this.endJump(); + } + + // Back down at ground level. Jump completed. + if (this.yPos > this.groundYPos) { + this.reset(); + this.jumpCount++; + } + }, + + /** + * Set the speed drop. Immediately cancels the current jump. + */ + setSpeedDrop: function() { + this.speedDrop = true; + this.jumpVelocity = 1; + }, + + /** + * @param {boolean} isDucking. + */ + setDuck: function(isDucking) { + if (isDucking && this.status != Trex.status.DUCKING) { + this.update(0, Trex.status.DUCKING); + this.ducking = true; + } else if (this.status == Trex.status.DUCKING) { + this.update(0, Trex.status.RUNNING); + this.ducking = false; + } + }, + + /** + * Reset the t-rex to running at start of game. + */ + reset: function() { + this.yPos = this.groundYPos; + this.jumpVelocity = 0; + this.jumping = false; + this.ducking = false; + this.update(0, Trex.status.RUNNING); + this.midair = false; + this.speedDrop = false; + this.jumpCount = 0; + } +}; + + +//****************************************************************************** + +/** + * Handles displaying the distance meter. + * @param {!HTMLCanvasElement} canvas + * @param {Object} spritePos Image position in sprite. + * @param {number} canvasWidth + * @constructor + */ +function DistanceMeter(canvas, spritePos, canvasWidth) { + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.image = Runner.imageSprite; + this.spritePos = spritePos; + this.x = 0; + this.y = 5; + + this.currentDistance = 0; + this.maxScore = 0; + this.highScore = 0; + this.container = null; + + this.digits = []; + this.achievement = false; + this.defaultString = ''; + this.flashTimer = 0; + this.flashIterations = 0; + this.invertTrigger = false; + + this.config = DistanceMeter.config; + this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS; + this.init(canvasWidth); +}; + + +/** + * @enum {number} + */ +DistanceMeter.dimensions = { + WIDTH: 10, + HEIGHT: 13, + DEST_WIDTH: 11 +}; + + +/** + * Y positioning of the digits in the sprite sheet. + * X position is always 0. + * @type {Array} + */ +DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120]; + + +/** + * Distance meter config. + * @enum {number} + */ +DistanceMeter.config = { + // Number of digits. + MAX_DISTANCE_UNITS: 5, + + // Distance that causes achievement animation. + ACHIEVEMENT_DISTANCE: 100, + + // Used for conversion from pixel distance to a scaled unit. + COEFFICIENT: 0.025, + + // Flash duration in milliseconds. + FLASH_DURATION: 1000 / 4, + + // Flash iterations for achievement animation. + FLASH_ITERATIONS: 3 +}; + + +DistanceMeter.prototype = { + /** + * Initialise the distance meter to '00000'. + * @param {number} width Canvas width in px. + */ + init: function(width) { + var maxDistanceStr = ''; + + this.calcXPos(width); + this.maxScore = this.maxScoreUnits; + for (var i = 0; i < this.maxScoreUnits; i++) { + this.draw(i, 0); + this.defaultString += '0'; + maxDistanceStr += '9'; + } + + this.maxScore = parseInt(maxDistanceStr); + }, + + /** + * Calculate the xPos in the canvas. + * @param {number} canvasWidth + */ + calcXPos: function(canvasWidth) { + this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH * + (this.maxScoreUnits + 1)); + }, + + /** + * Draw a digit to canvas. + * @param {number} digitPos Position of the digit. + * @param {number} value Digit value 0-9. + * @param {boolean} opt_highScore Whether drawing the high score. + */ + draw: function(digitPos, value, opt_highScore) { + var sourceWidth = DistanceMeter.dimensions.WIDTH; + var sourceHeight = DistanceMeter.dimensions.HEIGHT; + var sourceX = DistanceMeter.dimensions.WIDTH * value; + var sourceY = 0; + + var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH; + var targetY = this.y; + var targetWidth = DistanceMeter.dimensions.WIDTH; + var targetHeight = DistanceMeter.dimensions.HEIGHT; + + // For high DPI we 2x source values. + if (IS_HIDPI) { + sourceWidth *= 2; + sourceHeight *= 2; + sourceX *= 2; + } + + sourceX += this.spritePos.x; + sourceY += this.spritePos.y; + + this.canvasCtx.save(); + + if (opt_highScore) { + // Left of the current score. + var highScoreX = this.x - (this.maxScoreUnits * 2) * + DistanceMeter.dimensions.WIDTH; + this.canvasCtx.translate(highScoreX, this.y); + } else { + this.canvasCtx.translate(this.x, this.y); + } + + this.canvasCtx.drawImage(this.image, sourceX, sourceY, + sourceWidth, sourceHeight, + targetX, targetY, + targetWidth, targetHeight + ); + + this.canvasCtx.restore(); + }, + + /** + * Covert pixel distance to a 'real' distance. + * @param {number} distance Pixel distance ran. + * @return {number} The 'real' distance ran. + */ + getActualDistance: function(distance) { + return distance ? Math.round(distance * this.config.COEFFICIENT) : 0; + }, + + /** + * Update the distance meter. + * @param {number} distance + * @param {number} deltaTime + * @return {boolean} Whether the acheivement sound fx should be played. + */ + update: function(deltaTime, distance) { + var paint = true; + var playSound = false; + + if (!this.achievement) { + distance = this.getActualDistance(distance); + // Score has gone beyond the initial digit count. + if (distance > this.maxScore && this.maxScoreUnits == + this.config.MAX_DISTANCE_UNITS) { + this.maxScoreUnits++; + this.maxScore = parseInt(this.maxScore + '9'); + } else { + this.distance = 0; + } + + if (distance > 0) { + // Acheivement unlocked + if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) { + // Flash score and play sound. + this.achievement = true; + this.flashTimer = 0; + playSound = true; + } + + // Create a string representation of the distance with leading 0. + var distanceStr = (this.defaultString + + distance).substr(-this.maxScoreUnits); + this.digits = distanceStr.split(''); + } else { + this.digits = this.defaultString.split(''); + } + } else { + // Control flashing of the score on reaching acheivement. + if (this.flashIterations <= this.config.FLASH_ITERATIONS) { + this.flashTimer += deltaTime; + + if (this.flashTimer < this.config.FLASH_DURATION) { + paint = false; + } else if (this.flashTimer > + this.config.FLASH_DURATION * 2) { + this.flashTimer = 0; + this.flashIterations++; + } + } else { + this.achievement = false; + this.flashIterations = 0; + this.flashTimer = 0; + } + } + + // Draw the digits if not flashing. + if (paint) { + for (var i = this.digits.length - 1; i >= 0; i--) { + this.draw(i, parseInt(this.digits[i])); + } + } + + this.drawHighScore(); + return playSound; + }, + + /** + * Draw the high score. + */ + drawHighScore: function() { + this.canvasCtx.save(); + this.canvasCtx.globalAlpha = .8; + for (var i = this.highScore.length - 1; i >= 0; i--) { + this.draw(i, parseInt(this.highScore[i], 10), true); + } + this.canvasCtx.restore(); + }, + + /** + * Set the highscore as a array string. + * Position of char in the sprite: H - 10, I - 11. + * @param {number} distance Distance ran in pixels. + */ + setHighScore: function(distance) { + distance = this.getActualDistance(distance); + var highScoreStr = (this.defaultString + + distance).substr(-this.maxScoreUnits); + + this.highScore = ['10', '11', ''].concat(highScoreStr.split('')); + }, + + /** + * Reset the distance meter back to '00000'. + */ + reset: function() { + this.update(0); + this.achievement = false; + } +}; + + +//****************************************************************************** + +/** + * Cloud background item. + * Similar to an obstacle object but without collision boxes. + * @param {HTMLCanvasElement} canvas Canvas element. + * @param {Object} spritePos Position of image in sprite. + * @param {number} containerWidth + */ +function Cloud(canvas, spritePos, containerWidth) { + this.canvas = canvas; + this.canvasCtx = this.canvas.getContext('2d'); + this.spritePos = spritePos; + this.containerWidth = containerWidth; + this.xPos = containerWidth; + this.yPos = 0; + this.remove = false; + this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP, + Cloud.config.MAX_CLOUD_GAP); + + this.init(); +}; + + +/** + * Cloud object config. + * @enum {number} + */ +Cloud.config = { + HEIGHT: 14, + MAX_CLOUD_GAP: 400, + MAX_SKY_LEVEL: 30, + MIN_CLOUD_GAP: 100, + MIN_SKY_LEVEL: 71, + WIDTH: 46 +}; + + +Cloud.prototype = { + /** + * Initialise the cloud. Sets the Cloud height. + */ + init: function() { + this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL, + Cloud.config.MIN_SKY_LEVEL); + this.draw(); + }, + + /** + * Draw the cloud. + */ + draw: function() { + this.canvasCtx.save(); + var sourceWidth = Cloud.config.WIDTH; + var sourceHeight = Cloud.config.HEIGHT; + var outputWidth = sourceWidth; + var outputHeight = sourceHeight; + if (IS_HIDPI) { + sourceWidth = sourceWidth * 2; + sourceHeight = sourceHeight * 2; + } + + this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x, + this.spritePos.y, + sourceWidth, sourceHeight, + this.xPos, this.yPos, + outputWidth, outputHeight); + + this.canvasCtx.restore(); + }, + + /** + * Update the cloud position. + * @param {number} speed + */ + update: function(speed) { + if (!this.remove) { + this.xPos -= Math.ceil(speed); + this.draw(); + + // Mark as removeable if no longer in the canvas. + if (!this.isVisible()) { + this.remove = true; + } + } + }, + + /** + * Check if the cloud is visible on the stage. + * @return {boolean} + */ + isVisible: function() { + return this.xPos + Cloud.config.WIDTH > 0; + } +}; + + +//****************************************************************************** + +/** + * Nightmode shows a moon and stars on the horizon. + */ +function NightMode(canvas, spritePos, containerWidth) { + this.spritePos = spritePos; + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.xPos = containerWidth - 50; + this.yPos = 30; + this.currentPhase = 0; + this.opacity = 0; + this.containerWidth = containerWidth; + this.stars = []; + this.drawStars = false; + this.placeStars(); +}; + +/** + * @enum {number} + */ +NightMode.config = { + FADE_SPEED: 0.035, + HEIGHT: 40, + MOON_SPEED: 0.25, + NUM_STARS: 2, + STAR_SIZE: 9, + STAR_SPEED: 0.3, + STAR_MAX_Y: 70, + WIDTH: 20 +}; + +NightMode.phases = [140, 120, 100, 60, 40, 20, 0]; + +NightMode.prototype = { + /** + * Update moving moon, changing phases. + * @param {boolean} activated Whether night mode is activated. + * @param {number} delta + */ + update: function(activated, delta) { + // Moon phase. + if (activated && this.opacity == 0) { + this.currentPhase++; + + if (this.currentPhase >= NightMode.phases.length) { + this.currentPhase = 0; + } + } + + // Fade in / out. + if (activated && (this.opacity < 1 || this.opacity == 0)) { + this.opacity += NightMode.config.FADE_SPEED; + } else if (this.opacity > 0) { + this.opacity -= NightMode.config.FADE_SPEED; + } + + // Set moon positioning. + if (this.opacity > 0) { + this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED); + + // Update stars. + if (this.drawStars) { + for (var i = 0; i < NightMode.config.NUM_STARS; i++) { + this.stars[i].x = this.updateXPos(this.stars[i].x, + NightMode.config.STAR_SPEED); + } + } + this.draw(); + } else { + this.opacity = 0; + this.placeStars(); + } + this.drawStars = true; + }, + + updateXPos: function(currentPos, speed) { + if (currentPos < -NightMode.config.WIDTH) { + currentPos = this.containerWidth; + } else { + currentPos -= speed; + } + return currentPos; + }, + + draw: function() { + var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 : + NightMode.config.WIDTH; + var moonSourceHeight = NightMode.config.HEIGHT; + var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase]; + var moonOutputWidth = moonSourceWidth; + var starSize = NightMode.config.STAR_SIZE; + var starSourceX = Runner.spriteDefinition.LDPI.STAR.x; + + if (IS_HIDPI) { + moonSourceWidth *= 2; + moonSourceHeight *= 2; + moonSourceX = this.spritePos.x + + (NightMode.phases[this.currentPhase] * 2); + starSize *= 2; + starSourceX = Runner.spriteDefinition.HDPI.STAR.x; + } + + this.canvasCtx.save(); + this.canvasCtx.globalAlpha = this.opacity; + + // Stars. + if (this.drawStars) { + for (var i = 0; i < NightMode.config.NUM_STARS; i++) { + this.canvasCtx.drawImage(Runner.imageSprite, + starSourceX, this.stars[i].sourceY, starSize, starSize, + Math.round(this.stars[i].x), this.stars[i].y, + NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE); + } + } + + // Moon. + this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX, + this.spritePos.y, moonSourceWidth, moonSourceHeight, + Math.round(this.xPos), this.yPos, + moonOutputWidth, NightMode.config.HEIGHT); + + this.canvasCtx.globalAlpha = 1; + this.canvasCtx.restore(); + }, + + // Do star placement. + placeStars: function() { + var segmentSize = Math.round(this.containerWidth / + NightMode.config.NUM_STARS); + + for (var i = 0; i < NightMode.config.NUM_STARS; i++) { + this.stars[i] = {}; + this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1)); + this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y); + + if (IS_HIDPI) { + this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y + + NightMode.config.STAR_SIZE * 2 * i; + } else { + this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y + + NightMode.config.STAR_SIZE * i; + } + } + }, + + reset: function() { + this.currentPhase = 0; + this.opacity = 0; + this.update(false); + } + +}; + + +//****************************************************************************** + +/** + * Horizon Line. + * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon. + * @param {HTMLCanvasElement} canvas + * @param {Object} spritePos Horizon position in sprite. + * @constructor + */ +function HorizonLine(canvas, spritePos) { + this.spritePos = spritePos; + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.sourceDimensions = {}; + this.dimensions = HorizonLine.dimensions; + this.sourceXPos = [this.spritePos.x, this.spritePos.x + + this.dimensions.WIDTH]; + this.xPos = []; + this.yPos = 0; + this.bumpThreshold = 0.5; + + this.setSourceDimensions(); + this.draw(); +}; + + +/** + * Horizon line dimensions. + * @enum {number} + */ +HorizonLine.dimensions = { + WIDTH: 600, + HEIGHT: 12, + YPOS: 127 +}; + + +HorizonLine.prototype = { + /** + * Set the source dimensions of the horizon line. + */ + setSourceDimensions: function() { + + for (var dimension in HorizonLine.dimensions) { + if (IS_HIDPI) { + if (dimension != 'YPOS') { + this.sourceDimensions[dimension] = + HorizonLine.dimensions[dimension] * 2; + } + } else { + this.sourceDimensions[dimension] = + HorizonLine.dimensions[dimension]; + } + this.dimensions[dimension] = HorizonLine.dimensions[dimension]; + } + + this.xPos = [0, HorizonLine.dimensions.WIDTH]; + this.yPos = HorizonLine.dimensions.YPOS; + }, + + /** + * Return the crop x position of a type. + */ + getRandomType: function() { + return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0; + }, + + /** + * Draw the horizon line. + */ + draw: function() { + this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0], + this.spritePos.y, + this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT, + this.xPos[0], this.yPos, + this.dimensions.WIDTH, this.dimensions.HEIGHT); + + this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1], + this.spritePos.y, + this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT, + this.xPos[1], this.yPos, + this.dimensions.WIDTH, this.dimensions.HEIGHT); + }, + + /** + * Update the x position of an indivdual piece of the line. + * @param {number} pos Line position. + * @param {number} increment + */ + updateXPos: function(pos, increment) { + var line1 = pos; + var line2 = pos == 0 ? 1 : 0; + + this.xPos[line1] -= increment; + this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH; + + if (this.xPos[line1] <= -this.dimensions.WIDTH) { + this.xPos[line1] += this.dimensions.WIDTH * 2; + this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH; + this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x; + } + }, + + /** + * Update the horizon line. + * @param {number} deltaTime + * @param {number} speed + */ + update: function(deltaTime, speed) { + var increment = Math.floor(speed * (FPS / 1000) * deltaTime); + + if (this.xPos[0] <= 0) { + this.updateXPos(0, increment); + } else { + this.updateXPos(1, increment); + } + this.draw(); + }, + + /** + * Reset horizon to the starting position. + */ + reset: function() { + this.xPos[0] = 0; + this.xPos[1] = HorizonLine.dimensions.WIDTH; + } +}; + + +//****************************************************************************** + +/** + * Horizon background class. + * @param {HTMLCanvasElement} canvas + * @param {Object} spritePos Sprite positioning. + * @param {Object} dimensions Canvas dimensions. + * @param {number} gapCoefficient + * @constructor + */ +function Horizon(canvas, spritePos, dimensions, gapCoefficient) { + this.canvas = canvas; + this.canvasCtx = this.canvas.getContext('2d'); + this.config = Horizon.config; + this.dimensions = dimensions; + this.gapCoefficient = gapCoefficient; + this.obstacles = []; + this.obstacleHistory = []; + this.horizonOffsets = [0, 0]; + this.cloudFrequency = this.config.CLOUD_FREQUENCY; + this.spritePos = spritePos; + this.nightMode = null; + + // Cloud + this.clouds = []; + this.cloudSpeed = this.config.BG_CLOUD_SPEED; + + // Horizon + this.horizonLine = null; + this.init(); +}; + + +/** + * Horizon config. + * @enum {number} + */ +Horizon.config = { + BG_CLOUD_SPEED: 0.2, + BUMPY_THRESHOLD: .3, + CLOUD_FREQUENCY: .5, + HORIZON_HEIGHT: 16, + MAX_CLOUDS: 6 +}; + + +Horizon.prototype = { + /** + * Initialise the horizon. Just add the line and a cloud. No obstacles. + */ + init: function() { + this.addCloud(); + this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON); + this.nightMode = new NightMode(this.canvas, this.spritePos.MOON, + this.dimensions.WIDTH); + }, + + /** + * @param {number} deltaTime + * @param {number} currentSpeed + * @param {boolean} updateObstacles Used as an override to prevent + * the obstacles from being updated / added. This happens in the + * ease in section. + * @param {boolean} showNightMode Night mode activated. + */ + update: function(deltaTime, currentSpeed, updateObstacles, showNightMode) { + this.runningTime += deltaTime; + this.horizonLine.update(deltaTime, currentSpeed); + this.nightMode.update(showNightMode); + this.updateClouds(deltaTime, currentSpeed); + + if (updateObstacles) { + this.updateObstacles(deltaTime, currentSpeed); + } + }, + + /** + * Update the cloud positions. + * @param {number} deltaTime + * @param {number} currentSpeed + */ + updateClouds: function(deltaTime, speed) { + var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed; + var numClouds = this.clouds.length; + + if (numClouds) { + for (var i = numClouds - 1; i >= 0; i--) { + this.clouds[i].update(cloudSpeed); + } + + var lastCloud = this.clouds[numClouds - 1]; + + // Check for adding a new cloud. + if (numClouds < this.config.MAX_CLOUDS && + (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap && + this.cloudFrequency > Math.random()) { + this.addCloud(); + } + + // Remove expired clouds. + this.clouds = this.clouds.filter(function(obj) { + return !obj.remove; + }); + } else { + this.addCloud(); + } + }, + + /** + * Update the obstacle positions. + * @param {number} deltaTime + * @param {number} currentSpeed + */ + updateObstacles: function(deltaTime, currentSpeed) { + // Obstacles, move to Horizon layer. + var updatedObstacles = this.obstacles.slice(0); + + for (var i = 0; i < this.obstacles.length; i++) { + var obstacle = this.obstacles[i]; + obstacle.update(deltaTime, currentSpeed); + + // Clean up existing obstacles. + if (obstacle.remove) { + updatedObstacles.shift(); + } + } + this.obstacles = updatedObstacles; + + if (this.obstacles.length > 0) { + var lastObstacle = this.obstacles[this.obstacles.length - 1]; + + if (lastObstacle && !lastObstacle.followingObstacleCreated && + lastObstacle.isVisible() && + (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) < + this.dimensions.WIDTH) { + this.addNewObstacle(currentSpeed); + lastObstacle.followingObstacleCreated = true; + } + } else { + // Create new obstacles. + this.addNewObstacle(currentSpeed); + } + }, + + removeFirstObstacle: function() { + this.obstacles.shift(); + }, + + /** + * Add a new obstacle. + * @param {number} currentSpeed + */ + addNewObstacle: function(currentSpeed) { + var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1); + var obstacleType = Obstacle.types[obstacleTypeIndex]; + + // Check for multiples of the same type of obstacle. + // Also check obstacle is available at current speed. + if (this.duplicateObstacleCheck(obstacleType.type) || + currentSpeed < obstacleType.minSpeed) { + this.addNewObstacle(currentSpeed); + } else { + var obstacleSpritePos = this.spritePos[obstacleType.type]; + + this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType, + obstacleSpritePos, this.dimensions, + this.gapCoefficient, currentSpeed, obstacleType.width)); + + this.obstacleHistory.unshift(obstacleType.type); + + if (this.obstacleHistory.length > 1) { + this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION); + } + } + }, + + /** + * Returns whether the previous two obstacles are the same as the next one. + * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION. + * @return {boolean} + */ + duplicateObstacleCheck: function(nextObstacleType) { + var duplicateCount = 0; + + for (var i = 0; i < this.obstacleHistory.length; i++) { + duplicateCount = this.obstacleHistory[i] == nextObstacleType ? + duplicateCount + 1 : 0; + } + return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION; + }, + + /** + * Reset the horizon layer. + * Remove existing obstacles and reposition the horizon line. + */ + reset: function() { + this.obstacles = []; + this.horizonLine.reset(); + this.nightMode.reset(); + }, + + /** + * Update the canvas width and scaling. + * @param {number} width Canvas width. + * @param {number} height Canvas height. + */ + resize: function(width, height) { + this.canvas.width = width; + this.canvas.height = height; + }, + + /** + * Add a new cloud to the horizon. + */ + addCloud: function() { + this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD, + this.dimensions.WIDTH)); + } +}; +})(); + +var HIDDEN_CLASS = 'hidden'; + +function onDocumentLoad() { + new Runner('.interstitial-wrapper'); +} + +document.addEventListener('DOMContentLoaded', onDocumentLoad); diff --git a/views/archive/dino/dino.min.css b/views/archive/dino/dino.min.css new file mode 100644 index 00000000..75bc918d --- /dev/null +++ b/views/archive/dino/dino.min.css @@ -0,0 +1,2 @@ +/*! Copyright (c) 2014 The Chromium Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. Extract source code from chromium by congerh. */ +body{--google-blue-600: rgb(26, 115, 232);--google-blue-700: rgb(25, 103, 210);--google-gray-50: rgb(248, 249, 250);--google-gray-500: rgb(154, 160, 166);--google-gray-600: rgb(128, 134, 139);--google-gray-700: rgb(95, 99, 105);background-color:#fff;color:var(--google-gray-700);word-wrap:break-word}html{-webkit-text-size-adjust:100%;font-size:125%}.icon{background-repeat:no-repeat;background-size:100%}.bad-clock button,.captive-portal button,.main-frame-blocked button,.neterror button,.offline button,.pdf button,.ssl button,.safe-browsing-billing button{background:var(--google-blue-600)}.error-code{color:#646464;font-size:.86667em;text-transform:uppercase;margin-top:12px}h1{color:var(--google-gray-900);font-size:1.6em;font-weight:400;line-height:1.25em;margin-bottom:16px}h2{font-size:1.2em;font-weight:400}.icon{height:72px;margin:0 0 40px;width:72px}.interstitial-wrapper{box-sizing:border-box;font-size:1em;line-height:1.6em;margin:14vh auto 0;max-width:600px;width:100%}#main-message>p{display:inline}@media (max-width:700px){.interstitial-wrapper{padding:0 10%}}@media (max-width:420px){.interstitial-wrapper{padding:0 5%}}@media (min-width:240px) and (max-width:420px) and (min-height:401px),(min-width:421px) and (min-height:240px) and (max-height:560px){body .nav-wrapper{background:#fff;bottom:0;box-shadow:0 -22px 40px #fff;left:0;margin:0 auto;max-width:736px;padding-left:24px;padding-right:24px;position:fixed;right:0;width:100%;z-index:2}.interstitial-wrapper{max-width:736px}#details,#main-content{padding-bottom:40px}}@media (max-width:420px) and (orientation:portrait),(max-height:560px){body{margin:0 auto}#details.hidden,#main-content.hidden{display:block;height:0;opacity:0;overflow:hidden;padding-bottom:0;transition:none}h1{font-size:1.5em;margin-bottom:8px}.icon{margin-bottom:5.69vh}.interstitial-wrapper{box-sizing:border-box;margin:7vh auto 12px;padding:0 24px;position:relative}.interstitial-wrapper p{font-size:.95em;line-height:1.61em;margin-top:8px}#main-content{margin:0;transition:opacity 100ms cubic-bezier(.4,0,.2,1)}}@media (min-width:421px) and (min-height:500px) and (max-height:560px){.interstitial-wrapper{margin-top:10vh}}@media (min-height:400px) and (orientation:portrait){.interstitial-wrapper{margin-bottom:145px}}@media (min-height:500px) and (max-height:650px) and (max-width:414px) and (orientation:portrait){.interstitial-wrapper{margin-top:7vh}}@media (min-height:650px) and (max-width:414px) and (orientation:portrait){.interstitial-wrapper{margin-top:10vh}}@media (max-height:400px) and (orientation:portrait),(max-height:239px) and (orientation:landscape),(max-width:419px) and (max-height:399px){.interstitial-wrapper{display:flex;flex-direction:column;margin-bottom:0}#main-content{flex:1 1 auto;order:0}}html[subframe] #main-frame-error{display:none}html:not([subframe]) #sub-frame-error{display:none}h1{margin-top:0;word-wrap:break-word}h1 span{font-weight:500}h2{color:#666;font-size:1.2em;font-weight:400;margin:10px 0}a{color:#15c;text-decoration:none}.icon{-webkit-user-select:none;display:inline-block}.icon-offline{content:-webkit-image-set(url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAGxJREFUeF7tyMEJwkAQRuFf5ipMKxYQiJ3Z2nSwrWwBA0+DQZcdxEOueaePp9+dQZFB7GpUcURSVU66yVNFj6LFICatThZB6r/ko/pbRpUgilY0Cbw5sNmb9txGXUKyuH7eV25x39DtJXUNPQGJtWFV+BT/QAAAAABJRU5ErkJggg==) 1x,url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQBAMAAAAVaP+LAAAAGFBMVEUAAABTU1NNTU1TU1NPT09SUlJSUlJTU1O8B7DEAAAAB3RSTlMAoArVKvVgBuEdKgAAAJ1JREFUeF7t1TEOwyAMQNG0Q6/UE+RMXD9d/tC6womIFSL9P+MnAYOXeTIzMzMzMzMzaz8J9Ri6HoITmuHXhISE8nEh9yxDh55aCEUoTGbbQwjqHwIkRAEiIaG0+0AA9VBMaE89Rogeoww936MQrWdBr4GN/z0IAdQ6nQ/FIpRXDwHcA+JIJcQowQAlFUA0MfQpXLlVQfkzR4igS6ENjknm/wiaGhsAAAAASUVORK5CYII=) 2x);position:relative}.icon-disabled{content:-webkit-image-set(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,url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOAAAACQAQMAAAArwfVjAAAABlBMVEVMaXFTU1OXUj8tAAAAAXRSTlMAQObYZgAAAYdJREFUeF7F1EFqwzAUBNARAmVj0FZe5QoBH6BX+dn4GlY2PYNzGx/A0CvkCIJuvIraKJKbgBvzf2g62weDGD7CYggpfFReis4J0ey9EGFIiEQQojFSlA9kSIiqd0KkFjKsewgRbStEN19mxUPTtmW9HQ/h6tyqNQ8NlSMZdzyE6qkoE0trVYGFm0n1WYeBhduzwbwBC7voS+vIxfeMjeaiLxsMMtQNwMPtuew+DjzcTHk8YMfDknEcIUOtf2lVfgVH3K4Xv5PRYAXRVMtItIJ3rfaCIVn9DsTH2NxisAVRex2Hh3hX+/mRUR08bAwPEYsI51ZxWH4Q0SpicQRXeyEaIug48FEdegARfMz/tADVsRciwTAxW308ehmC2gLraC+YCbV3QoTZexa+zegAEW5PhhgYfmbvJgcRqngGByOSXdFJcLk2JeDPEN0kxe1JhIt5FiFA+w+ItMELsUyPF2IaJ4aILqb4FbxPwhImwj6JauKgDUCYaxmYIsd4KXdMjIC9ItB5Bn4BNRwsG0XM2nwAAAAASUVORK5CYII=) 2x);width:112px}.error-code{display:block;font-size:.8em}.hidden{display:none}#suggestions-list p{margin-block-end:0}#suggestions-list ul{margin-top:0}.snackbar{background:#323232;border-radius:2px;bottom:24px;box-sizing:border-box;color:#fff;font-size:.87em;left:24px;max-width:568px;min-width:288px;opacity:0;padding:16px 24px 12px;position:fixed;transform:translateY(90px);will-change:opacity,transform;z-index:999}.snackbar-show{-webkit-animation:show-snackbar .25s cubic-bezier(0,0,.2,1) forwards,hide-snackbar .25s cubic-bezier(.4,0,1,1) forwards 5s}@-webkit-keyframes show-snackbar{to{opacity:1;transform:translateY(0)}}@-webkit-keyframes hide-snackbar{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(90px)}}@media (max-width:640px),(max-height:640px){h1{margin:0 0 15px}#content-top{margin:15px}}html[subframe] body{overflow:hidden}#sub-frame-error{-webkit-align-items:center;background-color:#ddd;display:-webkit-flex;-webkit-flex-flow:column;height:100%;-webkit-justify-content:center;left:0;position:absolute;text-align:center;top:0;transition:background-color .2s ease-in-out;width:100%}#sub-frame-error:hover{background-color:#eee}#sub-frame-error .icon-generic{margin:0 0 16px}#sub-frame-error-details{margin:0 10px;text-align:center;visibility:hidden}#sub-frame-error:hover #sub-frame-error-details{visibility:visible}@media (max-width:200px),(max-height:95px){#sub-frame-error-details{display:none}}@media (max-height:100px){#sub-frame-error .icon-generic{height:auto;margin:0;padding-top:0;width:25px}}.offline{transition:-webkit-filter 1.5s cubic-bezier(.65,.05,.36,1),background-color 1.5s cubic-bezier(.65,.05,.36,1);will-change:-webkit-filter,background-color}.offline #main-message>p{display:none}.offline.inverted{-webkit-filter:invert(100%);background-color:#000}.offline .interstitial-wrapper{color:#2b2b2b;font-size:1em;line-height:1.55;margin:0 auto;max-width:600px;padding-top:100px;width:100%}.offline .runner-container{direction:ltr;height:150px;max-width:600px;overflow:hidden;position:absolute;top:35px;width:44px}.offline .runner-canvas{height:150px;max-width:600px;opacity:1;overflow:hidden;position:absolute;top:0;z-index:10}.offline .controller{background:rgba(247,247,247,.1);height:100vh;left:0;position:absolute;top:0;width:100vw;z-index:9}#offline-resources{display:none}@media (max-width:420px){.snackbar{left:0;bottom:0;width:100%;border-radius:0}}@media (max-height:350px){h1{margin:0 0 15px}.icon-offline{margin:0 0 10px}.interstitial-wrapper{margin-top:5%}}@media (min-width:420px) and (max-width:736px) and (min-height:240px) and (max-height:420px) and (orientation:landscape){.interstitial-wrapper{margin-bottom:100px}}@media (max-width:360px) and (max-height:480px){.offline .interstitial-wrapper{padding-top:60px}.offline .runner-container{top:8px}}@media (min-height:240px) and (orientation:landscape){.offline .interstitial-wrapper{margin-bottom:90px}.icon-offline{margin-bottom:20px}}@media (max-height:320px) and (orientation:landscape){.icon-offline{margin-bottom:0}.offline .runner-container{top:10px}}@media (max-width:240px){.interstitial-wrapper{overflow:inherit;padding:0 8px}}.arcade-mode,.arcade-mode .runner-container,.arcade-mode .runner-canvas{image-rendering:pixelated;max-width:100%;overflow:hidden}.arcade-mode #buttons,.arcade-mode #main-content{opacity:0;overflow:hidden}.arcade-mode .interstitial-wrapper{height:100vh;max-width:100%;overflow:hidden}.arcade-mode .runner-container{left:0;margin:auto;right:0;transform-origin:top center;transition:transform 250ms cubic-bezier(.4,0,1,1) .4s;z-index:2} diff --git a/views/archive/dino/dino.min.js b/views/archive/dino/dino.min.js new file mode 100644 index 00000000..af336b1e --- /dev/null +++ b/views/archive/dino/dino.min.js @@ -0,0 +1,65 @@ +/*! Copyright (c) 2014 The Chromium Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. Extract source code from chromium by congerh. */ +(function(){function c(a,b){if(c.instance_)return c.instance_;c.instance_=this;this.outerContainerEl=document.querySelector(a);this.touchController=this.snackbarEl=this.containerEl=null;this.config=b||c.config;this.dimensions=c.defaultDimensions;this.distanceMeter=this.tRex=this.canvasCtx=this.canvas=null;this.runningTime=this.time=this.highestScore=this.distanceRan=0;this.msPerFrame=1E3/60;this.currentSpeed=this.config.SPEED;this.obstacles=[];this.inverted=this.paused=this.crashed=this.playing=this.activated= +!1;this.invertTimer=0;this.resizeTimerId_=null;this.playCount=0;this.audioBuffer=null;this.soundFx={};this.audioContext=null;this.images={};this.imagesLoaded=0;this.isDisabled()?this.setupDisabledRunner():this.loadImages()}function q(a,b){return Math.floor(Math.random()*(b-a+1))+a}function B(a){var b=a.length/4*3;a=atob(a);var d=new ArrayBuffer(b);d=new Uint8Array(d);for(var c=0;cc&&a.yb.y&&(d=!0);return d}function g(a,b,d,c){this.x=a;this.y=b;this.width=d;this.height=c}function m(a,b,d,c,n,f,e){this.canvasCtx=a;this.spritePos=d;this.typeConfig=b;this.gapCoefficient=n;this.size=q(1,m.MAX_OBSTACLE_LENGTH); +this.dimensions=c;this.remove=!1;this.xPos=c.WIDTH+(e||0);this.width=this.yPos=0;this.collisionBoxes=[];this.timer=this.currentFrame=this.speedOffset=this.gap=0;this.init(f)}function f(a,b){this.canvas=a;this.canvasCtx=a.getContext("2d");this.spritePos=b;this.currentFrame=this.groundYPos=this.yPos=this.xPos=0;this.currentAnimFrames=[];this.timer=this.animStartTime=this.blinkCount=this.blinkDelay=0;this.msPerFrame=1E3/60;this.config=f.config;this.status=f.status.WAITING;this.ducking=this.jumping=!1; +this.jumpVelocity=0;this.speedDrop=this.reachedMinHeight=!1;this.jumpspotX=this.jumpCount=0;this.init()}function h(a,b,d){this.canvas=a;this.canvasCtx=a.getContext("2d");this.image=c.imageSprite;this.spritePos=b;this.x=0;this.y=5;this.highScore=this.maxScore=this.currentDistance=0;this.container=null;this.digits=[];this.achievement=!1;this.defaultString="";this.flashIterations=this.flashTimer=0;this.invertTrigger=!1;this.config=h.config;this.maxScoreUnits=this.config.MAX_DISTANCE_UNITS;this.init(d)} +function p(a,b,d){this.canvas=a;this.canvasCtx=this.canvas.getContext("2d");this.spritePos=b;this.xPos=this.containerWidth=d;this.yPos=0;this.remove=!1;this.cloudGap=q(p.config.MIN_CLOUD_GAP,p.config.MAX_CLOUD_GAP);this.init()}function e(a,b,d){this.spritePos=b;this.canvas=a;this.canvasCtx=a.getContext("2d");this.xPos=d-50;this.yPos=30;this.opacity=this.currentPhase=0;this.containerWidth=d;this.stars=[];this.drawStars=!1;this.placeStars()}function k(a,b){this.spritePos=b;this.canvas=a;this.canvasCtx= +a.getContext("2d");this.sourceDimensions={};this.dimensions=k.dimensions;this.sourceXPos=[this.spritePos.x,this.spritePos.x+this.dimensions.WIDTH];this.xPos=[];this.yPos=0;this.bumpThreshold=.5;this.setSourceDimensions();this.draw()}function v(a,b,d,c){this.canvas=a;this.canvasCtx=this.canvas.getContext("2d");this.config=v.config;this.dimensions=d;this.gapCoefficient=c;this.obstacles=[];this.obstacleHistory=[];this.horizonOffsets=[0,0];this.cloudFrequency=this.config.CLOUD_FREQUENCY;this.spritePos= +b;this.nightMode=null;this.clouds=[];this.cloudSpeed=this.config.BG_CLOUD_SPEED;this.horizonLine=null;this.init()}window.Runner=c;var t=1this.dimensions.WIDTH?(a=b*this.dimensions.WIDTH/600*this.config.MOBILE_SPEED_COEFFICIENT,this.currentSpeed=a>b?b:a):a&&(this.currentSpeed=a)},init:function(){document.querySelector("."+c.classes.ICON).style.visibility= +"hidden";this.adjustDimensions();this.setSpeed();this.containerEl=document.createElement("div");this.containerEl.className=c.classes.CONTAINER;var a=this.containerEl,b=this.dimensions.WIDTH,d=this.dimensions.HEIGHT,l=c.classes.PLAYER,n=document.createElement("canvas");n.className=l?c.classes.CANVAS+" "+l:c.classes.CANVAS;n.width=b;n.height=d;a.appendChild(n);this.canvas=n;this.canvasCtx=this.canvas.getContext("2d");this.canvasCtx.fillStyle="#f7f7f7";this.canvasCtx.fill();c.updateCanvasScaling(this.canvas); +this.horizon=new v(this.canvas,this.spriteDef,this.dimensions,this.config.GAP_COEFFICIENT);this.distanceMeter=new h(this.canvas,this.spriteDef.TEXT_SPRITE,this.dimensions.WIDTH);this.tRex=new f(this.canvas,this.spriteDef.TREX);this.outerContainerEl.appendChild(this.containerEl);this.startListening();this.update();window.addEventListener(c.events.RESIZE,this.debounceResize.bind(this))},createTouchController:function(){this.touchController=document.createElement("div");this.touchController.className= +c.classes.TOUCH_CONTROLLER;this.touchController.addEventListener(c.events.TOUCHSTART,this);this.touchController.addEventListener(c.events.TOUCHEND,this);this.outerContainerEl.appendChild(this.touchController)},debounceResize:function(){this.resizeTimerId_||(this.resizeTimerId_=setInterval(this.adjustDimensions.bind(this),250))},adjustDimensions:function(){clearInterval(this.resizeTimerId_);this.resizeTimerId_=null;var a=window.getComputedStyle(this.outerContainerEl);a=Number(a.paddingLeft.substr(0, +a.paddingLeft.length-2));this.dimensions.WIDTH=this.outerContainerEl.offsetWidth-2*a;this.isArcadeMode()&&(this.dimensions.WIDTH=Math.min(600,this.dimensions.WIDTH),this.activated&&this.setArcadeModeContainerScale());this.canvas&&(this.canvas.width=this.dimensions.WIDTH,this.canvas.height=this.dimensions.HEIGHT,c.updateCanvasScaling(this.canvas),this.distanceMeter.calcXPos(this.dimensions.WIDTH),this.clearCanvas(),this.horizon.update(0,0,!0),this.tRex.update(0),this.playing||this.crashed||this.paused? +(this.containerEl.style.width=this.dimensions.WIDTH+"px",this.containerEl.style.height=this.dimensions.HEIGHT+"px",this.distanceMeter.update(0,Math.ceil(this.distanceRan)),this.stop()):this.tRex.draw(0,0),this.crashed&&this.gameOverPanel&&(this.gameOverPanel.updateDimensions(this.dimensions.WIDTH),this.gameOverPanel.draw()))},playIntro:function(){this.activated||this.crashed?this.crashed&&this.restart():(this.playingIntro=!0,this.tRex.playingIntro=!0,document.styleSheets[0].insertRule("@-webkit-keyframes intro { from { width:"+ +f.config.WIDTH+"px }to { width: "+this.dimensions.WIDTH+"px }}",0),this.containerEl.addEventListener(c.events.ANIM_END,this.startGame.bind(this)),this.containerEl.style.webkitAnimation="intro .4s ease-out 1 both",this.containerEl.style.width=this.dimensions.WIDTH+"px",this.setPlayStatus(!0),this.activated=!0)},startGame:function(){this.isArcadeMode()&&this.setArcadeMode();this.runningTime=0;this.playingIntro=!1;this.tRex.playingIntro=!1;this.containerEl.style.webkitAnimation="";this.playCount++;document.addEventListener(c.events.VISIBILITY, +this.onVisibilityChange.bind(this));window.addEventListener(c.events.BLUR,this.onVisibilityChange.bind(this));window.addEventListener(c.events.FOCUS,this.onVisibilityChange.bind(this))},clearCanvas:function(){this.canvasCtx.clearRect(0,0,this.dimensions.WIDTH,this.dimensions.HEIGHT)},isCanvasInView:function(){return this.containerEl.getBoundingClientRect().top>c.config.CANVAS_IN_VIEW_OFFSET},update:function(){this.updatePending=!1;var a=r(),b=a-(this.time||a);this.time=a;if(this.playing){this.clearCanvas(); +this.tRex.jumping&&this.tRex.updateJump(b);this.runningTime+=b;a=this.runningTime>this.config.CLEAR_TIME;1!=this.tRex.jumpCount||this.playingIntro||this.playIntro();this.playingIntro?this.horizon.update(0,this.currentSpeed,a):(b=this.activated?b:0,this.horizon.update(b,this.currentSpeed,a,this.inverted));if(a)a:{var d=this.horizon.obstacles[0],l=this.tRex;a=new g(l.xPos+1,l.yPos+1,l.config.WIDTH-2,l.config.HEIGHT-2);var n=new g(d.xPos+1,d.yPos+1,d.typeConfig.width*d.size-2,d.typeConfig.height-2); +if(A(a,n)){d=d.collisionBoxes;l=l.ducking?f.collisionBoxes.DUCKING:f.collisionBoxes.RUNNING;for(var e=0;ethis.config.INVERT_FADE_DURATION? +(this.invertTimer=0,this.invertTrigger=!1,this.invert()):this.invertTimer?this.invertTimer+=b:(a=this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan)),0=this.config.GAMEOVER_CLEAR_TIME&&c.keycodes.JUMP[b])&&this.restart()):this.paused&&d&&(this.tRex.reset(), +this.play())},isLeftClickOnCanvas:function(a){return null!=a.button&&2>a.button&&a.type==c.events.POINTERUP&&a.target==this.canvas},scheduleNextUpdate:function(){this.updatePending||(this.updatePending=!0,this.raqId=requestAnimationFrame(this.update.bind(this)))},isRunning:function(){return!!this.raqId},gameOver:function(){this.playSound(this.soundFx.HIT);y&&window.navigator.vibrate&&window.navigator.vibrate(200);this.stop();this.crashed=!0;this.distanceMeter.achievement=!1;this.tRex.update(100,f.status.CRASHED); +this.gameOverPanel?this.gameOverPanel.draw():this.gameOverPanel=new u(this.canvas,this.spriteDef.TEXT_SPRITE,this.spriteDef.RESTART,this.dimensions);this.distanceRan>this.highestScore&&(this.highestScore=Math.ceil(this.distanceRan),this.distanceMeter.setHighScore(this.highestScore));this.time=r()},stop:function(){this.setPlayStatus(!1);this.paused=!0;cancelAnimationFrame(this.raqId);this.raqId=0},play:function(){this.crashed||(this.setPlayStatus(!0),this.paused=!1,this.tRex.update(0,f.status.RUNNING), +this.time=r(),this.update())},restart:function(){this.raqId||(this.playCount++,this.runningTime=0,this.setPlayStatus(!0),this.crashed=this.paused=!1,this.distanceRan=0,this.setSpeed(this.config.SPEED),this.time=r(),this.containerEl.classList.remove(c.classes.CRASHED),this.clearCanvas(),this.distanceMeter.reset(this.highestScore),this.horizon.reset(),this.tRex.reset(),this.playSound(this.soundFx.BUTTON_PRESS),this.invert(!0),this.bdayFlashTimer=null,this.update())},setPlayStatus:function(a){this.touchController&& +this.touchController.classList.toggle(HIDDEN_CLASS,!a);this.playing=a},isArcadeMode:function(){return"chrome://dino/"==document.title},setArcadeMode:function(){document.body.classList.add(c.classes.ARCADE_MODE);this.setArcadeModeContainerScale()},setArcadeModeContainerScale:function(){var a=window.innerHeight,b=Math.max(1,Math.min(a/this.dimensions.HEIGHT,window.innerWidth/this.dimensions.WIDTH));this.containerEl.style.transform="scale("+b+") translateY("+Math.ceil(Math.max(0,(a-this.dimensions.HEIGHT* +b-c.config.ARCADE_MODE_INITIAL_TOP_POSITION)*c.config.ARCADE_MODE_TOP_POSITION_PERCENT))*window.devicePixelRatio+"px)"},onVisibilityChange:function(a){document.hidden||document.webkitHidden||"blur"==a.type||"visible"!=document.visibilityState?this.stop():this.crashed||(this.tRex.reset(),this.play())},playSound:function(a){if(a){var b=this.audioContext.createBufferSource();b.buffer=a;b.connect(this.audioContext.destination);b.start(0)}},invert:function(a){a?(document.body.classList.toggle(c.classes.INVERTED, +!1),this.invertTimer=0,this.inverted=!1):this.inverted=document.body.classList.toggle(c.classes.INVERTED,this.invertTrigger)}};c.updateCanvasScaling=function(a,b,d){var c=a.getContext("2d"),n=Math.floor(window.devicePixelRatio)||1,f=Math.floor(c.webkitBackingStorePixelRatio)||1,e=n/f;if(n!==f)return b=b||a.width,d=d||a.height,a.width=b*e,a.height=d*e,a.style.width=b+"px",a.style.height=d+"px",c.scale(e,e),!0;1==n&&(a.style.width=a.width+"px",a.style.height=a.height+"px");return!1};u.dimensions={TEXT_X:0, +TEXT_Y:13,TEXT_WIDTH:191,TEXT_HEIGHT:11,RESTART_WIDTH:36,RESTART_HEIGHT:32};u.prototype={updateDimensions:function(a,b){this.canvasDimensions.WIDTH=a;b&&(this.canvasDimensions.HEIGHT=b)},draw:function(){var a=u.dimensions,b=this.canvasDimensions.WIDTH/2,d=a.TEXT_X,l=a.TEXT_Y,e=a.TEXT_WIDTH,f=a.TEXT_HEIGHT,g=Math.round(b-a.TEXT_WIDTH/2),h=Math.round((this.canvasDimensions.HEIGHT-25)/3),k=a.TEXT_WIDTH,p=a.TEXT_HEIGHT,m=a.RESTART_WIDTH,q=a.RESTART_HEIGHT;b-=a.RESTART_WIDTH/2;var r=this.canvasDimensions.HEIGHT/ +2;t&&(l*=2,d*=2,e*=2,f*=2,m*=2,q*=2);d+=this.textImgPos.x;l+=this.textImgPos.y;this.canvasCtx.drawImage(c.imageSprite,d,l,e,f,g,h,k,p);this.canvasCtx.drawImage(c.imageSprite,this.restartImgPos.x,this.restartImgPos.y,m,q,b,r,a.RESTART_WIDTH,a.RESTART_HEIGHT)}};m.MAX_GAP_COEFFICIENT=1.5;m.MAX_OBSTACLE_LENGTH=3;m.prototype={init:function(a){this.cloneCollisionBoxes();1a&&(this.size=1);this.width=this.typeConfig.width*this.size;if(Array.isArray(this.typeConfig.yPos)){var b= +y?this.typeConfig.yPosMobile:this.typeConfig.yPos;this.yPos=b[q(0,b.length-1)]}else this.yPos=this.typeConfig.yPos;this.draw();1=this.typeConfig.frameRate&&(this.currentFrame= +this.currentFrame==this.typeConfig.numFrames-1?0:this.currentFrame+1,this.timer=0)),this.draw(),this.isVisible()||(this.remove=!0))},getGap:function(a,b){var d=Math.round(this.width*b+this.typeConfig.minGap*a);return q(d,Math.round(d*m.MAX_GAP_COEFFICIENT))},isVisible:function(){return 0=this.msPerFrame&&(this.currentFrame=this.currentFrame==this.currentAnimFrames.length-1?0:this.currentFrame+1,this.timer=0);this.speedDrop&&this.yPos==this.groundYPos&&(this.speedDrop=!1,this.setDuck(!0))},draw:function(a,b){var d=a,l=b,e=this.ducking&&this.status!=f.status.CRASHED?this.config.WIDTH_DUCK:this.config.WIDTH,g=this.config.HEIGHT,h=g;t&&(d*=2,l*=2,e*=2,g*=2);d+=this.spritePos.x;l+=this.spritePos.y;this.ducking&&this.status!= +f.status.CRASHED?this.canvasCtx.drawImage(c.imageSprite,d,l,e,g,this.xPos,this.yPos,this.config.WIDTH_DUCK,h):(this.ducking&&this.status==f.status.CRASHED&&this.xPos++,this.canvasCtx.drawImage(c.imageSprite,d,l,e,g,this.xPos,this.yPos,this.config.WIDTH,h));this.canvasCtx.globalAlpha=1},setBlinkDelay:function(){this.blinkDelay=Math.ceil(Math.random()*f.BLINK_TIMING)},blink:function(a){a-this.animStartTime>=this.blinkDelay&&(this.draw(this.currentAnimFrames[this.currentFrame],0),1==this.currentFrame&& +(this.setBlinkDelay(),this.animStartTime=a,this.blinkCount++))},startJump:function(a){this.jumping||(this.update(0,f.status.JUMPING),this.jumpVelocity=this.config.INIITAL_JUMP_VELOCITY-a/10,this.jumping=!0,this.speedDrop=this.reachedMinHeight=!1)},endJump:function(){this.reachedMinHeight&&this.jumpVelocitythis.groundYPos&&(this.reset(),this.jumpCount++)},setSpeedDrop:function(){this.speedDrop=!0;this.jumpVelocity=1},setDuck:function(a){a&&this.status!=f.status.DUCKING?(this.update(0,f.status.DUCKING),this.ducking=!0):this.status== +f.status.DUCKING&&(this.update(0,f.status.RUNNING),this.ducking=!1)},reset:function(){this.yPos=this.groundYPos;this.jumpVelocity=0;this.ducking=this.jumping=!1;this.update(0,f.status.RUNNING);this.speedDrop=this.midair=!1;this.jumpCount=0}};h.dimensions={WIDTH:10,HEIGHT:13,DEST_WIDTH:11};h.yPos=[0,13,27,40,53,67,80,93,107,120];h.config={MAX_DISTANCE_UNITS:5,ACHIEVEMENT_DISTANCE:100,COEFFICIENT:.025,FLASH_DURATION:250,FLASH_ITERATIONS:3};h.prototype={init:function(a){var b="";this.calcXPos(a);this.maxScore= +this.maxScoreUnits;for(a=0;a2*this.config.FLASH_DURATION&&(this.flashTimer=0,this.flashIterations++)):(this.achievement= +!1,this.flashTimer=this.flashIterations=0):(b=this.getActualDistance(b),b>this.maxScore&&this.maxScoreUnits==this.config.MAX_DISTANCE_UNITS?(this.maxScoreUnits++,this.maxScore=parseInt(this.maxScore+"9")):this.distance=0,0=e.phases.length&&(this.currentPhase=0));a&&(1>this.opacity||0==this.opacity)?this.opacity+=e.config.FADE_SPEED:0this.bumpThreshold?this.dimensions.WIDTH:0},draw:function(){this.canvasCtx.drawImage(c.imageSprite,this.sourceXPos[0],this.spritePos.y,this.sourceDimensions.WIDTH,this.sourceDimensions.HEIGHT,this.xPos[0],this.yPos,this.dimensions.WIDTH,this.dimensions.HEIGHT);this.canvasCtx.drawImage(c.imageSprite, +this.sourceXPos[1],this.spritePos.y,this.sourceDimensions.WIDTH,this.sourceDimensions.HEIGHT,this.xPos[1],this.yPos,this.dimensions.WIDTH,this.dimensions.HEIGHT)},updateXPos:function(a,b){var c=0==a?1:0;this.xPos[a]-=b;this.xPos[c]=this.xPos[a]+this.dimensions.WIDTH;this.xPos[a]<=-this.dimensions.WIDTH&&(this.xPos[a]+=2*this.dimensions.WIDTH,this.xPos[c]=this.xPos[a]-this.dimensions.WIDTH,this.sourceXPos[a]=this.getRandomType()+this.spritePos.x)},update:function(a,b){var c=Math.floor(.06*b*a);0>= +this.xPos[0]?this.updateXPos(0,c):this.updateXPos(1,c);this.draw()},reset:function(){this.xPos[0]=0;this.xPos[1]=k.dimensions.WIDTH}};v.config={BG_CLOUD_SPEED:.2,BUMPY_THRESHOLD:.3,CLOUD_FREQUENCY:.5,HORIZON_HEIGHT:16,MAX_CLOUDS:6};v.prototype={init:function(){this.addCloud();this.horizonLine=new k(this.canvas,this.spritePos.HORIZON);this.nightMode=new e(this.canvas,this.spritePos.MOON,this.dimensions.WIDTH)},update:function(a,b,c,e){this.runningTime+=a;this.horizonLine.update(a,b);this.nightMode.update(e); +this.updateClouds(a,b);c&&this.updateObstacles(a,b)},updateClouds:function(a,b){var c=this.cloudSpeed/1E3*a*b,e=this.clouds.length;if(e){for(var f=e-1;0<=f;f--)this.clouds[f].update(c);c=this.clouds[e-1];ec.cloudGap&&this.cloudFrequency>Math.random()&&this.addCloud();this.clouds=this.clouds.filter(function(a){return!a.remove})}else this.addCloud()},updateObstacles:function(a,b){for(var c=this.obstacles.slice(0),e=0;e=c.config.MAX_OBSTACLE_DUPLICATION},reset:function(){this.obstacles=[];this.horizonLine.reset();this.nightMode.reset()},resize:function(a, +b){this.canvas.width=a;this.canvas.height=b},addCloud:function(){this.clouds.push(new p(this.canvas,this.spritePos.CLOUD,this.dimensions.WIDTH))}}})();var HIDDEN_CLASS="hidden";function onDocumentLoad(){new Runner(".interstitial-wrapper")}document.addEventListener("DOMContentLoaded",onDocumentLoad); diff --git a/views/archive/dino/images/default_100_percent/100-disabled.png b/views/archive/dino/images/default_100_percent/100-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..526075e6d55359bb31f0ba3b1154524a04dea4cf GIT binary patch literal 382 zcmV-^0fGLBP)h^Cw))S&|eF$oR3&qP*utvWl5u)5BJ5eLqbe@ouCD271F;(~FE7iZs|(POS0c)#JwLWY zBGQt4#20h_G?#uPB!We9p#XAWq~&LmH9l zY6Gzw`25#Oqr+4hKzURGLGb0_L0YaxKTTRDpHf`i`r+vJIt7J=jZJm~@*=1JDx(!A z(6!WXH6ham$Q&<5=RnaA)j&s&v^0XWUJ%hZlTrV~H@vTQ90^h0-VRL=j$1_pdEk_W zbl0|iz_%lwv;u`}tu)lD1!;yktq8F?pV#rvZ=#7C!AM$ZF^Q4SdDMnWOVVySRbvsh zq!oab5%H%&)@v_V=frZhCRJ9=&??LFk^i5;$ToIOUOd`YcvCxfV8H< z2n~Z5Mv#V)3Un=89=!G^`z@rkq%J@&r0K@i(l$7HzJKHm+7_f?KGS(Q-YPBYAgwHv zv}u8Tf$x^)uEpeir|CTIUj$$U6DqY zIbu0W8VjW9=F}0Sp>SPVUHt{+Do8_P8)?7j&#ypfN!t4`r@b#pI~-pOYlknD*3$u~ z=2Sr%8j7GT&D9ssju=52MmmtDUe?vrFsDVxx0Hq>=f8{6UWn{(1Qug^pWyL6X?QWi z%NnjE9iC4bAUGqfk?Qf%e*H%`BuRU_Ds3+JOOtEToL4f+s%&YxVM;p>9xm;_kNly@ zaZseiC>Q@9#(J5tw74Y3#$NTgzq)}mnU}^s*PsdaxhUhWd!%VlwQ^J0jYVm2V!p6( z#Aq5im@j$Y12LxsN=wpYJ89XIOT)FN_LIvvl>Dz5ng+Ve@x?Ol@750pnPXISl(PuZ z?5!_A)8WN3q=}l%sMOLh#G|a0=Blrf=C3_18;~YR+P2cV46q8y23RPJ(#rMH8qcc! zsyQvXw0H5+GFFEfA{LHPB_TvB^DQe_LvgD#d+YPzJ~9N)$t;i33P|h5-;v{?Fi0!< z`Wk7q|0D7h(#D^AO7Iz%Z8A5O#F}?1E#o1DA&tjuD6M1arExJx>&C(;jTp1udTA9^ z@ue;4qBg%irx{lVq{=Np8gn`BOd>dmP$l)HC~1A06)!a`JLW>!V^i7~NHe#N+Ny(H~K?&PJ_@ua=JzWz{YFzcU2`MkxKh6xN? z9;J0W6aa6O#>KKUA6wd~t_Ux(Njz!){O)ngopA+9BazmWMj~fvg6|yyCvmDIV!Xn^ zq80|P$*@!Q#FQrQNRPnWdN?)UoGq)fGbK$IN-eFR1!-{{&#MPM8wt=@Ol~o zXnIKymYqr~WgHvQWDKND6JJ`!JuKpKDh&mYlMrdt1W2PM{+t$H+T?r3a1yCXBBG$t zDD5I$sia+mm!(--50{3pUFYEUM)}fpX>M7X^K%*&ig=zDJ29mtX+?-MY8It6AC2n# zAW^S9zeXCO&*vjmNhYB_Fkz-+lUjf@73N@R_D8TP2TSW%6^2X98lYXaW@nXLSW>s(}`US{fSPLmJ1fk_m0SH1n=j#@4r& z21IqOCo!Z^4G_doOG6{VyYM!k#?h-}N;9c-aAQ+rADRoVl*SZE8rAjRH{0P#k`UDZ zK@63&sPAD<()K1T{#~sU!RGr&Gp_C9S4l&&sNv;W3<{#p)R@OX*jM#{&1;&zw9%(PS5sqR zB=oBa4GYuGq~UZe+MW7-ur%hf<2h*^v+$3S=9_gD`R=3fqmEoG!W>#<9N2V~gVq3Q zbSbUGF9>;Q1{-WOSdQ9yaG3Td%{5Y~Mua;m|14=4Lt2rQShu;=FgiG$!`20PPHCj2 zMY>CT{OB`b77nlCz&528xQ1TYr8Iwki-P8)F%dI3uluUvC%&`@`C-y18>LOwjX+u- zO46QJ8sj0XYJ{0X*FZv+G-fWlg65ItKbA4 zI%U;iqTSs&BR=o;!TYvN`|5^$_1%+_l9KX2Cxk^;gn{4R00000NkvXXu0mjf2iS$W literal 0 HcmV?d00001 diff --git a/views/archive/dino/images/default_200_percent/200-disabled.png b/views/archive/dino/images/default_200_percent/200-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..ea2b90ec9fadda7658a4424ada04e9dc4a3279e5 GIT binary patch literal 479 zcmV<50U-W~P)XE93}*ZoG=xD3Y6i8}ol?I@7(T*c2q}D2UW(2M(d@ktVMvHX z5TY?sln-P`h^lu&q!u!)dk7J2D?~S6X2nC(wq?B?58>+Esx=P{l_MEo8eh|?rVsQjldm$Pk=s56E9eMx}e9Zr}0M)Su4{S z3AJ}ZgxPx*zRl2002ovPDHLkV1hR&z=Qw* literal 0 HcmV?d00001 diff --git a/views/archive/dino/images/default_200_percent/200-error-offline.png b/views/archive/dino/images/default_200_percent/200-error-offline.png new file mode 100644 index 0000000000000000000000000000000000000000..79cae8411bbc1a0cc7e867e945b082dca37009a6 GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^6F``S8Ays|{O<-*5&=FTu0T3CIM~riKhsM25i$LfsD(<_aPAt2kH^Q*)zxhCOR53jehw0tU5{I28NJ}-*HRl_Ik zwll{oc_tMXMkU3CTYft*d=2o)@V9mAkXy`Sdw;RO+H}YDjlKVEIT){4&E-F;G$ov$ z@ecDN57i?Y2N_gF9ZU?rXvXXeb^K}U-m$=YA#b1Ov;Q2kq@;lkW$<+Mb6Mw<&;$U4 CSzM?9 literal 0 HcmV?d00001 diff --git a/views/archive/dino/images/default_200_percent/200-offline-sprite.png b/views/archive/dino/images/default_200_percent/200-offline-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..1a5ff75b9d64d0bb441e63c07f15a7a49a2bf264 GIT binary patch literal 3056 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 XIVF+gOt=A~=QB64Hhy7v`S$+-{80k{ literal 0 HcmV?d00001 diff --git a/views/archive/dino/index.html b/views/archive/dino/index.html new file mode 100644 index 00000000..dba0405a --- /dev/null +++ b/views/archive/dino/index.html @@ -0,0 +1,26 @@ + + + + + + T-Rex Runner + + + + +
+
+
+
+
+
+ + + +
+ + diff --git a/views/archive/dino/sounds/button-press.mp3 b/views/archive/dino/sounds/button-press.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..028f56183820bf7f1e0a869d56a13658d052d1ec GIT binary patch literal 5178 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 Y0n&OTCy>(SbG{JRIWpy}07?w}7nD!4*Z=?k literal 0 HcmV?d00001 diff --git a/views/archive/dino/sounds/score-reached.mp3 b/views/archive/dino/sounds/score-reached.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..75b83ba85767ce2e9b1b777d21e86f67b4bdb59a GIT binary patch literal 9250 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​eOS is a Jav​aScript based Desk​top Envior​nment that runs in your brow​ser. +
+ +