├── .gitignore ├── favicon.ico ├── meta ├── apple-touch-icon.png ├── launcher-icon-1-5x.png ├── launcher-icon-1x.png ├── launcher-icon-2x.png ├── launcher-icon-3x.png ├── launcher-icon-4x.png ├── launcher-icon-0-75x.png ├── apple-touch-startup-image-640x920.png └── apple-touch-startup-image-640x1096.png ├── style ├── fonts │ ├── ClearSans-Bold-webfont.eot │ ├── ClearSans-Bold-webfont.woff │ ├── ClearSans-Light-webfont.eot │ ├── ClearSans-Light-webfont.woff │ ├── ClearSans-Regular-webfont.eot │ ├── ClearSans-Regular-webfont.woff │ ├── clear-sans.css │ └── ClearSans-Bold-webfont.svg ├── helpers.scss ├── addtohomescreen.css ├── main.scss └── main.css ├── js ├── bind_polyfill.js ├── tile.js ├── application.js ├── animframe_polyfill.js ├── local_storage_manager.js ├── classlist_polyfill.js ├── grid.js ├── keyboard_input_manager.js ├── html_actuator.js ├── i18n.js ├── game_manager.js └── addtohomescreen.min.js ├── Rakefile ├── .jshintrc ├── manifest.json ├── README.md ├── LICENSE.txt └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache/ 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/favicon.ico -------------------------------------------------------------------------------- /meta/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/meta/apple-touch-icon.png -------------------------------------------------------------------------------- /meta/launcher-icon-1-5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/meta/launcher-icon-1-5x.png -------------------------------------------------------------------------------- /meta/launcher-icon-1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/meta/launcher-icon-1x.png -------------------------------------------------------------------------------- /meta/launcher-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/meta/launcher-icon-2x.png -------------------------------------------------------------------------------- /meta/launcher-icon-3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/meta/launcher-icon-3x.png -------------------------------------------------------------------------------- /meta/launcher-icon-4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/meta/launcher-icon-4x.png -------------------------------------------------------------------------------- /meta/launcher-icon-0-75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/meta/launcher-icon-0-75x.png -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/style/fonts/ClearSans-Bold-webfont.eot -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/style/fonts/ClearSans-Bold-webfont.woff -------------------------------------------------------------------------------- /style/fonts/ClearSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/style/fonts/ClearSans-Light-webfont.eot -------------------------------------------------------------------------------- /meta/apple-touch-startup-image-640x920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/meta/apple-touch-startup-image-640x920.png -------------------------------------------------------------------------------- /style/fonts/ClearSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/style/fonts/ClearSans-Light-webfont.woff -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/style/fonts/ClearSans-Regular-webfont.eot -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/style/fonts/ClearSans-Regular-webfont.woff -------------------------------------------------------------------------------- /meta/apple-touch-startup-image-640x1096.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymfa/phd-2048/HEAD/meta/apple-touch-startup-image-640x1096.png -------------------------------------------------------------------------------- /js/bind_polyfill.js: -------------------------------------------------------------------------------- 1 | Function.prototype.bind = Function.prototype.bind || function (target) { 2 | var self = this; 3 | return function (args) { 4 | if (!(args instanceof Array)) { 5 | args = [args]; 6 | } 7 | self.apply(target, args); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "date" 2 | 3 | namespace :appcache do 4 | desc "update the date in the appcache file (in the gh-pages branch)" 5 | task :update do 6 | appcache = File.read("cache.appcache") 7 | updated = "# Updated: #{DateTime.now}" 8 | 9 | File.write("cache.appcache", appcache.sub(/^# Updated:.*$/, updated)) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "indent": 2, 4 | "maxlen": 80, 5 | "freeze": true, 6 | "camelcase": true, 7 | "unused": true, 8 | "eqnull": true, 9 | "proto": true, 10 | "supernew": true, 11 | "noyield": true, 12 | "evil": true, 13 | "node": true, 14 | "boss": true, 15 | "expr": true, 16 | "loopfunc": true, 17 | "white": true, 18 | "maxdepth": 4 19 | } 20 | -------------------------------------------------------------------------------- /js/tile.js: -------------------------------------------------------------------------------- 1 | function Tile(position, value) { 2 | this.x = position.x; 3 | this.y = position.y; 4 | this.value = value; 5 | 6 | this.previousPosition = null; 7 | this.mergedFrom = null; // Tracks tiles that merged together 8 | this.benefitedFrom = null; // TS of the relationship it benefited from 9 | } 10 | 11 | Tile.prototype.savePosition = function () { 12 | this.previousPosition = { x: this.x, y: this.y }; 13 | }; 14 | 15 | Tile.prototype.updatePosition = function (position) { 16 | this.x = position.x; 17 | this.y = position.y; 18 | }; 19 | 20 | Tile.prototype.serialize = function () { 21 | return { 22 | position: { 23 | x: this.x, 24 | y: this.y 25 | }, 26 | value: this.value, 27 | benefitedFrom: this.benefitedFrom 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /js/application.js: -------------------------------------------------------------------------------- 1 | // Wait till the browser is ready to render the game (avoids glitches) 2 | window.requestAnimationFrame(function () { 3 | window.game = new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager); 4 | var lang_pref = window.game.storageManager.storage.getItem('lang'); 5 | if(lang_pref == 'en') play_in_english(); 6 | else if(lang_pref == 'zh') play_in_chinese(); 7 | else { 8 | var nav_langs = navigator.languages; 9 | var require_english = true; 10 | if(nav_langs){ 11 | for(var i=0; i 1 { 9 | @for $i from 2 through $exponent { 10 | $value: $value * $base; } } 11 | // negitive intergers get divided. A number divided by itself is 1 12 | @if $exponent < 1 { 13 | @for $i from 0 through -$exponent { 14 | $value: $value / $base; } } 15 | // return the last value written 16 | @return $value; 17 | } 18 | 19 | @function pow($base, $exponent) { 20 | @return exponent($base, $exponent); 21 | } 22 | 23 | // Transition mixins 24 | @mixin transition($args...) { 25 | -webkit-transition: $args; 26 | -moz-transition: $args; 27 | transition: $args; 28 | } 29 | 30 | @mixin transition-property($args...) { 31 | -webkit-transition-property: $args; 32 | -moz-transition-property: $args; 33 | transition-property: $args; 34 | } 35 | 36 | @mixin animation($args...) { 37 | -webkit-animation: $args; 38 | -moz-animation: $args; 39 | animation: $args; 40 | } 41 | 42 | @mixin animation-fill-mode($args...) { 43 | -webkit-animation-fill-mode: $args; 44 | -moz-animation-fill-mode: $args; 45 | animation-fill-mode: $args; 46 | } 47 | 48 | @mixin transform($args...) { 49 | -webkit-transform: $args; 50 | -moz-transform: $args; 51 | -ms-transform: $args; 52 | transform: $args; 53 | } 54 | 55 | // Keyframe animations 56 | @mixin keyframes($animation-name) { 57 | @-webkit-keyframes $animation-name { 58 | @content; 59 | } 60 | @-moz-keyframes $animation-name { 61 | @content; 62 | } 63 | @keyframes $animation-name { 64 | @content; 65 | } 66 | } 67 | 68 | // Media queries 69 | @mixin smaller($width) { 70 | @media screen and (max-width: $width) { 71 | @content; 72 | } 73 | } 74 | 75 | // Clearfix 76 | @mixin clearfix { 77 | &:after { 78 | content: ""; 79 | display: block; 80 | clear: both; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /js/classlist_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | if (typeof window.Element === "undefined" || 3 | "classList" in document.documentElement) { 4 | return; 5 | } 6 | 7 | var prototype = Array.prototype, 8 | push = prototype.push, 9 | splice = prototype.splice, 10 | join = prototype.join; 11 | 12 | function DOMTokenList(el) { 13 | this.el = el; 14 | // The className needs to be trimmed and split on whitespace 15 | // to retrieve a list of classes. 16 | var classes = el.className.replace(/^\s+|\s+$/g, '').split(/\s+/); 17 | for (var i = 0; i < classes.length; i++) { 18 | push.call(this, classes[i]); 19 | } 20 | } 21 | 22 | DOMTokenList.prototype = { 23 | add: function (token) { 24 | if (this.contains(token)) return; 25 | push.call(this, token); 26 | this.el.className = this.toString(); 27 | }, 28 | contains: function (token) { 29 | return this.el.className.indexOf(token) != -1; 30 | }, 31 | item: function (index) { 32 | return this[index] || null; 33 | }, 34 | remove: function (token) { 35 | if (!this.contains(token)) return; 36 | for (var i = 0; i < this.length; i++) { 37 | if (this[i] == token) break; 38 | } 39 | splice.call(this, i, 1); 40 | this.el.className = this.toString(); 41 | }, 42 | toString: function () { 43 | return join.call(this, ' '); 44 | }, 45 | toggle: function (token) { 46 | if (!this.contains(token)) { 47 | this.add(token); 48 | } else { 49 | this.remove(token); 50 | } 51 | 52 | return this.contains(token); 53 | } 54 | }; 55 | 56 | window.DOMTokenList = DOMTokenList; 57 | 58 | function defineElementGetter(obj, prop, getter) { 59 | if (Object.defineProperty) { 60 | Object.defineProperty(obj, prop, { 61 | get: getter 62 | }); 63 | } else { 64 | obj.__defineGetter__(prop, getter); 65 | } 66 | } 67 | 68 | defineElementGetter(HTMLElement.prototype, 'classList', function () { 69 | return new DOMTokenList(this); 70 | }); 71 | })(); 72 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | PhD 2048: a sliding puzzle 2 | Copyright (C) 2017 Yimai Fang 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | 17 | 18 | ======= 19 | 20 | Component: 2048 21 | 22 | The MIT License (MIT) 23 | Copyright (c) 2014 Gabriele Cirulli 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining a copy 26 | of this software and associated documentation files (the "Software"), to deal 27 | in the Software without restriction, including without limitation the rights 28 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | copies of the Software, and to permit persons to whom the Software is 30 | furnished to do so, subject to the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be included in 33 | all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 41 | THE SOFTWARE. 42 | 43 | 44 | ======= 45 | 46 | Component: Add to Homescreen 47 | 48 | Copyright (c) 2014 Matteo Spinelli, http://cubiq.org/ 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 51 | 52 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 55 | -------------------------------------------------------------------------------- /js/grid.js: -------------------------------------------------------------------------------- 1 | function Grid(size, previousState) { 2 | this.size = size; 3 | this.cells = previousState ? this.fromState(previousState) : this.empty(); 4 | } 5 | 6 | // Build a grid of the specified size 7 | Grid.prototype.empty = function () { 8 | var cells = []; 9 | 10 | for (var x = 0; x < this.size; x++) { 11 | var row = cells[x] = []; 12 | 13 | for (var y = 0; y < this.size; y++) { 14 | row.push(null); 15 | } 16 | } 17 | 18 | return cells; 19 | }; 20 | 21 | Grid.prototype.fromState = function (state) { 22 | var cells = []; 23 | 24 | for (var x = 0; x < this.size; x++) { 25 | var row = cells[x] = []; 26 | 27 | for (var y = 0; y < this.size; y++) { 28 | var tileInfo = state[x][y]; 29 | if (tileInfo){ 30 | var tile = new Tile(tileInfo.position, tileInfo.value); 31 | tile.benefitedFrom = tileInfo.benefitedFrom; 32 | } 33 | else var tile = null; 34 | row.push(tile); 35 | } 36 | } 37 | 38 | return cells; 39 | }; 40 | 41 | // Find the first available random position 42 | Grid.prototype.randomAvailableCell = function () { 43 | var cells = this.availableCells(); 44 | 45 | if (cells.length) { 46 | return cells[Math.floor(Math.random() * cells.length)]; 47 | } 48 | }; 49 | 50 | Grid.prototype.availableCells = function () { 51 | var cells = []; 52 | 53 | this.eachCell(function (x, y, tile) { 54 | if (!tile) { 55 | cells.push({ x: x, y: y }); 56 | } 57 | }); 58 | 59 | return cells; 60 | }; 61 | 62 | // Call callback for every cell 63 | Grid.prototype.eachCell = function (callback) { 64 | for (var x = 0; x < this.size; x++) { 65 | for (var y = 0; y < this.size; y++) { 66 | callback(x, y, this.cells[x][y]); 67 | } 68 | } 69 | }; 70 | 71 | // Check if there are any cells available 72 | Grid.prototype.cellsAvailable = function () { 73 | return !!this.availableCells().length; 74 | }; 75 | 76 | // Check if the specified cell is taken 77 | Grid.prototype.cellAvailable = function (cell) { 78 | return !this.cellOccupied(cell); 79 | }; 80 | 81 | Grid.prototype.cellOccupied = function (cell) { 82 | return !!this.cellContent(cell); 83 | }; 84 | 85 | Grid.prototype.cellContent = function (cell) { 86 | if (this.withinBounds(cell)) { 87 | return this.cells[cell.x][cell.y]; 88 | } else { 89 | return null; 90 | } 91 | }; 92 | 93 | // Inserts a tile at its position 94 | Grid.prototype.insertTile = function (tile) { 95 | this.cells[tile.x][tile.y] = tile; 96 | }; 97 | 98 | Grid.prototype.removeTile = function (tile) { 99 | this.cells[tile.x][tile.y] = null; 100 | }; 101 | 102 | Grid.prototype.withinBounds = function (position) { 103 | return position.x >= 0 && position.x < this.size && 104 | position.y >= 0 && position.y < this.size; 105 | }; 106 | 107 | Grid.prototype.serialize = function () { 108 | var cellState = []; 109 | 110 | for (var x = 0; x < this.size; x++) { 111 | var row = cellState[x] = []; 112 | 113 | for (var y = 0; y < this.size; y++) { 114 | row.push(this.cells[x][y] ? this.cells[x][y].serialize() : null); 115 | } 116 | } 117 | 118 | return { 119 | size: this.size, 120 | cells: cellState 121 | }; 122 | }; 123 | 124 | Grid.prototype.clearRelationship = function (createGarbage) { 125 | var changes = 0; 126 | for(var col = 0; col < this.size; col++){ 127 | for(var row = 0; row < this.size; row++){ 128 | var tile = this.cells[col][row]; 129 | if(tile && tile.value == 1){ 130 | if(createGarbage) tile.value = 0; 131 | else this.removeTile(tile); 132 | changes++; 133 | } 134 | } 135 | } 136 | return changes; 137 | } -------------------------------------------------------------------------------- /js/keyboard_input_manager.js: -------------------------------------------------------------------------------- 1 | function KeyboardInputManager() { 2 | this.events = {}; 3 | 4 | if (window.navigator.msPointerEnabled) { 5 | //Internet Explorer 10 style 6 | this.eventTouchstart = "MSPointerDown"; 7 | this.eventTouchmove = "MSPointerMove"; 8 | this.eventTouchend = "MSPointerUp"; 9 | } else { 10 | this.eventTouchstart = "touchstart"; 11 | this.eventTouchmove = "touchmove"; 12 | this.eventTouchend = "touchend"; 13 | } 14 | 15 | this.listen(); 16 | } 17 | 18 | KeyboardInputManager.prototype.on = function (event, callback) { 19 | if (!this.events[event]) { 20 | this.events[event] = []; 21 | } 22 | this.events[event].push(callback); 23 | }; 24 | 25 | KeyboardInputManager.prototype.emit = function (event, data) { 26 | var callbacks = this.events[event]; 27 | if (callbacks) { 28 | callbacks.forEach(function (callback) { 29 | callback(data); 30 | }); 31 | } 32 | }; 33 | 34 | KeyboardInputManager.prototype.listen = function () { 35 | var self = this; 36 | 37 | var map = { 38 | 38: 0, // Up 39 | 39: 1, // Right 40 | 40: 2, // Down 41 | 37: 3, // Left 42 | 75: 0, // Vim up 43 | 76: 1, // Vim right 44 | 74: 2, // Vim down 45 | 72: 3, // Vim left 46 | 87: 0, // W 47 | 68: 1, // D 48 | 83: 2, // S 49 | 65: 3 // A 50 | }; 51 | 52 | // Respond to direction keys 53 | document.addEventListener("keydown", function (event) { 54 | var modifiers = event.altKey || event.ctrlKey || event.metaKey || 55 | event.shiftKey; 56 | var mapped = map[event.which]; 57 | 58 | if (!modifiers) { 59 | if (mapped !== undefined) { 60 | event.preventDefault(); 61 | self.emit("move", mapped); 62 | } 63 | } 64 | 65 | // R key restarts the game 66 | if (!modifiers && event.which === 82) { 67 | self.restart.call(self, event); 68 | } 69 | }); 70 | 71 | // Respond to button presses 72 | this.bindButtonPress(".retry-button", this.restart); 73 | this.bindButtonPress(".restart-button", this.restart); 74 | this.bindButtonPress(".keep-playing-button", this.keepPlaying); 75 | 76 | // Respond to swipe events 77 | var touchStartClientX, touchStartClientY; 78 | var gameContainer = document.querySelector(".game-container"); 79 | 80 | gameContainer.addEventListener(this.eventTouchstart, function (event) { 81 | if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || 82 | event.targetTouches.length > 1) { 83 | return; // Ignore if touching with more than 1 finger 84 | } 85 | 86 | if (window.navigator.msPointerEnabled) { 87 | touchStartClientX = event.pageX; 88 | touchStartClientY = event.pageY; 89 | } else { 90 | touchStartClientX = event.touches[0].clientX; 91 | touchStartClientY = event.touches[0].clientY; 92 | } 93 | 94 | event.preventDefault(); 95 | }); 96 | 97 | gameContainer.addEventListener(this.eventTouchmove, function (event) { 98 | event.preventDefault(); 99 | }); 100 | 101 | gameContainer.addEventListener(this.eventTouchend, function (event) { 102 | if ((!window.navigator.msPointerEnabled && event.touches.length > 0) || 103 | event.targetTouches.length > 0) { 104 | return; // Ignore if still touching with one or more fingers 105 | } 106 | 107 | var touchEndClientX, touchEndClientY; 108 | 109 | if (window.navigator.msPointerEnabled) { 110 | touchEndClientX = event.pageX; 111 | touchEndClientY = event.pageY; 112 | } else { 113 | touchEndClientX = event.changedTouches[0].clientX; 114 | touchEndClientY = event.changedTouches[0].clientY; 115 | } 116 | 117 | var dx = touchEndClientX - touchStartClientX; 118 | var absDx = Math.abs(dx); 119 | 120 | var dy = touchEndClientY - touchStartClientY; 121 | var absDy = Math.abs(dy); 122 | 123 | if (Math.max(absDx, absDy) > 10) { 124 | // (right : left) : (down : up) 125 | self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); 126 | } 127 | }); 128 | }; 129 | 130 | KeyboardInputManager.prototype.restart = function (event) { 131 | event.preventDefault(); 132 | this.emit("restart"); 133 | }; 134 | 135 | KeyboardInputManager.prototype.keepPlaying = function (event) { 136 | event.preventDefault(); 137 | this.emit("keepPlaying"); 138 | }; 139 | 140 | KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) { 141 | var button = document.querySelector(selector); 142 | button.addEventListener("click", fn.bind(this)); 143 | button.addEventListener(this.eventTouchend, fn.bind(this)); 144 | }; 145 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PhD 2048: Move the bricks 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 |

PhD

28 |
29 |
0
30 |
0
31 |
32 |
33 | 34 |
35 |

Move the bricks to complete your PhD.

36 | Drop out 37 |
38 | 39 |
40 |
41 |

42 |
43 | Keep going 44 | Try again 45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 75 |
76 | 77 |
78 |
79 | 80 |

81 | How to play: Use your arrow keys to move the bricks. When two bricks of the same type touch, they merge into one!
However, your ideas and experiments may not always work — they may produce the sticky garbage, which is resistant to moves. Two garbage bricks vanish when they touch. You will stop producing garbage after getting a paper (except for one more piece to help you eliminate any existing garbage).
A relationship upgrades any brick it touches for the first time. The brick shows the number of times you have benefited from it. When the 10-sec relationship ends, it will become a break-up (or garbage if you didn't use it), which downgrades bricks until you have repaid the benefits. 82 |

83 |
84 |
85 | Fork this game on GitHub! 86 |
87 | 88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /js/html_actuator.js: -------------------------------------------------------------------------------- 1 | function HTMLActuator() { 2 | this.tileContainer = document.querySelector(".tile-container"); 3 | this.scoreContainer = document.querySelector(".score-container"); 4 | this.bestContainer = document.querySelector(".best-container"); 5 | this.messageContainer = document.querySelector(".game-message"); 6 | this.progressBar = document.getElementById("progress"); 7 | this.titleBar = document.getElementById("title"); 8 | this.statusBar = document.querySelector('.game-intro'); 9 | 10 | this.score = 0; 11 | } 12 | 13 | HTMLActuator.prototype.actuate = function (grid, metadata) { 14 | var self = this; 15 | 16 | window.requestAnimationFrame(function () { 17 | self.clearContainer(self.tileContainer); 18 | 19 | grid.cells.forEach(function (column) { 20 | column.forEach(function (cell) { 21 | if (cell) { 22 | self.addTile(cell); 23 | } 24 | }); 25 | }); 26 | 27 | self.updateScore(metadata.score); 28 | self.updateBestScore(metadata.bestScore); 29 | 30 | if (metadata.terminated) { 31 | if (metadata.over) { 32 | self.message(false); // The game has ended 33 | } else if (metadata.won) { 34 | self.message(true); // You can continue playing 35 | } 36 | } 37 | 38 | }); 39 | }; 40 | 41 | // Continues the game (both restart and keep playing) 42 | HTMLActuator.prototype.continueGame = function () { 43 | this.clearMessage(); 44 | }; 45 | 46 | HTMLActuator.prototype.clearContainer = function (container) { 47 | while (container.firstChild) { 48 | container.removeChild(container.firstChild); 49 | } 50 | }; 51 | 52 | var val2caption = function(val){ 53 | if(val <= 0) return caption_garbage; 54 | if(val == 1){ 55 | var caption = ""; 56 | if(window.game.relTime) caption += captions_rel[0]; 57 | else caption += captions_rel[1]; 58 | caption += "
" + window.game.karma + "
"; 59 | return caption; 60 | } 61 | var idx = -1; 62 | var n = 1; 63 | while(n < val) { 64 | n <<= 1; 65 | idx++; 66 | } 67 | if(idx >= 0 && idx < captions.length) 68 | return captions[idx]; 69 | else 70 | return val; 71 | }; 72 | 73 | HTMLActuator.prototype.addTile = function (tile) { 74 | var self = this; 75 | 76 | var wrapper = document.createElement("div"); 77 | var inner = document.createElement("div"); 78 | var position = tile.previousPosition || { x: tile.x, y: tile.y }; 79 | var positionClass = this.positionClass(position); 80 | 81 | // We can't use classlist because it somehow glitches when replacing classes 82 | var classes = ["tile", "tile-" + tile.value, positionClass]; 83 | 84 | if (tile.value > 2048) classes.push("tile-super"); 85 | 86 | this.applyClasses(wrapper, classes); 87 | 88 | inner.classList.add("tile-inner"); 89 | inner.innerHTML = val2caption(tile.value); 90 | 91 | if (tile.previousPosition) { 92 | // Make sure that the tile gets rendered in the previous position first 93 | window.requestAnimationFrame(function () { 94 | classes[2] = self.positionClass({ x: tile.x, y: tile.y }); 95 | self.applyClasses(wrapper, classes); // Update the position 96 | }); 97 | } else if (tile.mergedFrom) { 98 | classes.push("tile-merged"); 99 | this.applyClasses(wrapper, classes); 100 | 101 | // Render the tiles that merged 102 | tile.mergedFrom.forEach(function (merged) { 103 | self.addTile(merged); 104 | }); 105 | } else { 106 | classes.push("tile-new"); 107 | this.applyClasses(wrapper, classes); 108 | } 109 | 110 | // Add the inner part of the tile to the wrapper 111 | wrapper.appendChild(inner); 112 | 113 | // Put the tile on the board 114 | this.tileContainer.appendChild(wrapper); 115 | }; 116 | 117 | HTMLActuator.prototype.applyClasses = function (element, classes) { 118 | element.setAttribute("class", classes.join(" ")); 119 | }; 120 | 121 | HTMLActuator.prototype.normalizePosition = function (position) { 122 | return { x: position.x + 1, y: position.y + 1 }; 123 | }; 124 | 125 | HTMLActuator.prototype.positionClass = function (position) { 126 | position = this.normalizePosition(position); 127 | return "tile-position-" + position.x + "-" + position.y; 128 | }; 129 | 130 | HTMLActuator.prototype.updateScore = function (score) { 131 | this.clearContainer(this.scoreContainer); 132 | 133 | var difference = score - this.score; 134 | this.score = score; 135 | 136 | this.scoreContainer.textContent = this.score; 137 | 138 | if (difference > 0) { 139 | var addition = document.createElement("div"); 140 | addition.classList.add("score-addition"); 141 | addition.textContent = "-" + difference; 142 | 143 | this.scoreContainer.appendChild(addition); 144 | } 145 | }; 146 | 147 | HTMLActuator.prototype.updateBestScore = function (bestScore) { 148 | this.bestContainer.textContent = bestScore; 149 | }; 150 | 151 | HTMLActuator.prototype.message = function (ended) { 152 | var type = ended ? "game-won" : "game-over"; 153 | var message = window.game.won ? result_msg + "PhD!" : result_msg + "CPGS!"; 154 | if(!window.game.won) { 155 | if(window.game.maxTile >= 1024) message = "One step away!"; 156 | else if(window.game.maxTile >= 512) message = "Not bad!"; 157 | } 158 | else if(window.game.maxTile > 2048) { 159 | message = result_msg + val2caption(window.game.maxTile) + "!"; 160 | } 161 | 162 | this.messageContainer.classList.add(type); 163 | this.messageContainer.getElementsByTagName("p")[0].innerHTML = message; 164 | }; 165 | 166 | HTMLActuator.prototype.clearMessage = function () { 167 | // IE only takes one value to remove at a time. 168 | this.messageContainer.classList.remove("game-won"); 169 | this.messageContainer.classList.remove("game-over"); 170 | }; 171 | 172 | HTMLActuator.prototype.refreshRel = function (remainingTime) { 173 | if(remainingTime > 0){ 174 | this.titleBar.textContent = game_alt_title; 175 | this.statusBar.textContent = "Your relationship will last for "+remainingTime+"s."; 176 | this.progressBar.textContent = game_alt_title; 177 | this.progressBar.style.display = ""; 178 | if(window.innerWidth < 520) 179 | this.progressBar.style.width = Math.round(100*remainingTime/window.game.relDuration) + "px"; 180 | else 181 | this.progressBar.style.width = Math.round(200*remainingTime/window.game.relDuration) + "px"; 182 | } 183 | else{ 184 | this.titleBar.textContent = game_title; 185 | this.statusBar.textContent = "Move the bricks to complete your PhD."; 186 | this.progressBar.textContent = ""; 187 | this.progressBar.style.display = "none"; 188 | this.progressBar.style.width = "0"; 189 | var rels = document.querySelectorAll('.rel'); 190 | for(var i=0; iDeep Learning", 6 | "See Supervisor", 7 | "Experiment", "Paper", 8 | "Conference", "Viva", "PhD", 9 | "Postdoc", 10 | "Lecturer", "Reader", "Prof."]; 11 | captions_rel = ["Relationship", 12 | "Break-up"]; 13 | } 14 | else{ 15 | captions = ["Coffee", "Panini", 16 | "Idea", "Code", 17 | "Deep Learning", 18 | "See Supervisor", 19 | "Experiment", "Paper", 20 | "Conference", "Viva", "PhD", 21 | "Postdoc", 22 | "Lecturer", "Reader", "Prof."]; 23 | captions_rel = ["Relationship", 24 | "Break-up"]; 25 | } 26 | } 27 | 28 | var span_en; 29 | 30 | function create_switch_en(){ 31 | span_en = document.createElement('div'); 32 | span_en.style.position = "absolute"; 33 | span_en.style.top = "0"; 34 | if(window.innerWidth < 520) 35 | span_en.style.fontSize = "10px"; 36 | else 37 | span_en.style.fontSize = "small"; 38 | span_en.style.backgroundColor = "#8f7a66"; 39 | span_en.style.borderRadius = "0 0 3px 3px"; 40 | span_en.style.padding = "3px 10px"; 41 | span_en.style.color = "white"; 42 | span_en.style.cursor = "pointer"; 43 | span_en.onclick = play_in_english; 44 | span_en.textContent = "🇬🇧 Switch to English"; 45 | var container = document.querySelector('.container'); 46 | container.insertBefore(span_en, container.firstChild); 47 | } 48 | 49 | var span_zh; 50 | 51 | function create_switch_zh(){ 52 | span_zh = document.createElement('div'); 53 | span_zh.style.position = "absolute"; 54 | span_zh.style.top = "0"; 55 | if(window.innerWidth < 520) 56 | span_zh.style.fontSize = "10px"; 57 | else 58 | span_zh.style.fontSize = "small"; 59 | span_zh.style.backgroundColor = "#8f7a66"; 60 | span_zh.style.borderRadius = "0 0 3px 3px"; 61 | span_zh.style.padding = "3px 10px"; 62 | span_zh.style.color = "white"; 63 | span_zh.style.cursor = "pointer"; 64 | span_zh.onclick = play_in_chinese; 65 | span_zh.textContent = "中文版"; 66 | var container = document.querySelector('.container'); 67 | container.insertBefore(span_zh, container.firstChild); 68 | } 69 | 70 | function play_in_english(){ 71 | update_captions(); 72 | window.addEventListener('resize', update_captions, true); 73 | 74 | caption_garbage = "Garbage"; 75 | window.game.actuate(); 76 | 77 | game_title = "PhD"; 78 | game_alt_title = "Love"; 79 | result_msg = "You got a "; 80 | var titleElem = document.getElementById('title'); 81 | if(titleElem.textContent != "Love") titleElem.textContent = game_title; 82 | document.querySelector('.restart-button').textContent = "Drop out"; 83 | document.querySelector('.retry-button').textContent = "Try again"; 84 | document.querySelector('.game-explanation').innerHTML = "How to play: Use your arrow keys to move the bricks. When two bricks of the same type touch, they merge into one!
However, your ideas and experiments may not always work — they may produce the sticky garbage, which is resistant to moves. Two garbage bricks vanish when they touch. You will stop producing garbage after getting a paper (except for one more piece to help you eliminate any existing garbage).
A relationship upgrades any brick it touches for the first time. The brick shows the number of times you have benefited from it. When the 10-sec relationship ends, it will become a break-up (or garbage if you didn't use it), which downgrades bricks until you have repaid the benefits."; 85 | 86 | if(span_en) span_en.parentNode.removeChild(span_en); 87 | create_switch_zh(); 88 | window.game.storageManager.storage.setItem('lang', 'en'); 89 | } 90 | 91 | var zh_var = null; 92 | 93 | function determine_zh_var(){ 94 | if(zh_var) return zh_var; 95 | var hant_locales = ['zh-hant', 'zh-tw', 'zh-hk', 'zh-mo']; 96 | var nav_langs = navigator.languages; 97 | var hant_fallback = false; 98 | if(nav_langs){ 99 | for(var i=0; i= 0 ? "hant" : "hans"; 103 | break; 104 | } 105 | else if(nav_lang.startsWith('ja-') || nav_lang.startsWith('ko-')) hant_fallback = true; 106 | } 107 | } 108 | else{ 109 | var nav_lang = navigator.language || navigator.userLanguage; 110 | if(nav_lang){ 111 | nav_lang = nav_lang.toLowerCase(); 112 | if(nav_lang.startsWith('zh-')) 113 | zh_var = hant_locales.indexOf(nav_lang) >= 0 ? "hant" : "hans"; 114 | else if(nav_lang.startsWith('ja-') || nav_lang.startsWith('ko-')) hant_fallback = true; 115 | } 116 | } 117 | if(!zh_var) zh_var = hant_fallback ? "hant" : "hans"; 118 | return zh_var; 119 | } 120 | 121 | function use_simplified(){ 122 | captions = ["Coffee", "Panini", 123 | "想法", "代码", "深度
学習
", "见导师", 124 | "实验", "Paper", "会议", "答辩", "PhD", 125 | "薄厚", "僵尸", "Reader", "叫兽"]; 126 | captions_rel = ["恋爱", "分手"]; 127 | caption_garbage = "垃圾"; 128 | game_alt_title = "爱"; 129 | window.game.actuate(); 130 | 131 | document.querySelector('.restart-button').textContent = "退学"; 132 | document.querySelector('.retry-button').textContent = "善"; 133 | document.querySelector('.game-explanation').innerHTML = "玩法: 使用方向键搬砖. 当两块相同的砖碰在一起时, 它们会组成一块更好的砖!
但是, 你的想法和实验也可能只是产生垃圾. 黏着的垃圾会阻碍砖块的移动, 直到被别的垃圾击中而消失. 你得到 paper 以后便不会再产生垃圾, 最多再来一块帮你清除别的垃圾.
恋爱砖触碰任何砖都能使其升级, 但一块砖只可享受一次. 恋爱砖上会显示你使用它的次数; 10 秒后它会变成分手砖, 触碰任何砖都能使其降级, 以此来偿还之前使用的次数."; 134 | } 135 | 136 | function use_traditional(){ 137 | captions = ["Coffee", "Panini", 138 | "想法", "原始碼", "深度
學習
", "見導師", 139 | "實驗", "Paper", "會議", "答辯", "PhD", 140 | "薄厚", "老屍", "Reader", "叫獸"]; 141 | captions_rel = ["戀愛", "分手"]; 142 | caption_garbage = "垃圾"; 143 | game_alt_title = "愛"; 144 | window.game.actuate(); 145 | 146 | document.querySelector('.restart-button').textContent = "退學"; 147 | document.querySelector('.retry-button').textContent = "善"; 148 | document.querySelector('.game-explanation').innerHTML = "玩法:用方向鍵搬磚。當兩塊相同的磚碰在一起時,它們會併成一塊更好的磚
但是,你的想法和實驗可能只是產生垃圾而已。黏在地上的垃圾會阻礙磚塊移動,直到被別的垃圾擊中而消失。你得到 paper 以後便不會再產生垃圾,最多再出一塊幫你清除場上剩下的垃圾。
戀愛磚觸碰任何磚都能使其升級,但一塊磚只得升級一次。戀愛磚上會顯示你用它的次數。10 秒後它會變成分手磚,觸碰任何磚都能使其降級,以此來償還之前使用的次數。"; 149 | 150 | document.body.style.fontFamily = '"Clear Sans", "Helvetica Neue", Arial, "Hiragino Sans CNS", "PingFang TC", "Microsoft JhengHei", "Source Han Sans TC", "Noto Sans CJK TC", sans-serif'; 151 | } 152 | 153 | function play_in_chinese(){ 154 | window.removeEventListener('resize', update_captions, true); 155 | game_title = "磗士"; 156 | result_msg = "你得到了"; 157 | var titleElem = document.getElementById('title'); 158 | if(titleElem.textContent != "Love") titleElem.textContent = game_title; 159 | 160 | if(determine_zh_var() == 'hant') use_traditional(); 161 | else use_simplified(); 162 | 163 | if(span_zh) span_zh.parentNode.removeChild(span_zh); 164 | create_switch_en(); 165 | window.game.storageManager.storage.setItem('lang', 'zh'); 166 | } 167 | -------------------------------------------------------------------------------- /style/addtohomescreen.css: -------------------------------------------------------------------------------- 1 | .ath-viewport * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | .ath-viewport { 8 | position: relative; 9 | z-index: 2147483641; 10 | pointer-events: none; 11 | 12 | -webkit-tap-highlight-color: rgba(0,0,0,0); 13 | -webkit-touch-callout: none; 14 | -webkit-user-select: none; 15 | -moz-user-select: none; 16 | -ms-user-select: none; 17 | user-select: none; 18 | -webkit-text-size-adjust: none; 19 | -moz-text-size-adjust: none; 20 | -ms-text-size-adjust: none; 21 | -o-text-size-adjust: none; 22 | text-size-adjust: none; 23 | } 24 | 25 | .ath-modal { 26 | pointer-events: auto !important; 27 | background: rgba(0,0,0,0.6); 28 | } 29 | 30 | .ath-mandatory { 31 | background: #000; 32 | } 33 | 34 | .ath-container { 35 | pointer-events: auto !important; 36 | position: absolute; 37 | z-index: 2147483641; 38 | padding: 0.7em 0.6em; 39 | width: 18em; 40 | 41 | background: #eee; 42 | background-size: 100% auto; 43 | 44 | box-shadow: 0 0.2em 0 #d1d1d1; 45 | 46 | font-family: sans-serif; 47 | font-size: 15px; 48 | line-height: 1.5em; 49 | text-align: center; 50 | } 51 | 52 | .ath-container small { 53 | font-size: 0.8em; 54 | line-height: 1.3em; 55 | display: block; 56 | margin-top: 0.5em; 57 | } 58 | 59 | .ath-ios.ath-phone { 60 | bottom: 1.8em; 61 | left: 50%; 62 | margin-left: -9em; 63 | } 64 | 65 | .ath-ios6.ath-tablet { 66 | left: 5em; 67 | top: 1.8em; 68 | } 69 | 70 | .ath-ios7.ath-tablet { 71 | left: 0.7em; 72 | top: 1.8em; 73 | } 74 | 75 | .ath-ios8.ath-tablet, 76 | .ath-ios9.ath-tablet, 77 | .ath-ios10.ath-tablet{ 78 | right: 0.4em; 79 | top: 1.8em; 80 | } 81 | 82 | .ath-android { 83 | bottom: 1.8em; 84 | left: 50%; 85 | margin-left: -9em; 86 | } 87 | 88 | /* close icon */ 89 | .ath-container:before { 90 | content: ''; 91 | position: relative; 92 | display: block; 93 | float: right; 94 | margin: -0.7em -0.6em 0 0.5em; 95 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAACECAMAAABmmnOVAAAAdVBMVEUAAAA5OTkzMzM7Ozs3NzdBQUFAQEA/Pz8+Pj5BQUFAQEA/Pz8+Pj5BQUFAQEA/Pz9BQUE+Pj4/Pz8/Pz8+Pj4/Pz8/Pz8/Pz8+Pj4/Pz8+Pj4/Pz8/Pz8/Pz8/Pz8/Pz8+Pj4/Pz8/Pz8/Pz8/Pz9AQEA/Pz+fdCaPAAAAJnRSTlMACQoNDjM4OTo7PEFCQ0RFS6ytsbS1tru8vcTFxu7x8vX19vf4+C5yomAAAAJESURBVHgBvdzLTsJAGEfxr4C2KBcVkQsIDsK8/yPaqIsPzVlyzrKrX/5p0kkXEz81L23otc9NpIbbWia2YVLqdnhlqFlhGWpSDHe1aopsSIpRb8gK0dC3G30b9rVmhWZIimTICsvQtx/FsuYOrWHoDjX3Gu31gzJxdki934WrAIOsAIOsAIOiAMPhPsJTgKGN0BVsYIVsYIVpYIVpYIVpYIVpYIVpYIVpYIVpYIVlAIVgEBRs8BRs8BRs8BRs8BRs8BRs8BRs8BRTNmgKNngKNngKNngKNngKNhiKGxgiOlZoBlaYBlaYBlaYBlaYBlaYBlaYBlaYBlZIBlBMfQMrVAMr2KAqBENSHFHhGEABhi5CV6gGUKgGUKgGUKgGUFwuqgEUvoEVsoEVpoEUpgEUggF+gKTKY+h1fxSlC7/Z+RrxOQ3fcEoAPPHZBlaYBlaYBlaYBlZYBlYIhvLBCstw7PgM7hkiWOEZWGEaWGEaWGEaIsakEAysmHkGVpxmvoEVqoEVpoEVpoEVpoEVpoEVpoEVkoEVgkFQsEFSsEFQsGEcoSvY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnmbNAUT2c2WAo2eAo2eAo2eAo2eAo2eArNEPFACjZ4CjZ4CjZ4CjaIird/rBvFH6llNCvewdli1URWCIakSIZesUaDoFg36dKFWk9zCZDei3TtwmCj7pC22AwikiIZPEU29IpFNliKxa/hC9DFITjQPYhcAAAAAElFTkSuQmCC); 96 | background-color: rgba(255,255,255,0.8); 97 | background-size: 50%; 98 | background-repeat: no-repeat; 99 | background-position: 50%; 100 | width: 2.7em; 101 | height: 2.7em; 102 | text-align: center; 103 | overflow: hidden; 104 | color: #a33; 105 | z-index: 2147483642; 106 | } 107 | 108 | .ath-container.ath-icon:before { 109 | position: absolute; 110 | top: 0; 111 | right: 0; 112 | margin: 0; 113 | float: none; 114 | } 115 | 116 | .ath-mandatory .ath-container:before { 117 | display: none; 118 | } 119 | 120 | .ath-container.ath-android:before { 121 | float: left; 122 | margin: -0.7em 0.5em 0 -0.6em; 123 | } 124 | 125 | .ath-container.ath-android.ath-icon:before { 126 | position: absolute; 127 | right: auto; 128 | left: 0; 129 | margin: 0; 130 | float: none; 131 | } 132 | 133 | 134 | /* applied only if the application icon is shown */ 135 | .ath-container.ath-icon { 136 | 137 | } 138 | 139 | .ath-action-icon { 140 | display: inline-block; 141 | vertical-align: middle; 142 | background-position: 50%; 143 | background-repeat: no-repeat; 144 | text-indent: -9999em; 145 | overflow: hidden; 146 | } 147 | 148 | .ath-ios7 .ath-action-icon, 149 | .ath-ios8 .ath-action-icon, 150 | .ath-ios9 .ath-action-icon, 151 | .ath-ios10 .ath-action-icon{ 152 | width: 1.6em; 153 | height: 1.6em; 154 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAACtCAYAAAB7l7tOAAAF6UlEQVR4AezZWWxUZRiH8VcQEdxZEFFiUZBFUCIa1ABBDARDcCciYGKMqTEGww3SOcNSAwQTjOBiiIpEhRjAhRgXRC8MFxojEhAFZUGttVhaoSxlaW3n8W3yXZxm6vTrOMM5Q98n+V9MMu1pvl++uZhKuypghu49KaaTWGdZSYoVN6VD95nMpLNYZ9XNbdQR2od2k88O3Gm6Bh0t7H0p5Vwp2Ax3ajpu2tYbciFWwkTFO63DY6+JcI4USFaSyYpWp8N7SVZJKR3EinkBk9JxvZFXxhnZSjBaoWp1ZL0ES8WKYXMZp0AndORgy8WKFe5Yf1zvvSBWDEpys2LU6MjD5kmEWQlGKsJRHXlcqUSQVcItEnDEA6gAb7LhjvD9WO6yIEfICQI5A1nzGCYB1T4og5bBiFcyv2f6ujYhl4iVxwKG6qp8MK55HsqPwK0rMr9v/yEo3uCPrJstVh5KMER30Aeh31Ioq0FrHfjXw9CYghnrvYFTuqfEymFzGSwBlT4ARYr7u+K6GLmCVGvAGg2NMG0d/sgJnpScZLjXSkC5z8H3eQ72/k24Q8NfzvwFyK4qtuJSZKaubRPyE/K/Mtx+EvCHL+7uasId1t10w0scz/RzSzYzAfgKV30D3LPaG7lRkR8RK4tKKJKAMp+D7r0EfmmOe0x3m2itAc/ZxBjgAt1mXHWKPPkdb+QGSTJdrDaU5EoJ2OtzwD0WwY7KNNzbRfMFFg24WPdtGHnS221Cflgsj56hjwTs8TnY7oq7/QDhjutGicsb2AVcovsO18l6uPPNNiE/JFaGAq7Q7fY50G4LYVtz3FrdaNGyBXbIl+q24DqhyHes9EaulwR3SwtZs+ktAT/7HORliru1gnCndONFyx44Dfn7MPLYN7yR6yTJZAllJeguAT/4HOBFz8I3ZWm4E0TLFbBD7qn7EVdtHYx53R9ZN0ksrZRuErDN5+AuLIWvm+Oe1k0ULdfADrmX7idcR0/DyBXeyCdlLuMMOGCBz4F1ng+f7yFcve5e0fIFHELeiav6BAx70Rt5p0yhY3u/wR0kyarW/uX35b403PtFyzewQ75ctwtXzSkY8WqruHslSV8RscrL6TJ1bcvfWJ0/HzbtIdw/ugdFyzdwOOAq3T6fmzxwGQ3vbmO8iFioIWqYSsHMj9M/ljfuTsOdItoZBXYBfXX7cVXVwvXLm/8+fU3lcdCqdEMNGBbgUmRmfQISQKd5sGEn4VK6YtEiAXYBA3QVuA4q8hCHrDcafR1ul65jewfuovsCl7vJrNlOuEbdo6JFCuwCrtb9hqusBu56Cw4cI1y1briIWEBn3Ue0XKPuMdGiBg4H9NdV0HJ/6QZLOEPmPN0GmpfSPS5arIBdwHUtIFfoBsl/ZsgfhHCfFi2WwC5goO4AmvanbqBkzJA76tboZokWa2AXMEi3RTdAvDLkDqJFAhzB32xFD2wZsGXA0WfAlgFbBmwZsGXAlgFbBpzk04JaKb0iA9ZnF9x5SQAFtRKKIgPWZxfaeRmwAZ/BGbAB37eaG6MCbnq2Aed5czYyKirgpmcbsAHHZAZswN0Wwo7KeG1fFf2jAm56dtzOQ42yB+65mDhWFBUwUETMUiMDNmADbp/APRaTAh6I2bpGCNw1bufRZJQ1cPdF/NueHZsgDEBBGLbMGoIu4AZu5gLOZeEaYmEXeznF3jRPyEv4frgJvvJe3qTefY0AAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwb8rwADBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgz4/sz1Nia/9hizA7zgklwy3RYwYMBzBRjw4bPjxAbAAizAAtwgwAIswAIswAIMGDBgARZgARZgAS4FWIAFWIAFWIABAwYswAIswAIswIUAC7AAC7AACzBgwIAFWIAFWIAFuBBgARZgARZgAQYMGPApQ99ZCdgWtzqwATbABtgAG2DbnxNb7zbRimsMLMACrDf2wMWI/WasfQAAAABJRU5ErkJggg==); 155 | margin-top: -0.3em; 156 | background-size: auto 100%; 157 | } 158 | 159 | .ath-ios6 .ath-action-icon { 160 | width: 1.8em; 161 | height: 1.8em; 162 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAAB0CAQAAADAmnOnAAAAAnNCSVQICFXsRgQAAAAJcEhZcwAAWwEAAFsBAXkZiFwAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAF4klEQVR4Ae3a/a+XdR3H8ec5HM45HDmKICoVohkZsxESRRCzcZM/2JKkdGR5MrSkleA0Pd00O4u5IVuNM2yYc6XSzCExU4oUNRPCJFdMUAhsYZpUGhscOHA4N8/WZzsL6HBxvofvdV3fa3yer//gsV3vH659KHzncBsJxUYhDzOEhCKQbORs+ip2wzgM+wvj+P9i35qAGLaHGcQSgKSTrxBLABJppZpYApCspoFYApBsZjSxBCD5OxOJJQBJG1cQSwCSLpqJJQCJ3MvgCGTinuSMCJS8LZwfgZL3FtMiUPIOcU0ESl4PLRHoRPsJtREoeRsYGYGS9yrvo6RmpbLaigWSfzOdErLs6+bLUMFA0sF1+QF1cz1UNlBYK9V5AHXyWSgEkKyiIWOgGh829Ki1lLcaxjCVK7mJRSxjBY+zgRf/u9pXcMB7jhEZAg32EUP3O6hMKOP5Iq2sZQeHMZXt5KKMgOpcY+iHVnFyjeQKlrCBdsxge5ieAVC9vzLUelI8H+A7bKIHM10H81IGGuKvDf1ggDxVTKOV1zG3/Yia1ICG+ltD32MgNTKfP2HuW0VDKkCNrjfUTOm9i6XswwrZJkaVHeh0f2fodkrtfO6jAytqrzG+rEDDfVG1x1sprZEs5RBW4PZxeT+Bbrf5hPu9arfzKaU6WjiAFbseWvoF1GW/6vYGSmkyW7Dit4xB5QHq9Br6Xx2t9GAhtp6zkoHsfNp1J9wX6H+jeR4LtJc4LxGopZZyNpN/YcG2mw9nBTSPLizgOmjKAujGgvJID3ekD7QYi7nGzkvmQtpA38Vi7iJf0TedlC7QTVjMfcY2QyvSBPpUMW/PIBfbo9pls1XpAX2EdizeznStob3OJpQO0DB2YfE21q2GtnghpAm0Gou3T9tm6BGHQppA12HRVt17eboNlydNoLHsx2JtmL801OYcQmkC/QKLtQt9ydBW3wNpA30ci7Ur3WdolUMhbaBqNhf/8qQJ9Hkszs5wjaH9XkUobaAqtmFRdoGbDb3sWMgG6DIs5852knO82RaXer+P+qyb3eWeo7ZNBrRZvm1otY2QFdBjeHIb6hTne49Put12+9ObMoDdYmfy5UkF6AK6cCCr9aM2u9IddptcOYCG+FNDB5xLKCugO7G01TndFp/xgAntdYvrfdwVLnORt3q9Vx25F27DUjbGPxr6qxMgW6Cd2N+d6wLXedA+6nKbK73Lr/pJxzusvE/wZrvX0FOOgGyBxmF/dprXutYOj6nNdS6xyYnWp/dGcaGdhr5vDWQN9E1MXrUzfcA2j2qPj/l1J1uT9iPOeh8w1O7nCGUN9HzyGZ7ndo9qp0ucanU2r1xH+wdDu5wIeQDVVx0+/kd1i697RNv8thdn+Qz4Uv9p6DeOhHyApmBfq3OBu+3Nfd7nVELZAX3Nw4ZarYG8gG7GY1dlk6/Zm3/2Rk8jlB1QvT82dNAmQjkBVf8Mj957fdrefM7ZVhPKEuidvmDob06CXIGGbsX/bZDf8KAhfdbJhLIGmuZuQ084HHIGatiLvRvrRkP6qldbBXkAzbfD0N0OhryBGqrEMOd50FC7d1hPKGugBh8ydMh5hPIGGouI1d5lj6F1vptQ9kDvcKOhN5wMlQH0QcRGnzC03yZCeQDN9G1D6xwBFQI07FI8x02GdjgB8gJqttPQcmuhYoAumzvG7YZWejrkA1TrPYYO+SVCFQO0aM4bqj0uJJQH0LluSP7PkyeQU9QOmyAvoBm+Zegpz4LKA/qYB/wE5AXUe3m81zqoRKAPOYWcuvP9dxvqcD6h7IAKkaNU3eUlHLcI9EzS5YlAi62h/zUy89QCqqKUmvgHywsJlEHnsQYxAvXVIJo5gIhnPhiBju1iNmLvLn85Ah1ZPYs5jBGo72awEzEC9dVwHqQHI9DxWoAYgSLQQKteGIESu/qhCJTYtT+PQBEoAkWgCBSBkotAEehUWwSKQBEoAkWg/BeBIlAEikARKAJFoFmealu4gVLy1Gt5dkARKAL9BzujPSurTmu/AAAAAElFTkSuQmCC); 163 | margin-bottom: 0.4em; 164 | background-size: 100% auto; 165 | } 166 | 167 | .ath-android .ath-action-icon { 168 | width: 1.4em; 169 | height: 1.5em; 170 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAANlBMVEVmZmb///9mZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZW6fJrAAAAEXRSTlMAAAYHG21ub8fLz9DR8/T4+RrZ9owAAAB3SURBVHja7dNLDoAgDATQWv4gKve/rEajJOJiWLgg6WzpSyB0aHqHiNj6nL1lovb4C+hYzkSNAT7mryQFAVOeGAj4CjwEtgrWXpD/uZKtwEJApXt+Vn0flzRhgNiFZQkOXY0aADQZCOCPlsZJ46Rx0jhp3IiN2wGDHhxtldrlwQAAAABJRU5ErkJggg==); 171 | background-size: 100% auto; 172 | } 173 | 174 | .ath-container p { 175 | margin: 0; 176 | padding: 0; 177 | position: relative; 178 | z-index: 2147483642; 179 | text-shadow: 0 0.1em 0 #fff; 180 | font-size: 1.1em; 181 | } 182 | 183 | .ath-ios.ath-phone:after { 184 | content: ''; 185 | background: #eee; 186 | position: absolute; 187 | width: 2em; 188 | height: 2em; 189 | bottom: -0.9em; 190 | left: 50%; 191 | margin-left: -1em; 192 | -webkit-transform: scaleX(0.9) rotate(45deg); 193 | transform: scaleX(0.9) rotate(45deg); 194 | box-shadow: 0.2em 0.2em 0 #d1d1d1; 195 | } 196 | 197 | .ath-ios.ath-tablet:after { 198 | content: ''; 199 | background: #eee; 200 | position: absolute; 201 | width: 2em; 202 | height: 2em; 203 | top: -0.9em; 204 | left: 50%; 205 | margin-left: -1em; 206 | -webkit-transform: scaleX(0.9) rotate(45deg); 207 | transform: scaleX(0.9) rotate(45deg); 208 | z-index: 2147483641; 209 | } 210 | 211 | .ath-application-icon { 212 | position: relative; 213 | padding: 0; 214 | border: 0; 215 | margin: 0 auto 0.2em auto; 216 | height: 6em; 217 | width: 6em; 218 | z-index: 2147483642; 219 | } 220 | 221 | .ath-container.ath-ios .ath-application-icon { 222 | border-radius: 1em; 223 | box-shadow: 0 0.2em 0.4em rgba(0,0,0,0.3), 224 | inset 0 0.07em 0 rgba(255,255,255,0.5); 225 | margin: 0 auto 0.4em auto; 226 | } 227 | 228 | @media only screen and (orientation: landscape) { 229 | .ath-container.ath-phone { 230 | width: 24em; 231 | } 232 | 233 | .ath-android.ath-phone { 234 | margin-left: -12em; 235 | } 236 | 237 | .ath-ios.ath-phone { 238 | margin-left: -12em; 239 | } 240 | 241 | .ath-ios6:after { 242 | left: 39%; 243 | } 244 | 245 | .ath-ios8.ath-phone { 246 | left: auto; 247 | bottom: auto; 248 | right: 0.4em; 249 | top: 1.8em; 250 | } 251 | 252 | .ath-ios8.ath-phone:after { 253 | bottom: auto; 254 | top: -0.9em; 255 | left: 68%; 256 | z-index: 2147483641; 257 | box-shadow: none; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /style/main.scss: -------------------------------------------------------------------------------- 1 | @import "helpers"; 2 | @import "fonts/clear-sans.css"; 3 | 4 | $field-width: 500px; 5 | $grid-spacing: 15px; 6 | $grid-row-cells: 4; 7 | $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; 8 | $tile-border-radius: 3px; 9 | 10 | $mobile-threshold: $field-width + 20px; 11 | 12 | $text-color: #776E65; 13 | $bright-text-color: #f9f6f2; 14 | 15 | $tile-color: #eee4da; 16 | $tile-gold-color: #edc22e; 17 | $tile-gold-glow-color: lighten($tile-gold-color, 15%); 18 | 19 | $game-container-margin-top: 40px; 20 | $game-container-background: #bbada0; 21 | 22 | $transition-speed: 100ms; 23 | 24 | html, body { 25 | margin: 0; 26 | padding: 0; 27 | 28 | background: #faf8ef; 29 | color: $text-color; 30 | font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; 31 | font-size: 18px; 32 | } 33 | 34 | body { 35 | margin: 80px 0; 36 | } 37 | 38 | .heading { 39 | @include clearfix; 40 | } 41 | 42 | h1.title { 43 | font-size: 80px; 44 | font-weight: bold; 45 | margin: 0; 46 | display: block; 47 | float: left; 48 | } 49 | 50 | @include keyframes(move-up) { 51 | 0% { 52 | top: 25px; 53 | opacity: 1; 54 | } 55 | 56 | 100% { 57 | top: -50px; 58 | opacity: 0; 59 | } 60 | } 61 | 62 | .scores-container { 63 | float: right; 64 | text-align: right; 65 | } 66 | 67 | .score-container, .best-container { 68 | $height: 25px; 69 | 70 | position: relative; 71 | display: inline-block; 72 | background: $game-container-background; 73 | padding: 15px 25px; 74 | font-size: $height; 75 | height: $height; 76 | line-height: $height + 22px; 77 | font-weight: bold; 78 | border-radius: 3px; 79 | color: white; 80 | margin-top: 8px; 81 | text-align: center; 82 | 83 | &:after { 84 | position: absolute; 85 | width: 100%; 86 | top: 10px; 87 | left: 0; 88 | text-transform: uppercase; 89 | font-size: 13px; 90 | line-height: 13px; 91 | text-align: center; 92 | color: $tile-color; 93 | } 94 | 95 | .score-addition { 96 | position: absolute; 97 | right: 30px; 98 | color: red; 99 | font-size: $height; 100 | line-height: $height; 101 | font-weight: bold; 102 | color: rgba($text-color, .9); 103 | z-index: 100; 104 | @include animation(move-up 600ms ease-in); 105 | @include animation-fill-mode(both); 106 | } 107 | } 108 | 109 | .score-container:after { 110 | content: "Score"; 111 | } 112 | 113 | .best-container:after { 114 | content: "Best" 115 | } 116 | 117 | p { 118 | margin-top: 0; 119 | margin-bottom: 10px; 120 | line-height: 1.65; 121 | } 122 | 123 | a { 124 | color: $text-color; 125 | font-weight: bold; 126 | text-decoration: underline; 127 | cursor: pointer; 128 | } 129 | 130 | strong { 131 | &.important { 132 | text-transform: uppercase; 133 | } 134 | } 135 | 136 | hr { 137 | border: none; 138 | border-bottom: 1px solid lighten($text-color, 40%); 139 | margin-top: 20px; 140 | margin-bottom: 30px; 141 | } 142 | 143 | .container { 144 | width: $field-width; 145 | margin: 0 auto; 146 | } 147 | 148 | @include keyframes(fade-in) { 149 | 0% { 150 | opacity: 0; 151 | } 152 | 153 | 100% { 154 | opacity: 1; 155 | } 156 | } 157 | 158 | // Styles for buttons 159 | @mixin button { 160 | display: inline-block; 161 | background: darken($game-container-background, 20%); 162 | border-radius: 3px; 163 | padding: 0 20px; 164 | text-decoration: none; 165 | color: $bright-text-color; 166 | height: 40px; 167 | line-height: 42px; 168 | } 169 | 170 | // Game field mixin used to render CSS at different width 171 | @mixin game-field { 172 | .game-container { 173 | margin-top: $game-container-margin-top; 174 | position: relative; 175 | padding: $grid-spacing; 176 | 177 | cursor: default; 178 | -webkit-touch-callout: none; 179 | -ms-touch-callout: none; 180 | 181 | -webkit-user-select: none; 182 | -moz-user-select: none; 183 | -ms-user-select: none; 184 | 185 | -ms-touch-action: none; 186 | touch-action: none; 187 | 188 | background: $game-container-background; 189 | border-radius: $tile-border-radius * 2; 190 | width: $field-width; 191 | height: $field-width; 192 | -webkit-box-sizing: border-box; 193 | -moz-box-sizing: border-box; 194 | box-sizing: border-box; 195 | 196 | .game-message { 197 | display: none; 198 | 199 | position: absolute; 200 | top: 0; 201 | right: 0; 202 | bottom: 0; 203 | left: 0; 204 | background: rgba($tile-color, .5); 205 | z-index: 100; 206 | 207 | text-align: center; 208 | 209 | p { 210 | font-size: 60px; 211 | font-weight: bold; 212 | height: 60px; 213 | line-height: 60px; 214 | margin-top: 222px; 215 | // height: $field-width; 216 | // line-height: $field-width; 217 | } 218 | 219 | .lower { 220 | display: block; 221 | margin-top: 59px; 222 | } 223 | 224 | a { 225 | @include button; 226 | margin-left: 9px; 227 | // margin-top: 59px; 228 | 229 | &.keep-playing-button { 230 | display: none; 231 | } 232 | } 233 | 234 | @include animation(fade-in 800ms ease $transition-speed * 12); 235 | @include animation-fill-mode(both); 236 | 237 | &.game-won { 238 | background: rgba($tile-gold-color, .5); 239 | color: $bright-text-color; 240 | 241 | a.keep-playing-button { 242 | display: inline-block; 243 | } 244 | } 245 | 246 | &.game-won, &.game-over { 247 | display: block; 248 | } 249 | } 250 | } 251 | 252 | .grid-container { 253 | position: absolute; 254 | z-index: 1; 255 | } 256 | 257 | .grid-row { 258 | margin-bottom: $grid-spacing; 259 | 260 | &:last-child { 261 | margin-bottom: 0; 262 | } 263 | 264 | &:after { 265 | content: ""; 266 | display: block; 267 | clear: both; 268 | } 269 | } 270 | 271 | .grid-cell { 272 | width: $tile-size; 273 | height: $tile-size; 274 | margin-right: $grid-spacing; 275 | float: left; 276 | 277 | border-radius: $tile-border-radius; 278 | 279 | background: rgba($tile-color, .35); 280 | 281 | &:last-child { 282 | margin-right: 0; 283 | } 284 | } 285 | 286 | .tile-container { 287 | position: absolute; 288 | z-index: 2; 289 | } 290 | 291 | .tile { 292 | &, .tile-inner { 293 | width: ceil($tile-size); 294 | height: ceil($tile-size); 295 | line-height: ceil($tile-size); 296 | } 297 | 298 | // Build position classes 299 | @for $x from 1 through $grid-row-cells { 300 | @for $y from 1 through $grid-row-cells { 301 | &.tile-position-#{$x}-#{$y} { 302 | $xPos: floor(($tile-size + $grid-spacing) * ($x - 1)); 303 | $yPos: floor(($tile-size + $grid-spacing) * ($y - 1)); 304 | @include transform(translate($xPos, $yPos)); 305 | } 306 | } 307 | } 308 | } 309 | } 310 | 311 | // End of game-field mixin 312 | @include game-field; 313 | 314 | .tile { 315 | position: absolute; // Makes transforms relative to the top-left corner 316 | 317 | .tile-inner { 318 | border-radius: $tile-border-radius; 319 | 320 | background: $tile-color; 321 | text-align: center; 322 | font-weight: bold; 323 | z-index: 10; 324 | 325 | font-size: 55px; 326 | } 327 | 328 | // Movement transition 329 | @include transition($transition-speed ease-in-out); 330 | -webkit-transition-property: -webkit-transform; 331 | -moz-transition-property: -moz-transform; 332 | transition-property: transform; 333 | 334 | $base: 2; 335 | $exponent: 1; 336 | $limit: 11; 337 | 338 | // Colors for all 11 states, false = no special color 339 | $special-colors: false false, // 2 340 | false false, // 4 341 | #f78e48 true, // 8 342 | #fc5e2e true, // 16 343 | #ff3333 true, // 32 344 | #ff0000 true, // 64 345 | false true, // 128 346 | false true, // 256 347 | false true, // 512 348 | false true, // 1024 349 | false true; // 2048 350 | 351 | // Build tile colors 352 | @while $exponent <= $limit { 353 | $power: pow($base, $exponent); 354 | 355 | &.tile-#{$power} .tile-inner { 356 | // Calculate base background color 357 | $gold-percent: ($exponent - 1) / ($limit - 1) * 100; 358 | $mixed-background: mix($tile-gold-color, $tile-color, $gold-percent); 359 | 360 | $nth-color: nth($special-colors, $exponent); 361 | 362 | $special-background: nth($nth-color, 1); 363 | $bright-color: nth($nth-color, 2); 364 | 365 | @if $special-background { 366 | $mixed-background: mix($special-background, $mixed-background, 55%); 367 | } 368 | 369 | @if $bright-color { 370 | color: $bright-text-color; 371 | } 372 | 373 | // Set background 374 | background: $mixed-background; 375 | 376 | // Add glow 377 | $glow-opacity: max($exponent - 4, 0) / ($limit - 4); 378 | 379 | @if not $special-background { 380 | box-shadow: 0 0 30px 10px rgba($tile-gold-glow-color, $glow-opacity / 1.8), 381 | inset 0 0 0 1px rgba(white, $glow-opacity / 3); 382 | } 383 | 384 | // Adjust font size for bigger numbers 385 | @if $power >= 100 and $power < 1000 { 386 | font-size: 45px; 387 | 388 | // Media queries placed here to avoid carrying over the rest of the logic 389 | @include smaller($mobile-threshold) { 390 | font-size: 25px; 391 | } 392 | } @else if $power >= 1000 { 393 | font-size: 35px; 394 | 395 | @include smaller($mobile-threshold) { 396 | font-size: 15px; 397 | } 398 | } 399 | } 400 | 401 | $exponent: $exponent + 1; 402 | } 403 | 404 | // Super tiles (above 2048) 405 | &.tile-super .tile-inner { 406 | color: $bright-text-color; 407 | background: mix(#333, $tile-gold-color, 95%); 408 | 409 | font-size: 30px; 410 | 411 | @include smaller($mobile-threshold) { 412 | font-size: 10px; 413 | } 414 | } 415 | } 416 | 417 | @include keyframes(appear) { 418 | 0% { 419 | opacity: 0; 420 | @include transform(scale(0)); 421 | } 422 | 423 | 100% { 424 | opacity: 1; 425 | @include transform(scale(1)); 426 | } 427 | } 428 | 429 | .tile-new .tile-inner { 430 | @include animation(appear 200ms ease $transition-speed); 431 | @include animation-fill-mode(backwards); 432 | } 433 | 434 | @include keyframes(pop) { 435 | 0% { 436 | @include transform(scale(0)); 437 | } 438 | 439 | 50% { 440 | @include transform(scale(1.2)); 441 | } 442 | 443 | 100% { 444 | @include transform(scale(1)); 445 | } 446 | } 447 | 448 | .tile-merged .tile-inner { 449 | z-index: 20; 450 | @include animation(pop 200ms ease $transition-speed); 451 | @include animation-fill-mode(backwards); 452 | } 453 | 454 | .above-game { 455 | @include clearfix; 456 | } 457 | 458 | .game-intro { 459 | float: left; 460 | line-height: 42px; 461 | margin-bottom: 0; 462 | } 463 | 464 | .restart-button { 465 | @include button; 466 | display: block; 467 | text-align: center; 468 | float: right; 469 | } 470 | 471 | .game-explanation { 472 | margin-top: 50px; 473 | } 474 | 475 | @include smaller($mobile-threshold) { 476 | // Redefine variables for smaller screens 477 | $field-width: 280px; 478 | $grid-spacing: 10px; 479 | $grid-row-cells: 4; 480 | $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; 481 | $tile-border-radius: 3px; 482 | $game-container-margin-top: 17px; 483 | 484 | html, body { 485 | font-size: 15px; 486 | } 487 | 488 | body { 489 | margin: 20px 0; 490 | padding: 0 20px; 491 | } 492 | 493 | h1.title { 494 | font-size: 27px; 495 | margin-top: 15px; 496 | } 497 | 498 | .container { 499 | width: $field-width; 500 | margin: 0 auto; 501 | } 502 | 503 | .score-container, .best-container { 504 | margin-top: 0; 505 | padding: 15px 10px; 506 | min-width: 40px; 507 | } 508 | 509 | .heading { 510 | margin-bottom: 10px; 511 | } 512 | 513 | // Show intro and restart button side by side 514 | .game-intro { 515 | width: 55%; 516 | display: block; 517 | box-sizing: border-box; 518 | line-height: 1.65; 519 | } 520 | 521 | .restart-button { 522 | width: 42%; 523 | padding: 0; 524 | display: block; 525 | box-sizing: border-box; 526 | margin-top: 2px; 527 | } 528 | 529 | // Render the game field at the right width 530 | @include game-field; 531 | 532 | // Rest of the font-size adjustments in the tile class 533 | .tile .tile-inner { 534 | font-size: 35px; 535 | } 536 | 537 | .game-message { 538 | p { 539 | font-size: 30px !important; 540 | height: 30px !important; 541 | line-height: 30px !important; 542 | margin-top: 90px !important; 543 | } 544 | 545 | .lower { 546 | margin-top: 30px !important; 547 | } 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /js/game_manager.js: -------------------------------------------------------------------------------- 1 | function GameManager(size, InputManager, Actuator, StorageManager) { 2 | this.size = size; // Size of the grid 3 | this.inputManager = new InputManager; 4 | this.storageManager = new StorageManager; 5 | this.actuator = new Actuator; 6 | 7 | this.startTiles = 2; 8 | this.relDuration = 10; // Duration (sec) of a relationship 9 | 10 | this.inputManager.on("move", this.move.bind(this)); 11 | this.inputManager.on("restart", this.restart.bind(this)); 12 | this.inputManager.on("keepPlaying", this.keepPlaying.bind(this)); 13 | 14 | this.setup(); 15 | } 16 | 17 | // Restart the game 18 | GameManager.prototype.restart = function () { 19 | this.storageManager.clearGameState(); 20 | this.actuator.continueGame(); // Clear the game won/lost message 21 | this.setup(); 22 | }; 23 | 24 | // Keep playing after winning (allows going over 2048) 25 | GameManager.prototype.keepPlaying = function () { 26 | this.keepPlaying = true; 27 | this.actuator.continueGame(); // Clear the game won/lost message 28 | }; 29 | 30 | // Return true if the game is lost, or has won and the user hasn't kept playing 31 | GameManager.prototype.isGameTerminated = function () { 32 | return this.over || (this.won && !this.keepPlaying); 33 | }; 34 | 35 | // Set up the game 36 | GameManager.prototype.setup = function () { 37 | var previousState = this.storageManager.getGameState(); 38 | 39 | // Reload the game from a previous game if present 40 | if (previousState) { 41 | this.grid = new Grid(previousState.grid.size, 42 | previousState.grid.cells); // Reload grid 43 | this.score = previousState.score; 44 | this.over = previousState.over; 45 | this.won = previousState.won; 46 | this.keepPlaying = previousState.keepPlaying; 47 | this.maxTile = previousState.maxTile || 4; 48 | this.garbCount = previousState.garbCount || 0; 49 | this.karma = previousState.karma || 0; 50 | this.relTime = previousState.relTime || null; 51 | if(this.relTime){ 52 | if((new Date().getTime()-this.relTime)/1000 > this.relDuration){ 53 | this.relTime = null; 54 | if(this.karma <= 0){ 55 | var changes = this.grid.clearRelationship(true); 56 | this.garbCount += changes; 57 | } 58 | } 59 | else 60 | this.setTimer(); 61 | } 62 | } else { 63 | this.grid = new Grid(this.size); 64 | this.score = 0; 65 | this.over = false; 66 | this.won = false; 67 | this.keepPlaying = false; 68 | this.maxTile = 4; 69 | this.garbCount = 0; 70 | this.karma = 0; 71 | this.relTime = null; 72 | 73 | // Add the initial tiles 74 | this.addStartTiles(); 75 | //window.history.pushState("new-game", "", "."); 76 | } 77 | 78 | // Update the actuator 79 | this.actuate(); 80 | }; 81 | 82 | // Set up the initial tiles to start the game with 83 | GameManager.prototype.addStartTiles = function () { 84 | for (var i = 0; i < this.startTiles; i++) { 85 | this.addRandomTile(); 86 | } 87 | }; 88 | 89 | // Adds a tile in a random position 90 | GameManager.prototype.addRandomTile = function () { 91 | var numCellsAvailable = this.grid.availableCells().length; 92 | if (numCellsAvailable > 0) { 93 | var coin = Math.random(); 94 | var p = 0.003; 95 | if(this.maxTile >= 1024) p = 0.001; 96 | else if(this.maxTile >= 256) p = 0.002; 97 | var value = coin < 0.9 ? 2 : 4; 98 | if(this.karma == 0 && this.relTime == null && numCellsAvailable > 1 && numCellsAvailable < 10 && coin >= 0.9-9*p && coin < 0.9+p){ 99 | value = 1; 100 | this.relTime = new Date().getTime(); 101 | this.setTimer(); 102 | } 103 | var tile = new Tile(this.grid.randomAvailableCell(), value); 104 | 105 | this.grid.insertTile(tile); 106 | } 107 | }; 108 | 109 | // Sends the updated grid to the actuator 110 | GameManager.prototype.actuate = function () { 111 | if (this.storageManager.getBestScore() < this.score) { 112 | this.storageManager.setBestScore(this.score); 113 | } 114 | 115 | // Clear the state when the game is over (game over only, not win) 116 | if (this.over) { 117 | this.storageManager.clearGameState(); 118 | } else { 119 | this.storageManager.setGameState(this.serialize()); 120 | } 121 | 122 | this.actuator.actuate(this.grid, { 123 | score: this.score, 124 | over: this.over, 125 | won: this.won, 126 | bestScore: this.storageManager.getBestScore(), 127 | terminated: this.isGameTerminated() 128 | }); 129 | 130 | }; 131 | 132 | // Represent the current game as an object 133 | GameManager.prototype.serialize = function () { 134 | return { 135 | grid: this.grid.serialize(), 136 | score: this.score, 137 | over: this.over, 138 | won: this.won, 139 | keepPlaying: this.keepPlaying, 140 | maxTile: this.maxTile, 141 | garbCount: this.garbCount, 142 | karma: this.karma, 143 | relTime: this.relTime 144 | }; 145 | }; 146 | 147 | // Save all tile positions and remove merger info 148 | GameManager.prototype.prepareTiles = function () { 149 | this.grid.eachCell(function (x, y, tile) { 150 | if (tile) { 151 | tile.mergedFrom = null; 152 | tile.savePosition(); 153 | } 154 | }); 155 | }; 156 | 157 | // Move a tile and its representation 158 | GameManager.prototype.moveTile = function (tile, cell) { 159 | this.grid.cells[tile.x][tile.y] = null; 160 | this.grid.cells[cell.x][cell.y] = tile; 161 | tile.updatePosition(cell); 162 | }; 163 | 164 | // Move tiles on the grid in the specified direction 165 | GameManager.prototype.move = function (direction) { 166 | // 0: up, 1: right, 2: down, 3: left 167 | var self = this; 168 | 169 | if (this.isGameTerminated()) return; // Don't do anything if the game's over 170 | 171 | var cell, tile; 172 | 173 | var vector = this.getVector(direction); 174 | var traversals = this.buildTraversals(vector); 175 | var moved = false; 176 | 177 | // Save the current tile positions and remove merger information 178 | this.prepareTiles(); 179 | 180 | // Traverse the grid in the right direction and move tiles 181 | traversals.x.forEach(function (x) { 182 | traversals.y.forEach(function (y) { 183 | cell = { x: x, y: y }; 184 | tile = self.grid.cellContent(cell); 185 | 186 | if (tile) { 187 | var positions = self.findFarthestPosition(cell, vector); 188 | var next = self.grid.cellContent(positions.next); 189 | 190 | if (tile.value == 0) { // Sticky garbage 191 | var lazyPosition = self.averagePosition(positions.farthest, cell); 192 | if (!self.positionsEqual(lazyPosition, positions.farthest)) { 193 | positions.farthest = lazyPosition; 194 | next = null; 195 | } 196 | } 197 | 198 | if (tile.value == 1 && next) { 199 | self.moveTile(tile, positions.farthest); 200 | if (self.relTime) { 201 | if (next.benefitedFrom != self.relTime) { 202 | next.value *= 2; 203 | if (next.value > self.maxTile) self.maxTile = next.value; 204 | if (next.value >= 2048) self.won = true; 205 | next.benefitedFrom = self.relTime; 206 | self.karma++; 207 | moved = true; 208 | } 209 | } 210 | else { 211 | if (next.value >= 4){ 212 | next.value /= 2; 213 | self.karma--; 214 | if (self.karma <= 0) self.grid.clearRelationship(false); 215 | moved = true; 216 | } 217 | } 218 | } 219 | else if (next && next.value === tile.value && !next.mergedFrom) { 220 | if(next.value != 0) { 221 | if((self.maxTile < 256 || self.garbCount % 2 > 0) 222 | && ((next.value == 8 && Math.random() >= 0.8) 223 | || (next.value == 128 && Math.random() >= 0.85))) { 224 | var merged = new Tile(positions.next, 0); 225 | self.garbCount++; 226 | } 227 | else { 228 | var merged = new Tile(positions.next, tile.value * 2); 229 | merged.benefitedFrom = tile.benefitedFrom; 230 | if(next.benefitedFrom > merged.benefitedFrom) merged.benefitedFrom = next.benefitedFrom; 231 | } 232 | merged.mergedFrom = [tile, next]; 233 | self.grid.insertTile(merged); 234 | self.score += merged.value; 235 | if (merged.value > self.maxTile) self.maxTile = merged.value; 236 | if (merged.value === 2048) self.won = true; 237 | } 238 | else { 239 | self.grid.removeTile(next); 240 | self.garbCount -= 2; 241 | } 242 | 243 | self.grid.removeTile(tile); 244 | 245 | // Converge the two tiles' positions 246 | tile.updatePosition(positions.next); 247 | } 248 | else { 249 | self.moveTile(tile, positions.farthest); 250 | } 251 | 252 | if (!self.positionsEqual(cell, tile)) { 253 | moved = true; // The tile moved from its original cell! 254 | } 255 | } 256 | }); 257 | }); 258 | 259 | if (moved) { 260 | this.addRandomTile(); 261 | 262 | if (!this.movesAvailable()) { 263 | this.over = true; // Game over! 264 | } 265 | 266 | this.actuate(); 267 | } 268 | }; 269 | 270 | // Get the vector representing the chosen direction 271 | GameManager.prototype.getVector = function (direction) { 272 | // Vectors representing tile movement 273 | var map = { 274 | 0: { x: 0, y: -1 }, // Up 275 | 1: { x: 1, y: 0 }, // Right 276 | 2: { x: 0, y: 1 }, // Down 277 | 3: { x: -1, y: 0 } // Left 278 | }; 279 | 280 | return map[direction]; 281 | }; 282 | 283 | // Build a list of positions to traverse in the right order 284 | GameManager.prototype.buildTraversals = function (vector) { 285 | var traversals = { x: [], y: [] }; 286 | 287 | for (var pos = 0; pos < this.size; pos++) { 288 | traversals.x.push(pos); 289 | traversals.y.push(pos); 290 | } 291 | 292 | // Always traverse from the farthest cell in the chosen direction 293 | if (vector.x === 1) traversals.x = traversals.x.reverse(); 294 | if (vector.y === 1) traversals.y = traversals.y.reverse(); 295 | 296 | return traversals; 297 | }; 298 | 299 | GameManager.prototype.findFarthestPosition = function (cell, vector) { 300 | var previous; 301 | 302 | // Progress towards the vector direction until an obstacle is found 303 | do { 304 | previous = cell; 305 | cell = { x: previous.x + vector.x, y: previous.y + vector.y }; 306 | } while (this.grid.withinBounds(cell) && 307 | this.grid.cellAvailable(cell)); 308 | 309 | return { 310 | farthest: previous, 311 | next: cell // Used to check if a merge is required 312 | }; 313 | }; 314 | 315 | GameManager.prototype.movesAvailable = function () { 316 | return this.relTime || this.grid.cellsAvailable() || this.tileMatchesAvailable(); 317 | }; 318 | 319 | // Check for available matches between tiles (more expensive check) 320 | GameManager.prototype.tileMatchesAvailable = function () { 321 | var self = this; 322 | 323 | var tile; 324 | 325 | for (var x = 0; x < this.size; x++) { 326 | for (var y = 0; y < this.size; y++) { 327 | tile = this.grid.cellContent({ x: x, y: y }); 328 | 329 | if (tile) { 330 | for (var direction = 0; direction < 4; direction++) { 331 | var vector = self.getVector(direction); 332 | var cell = { x: x + vector.x, y: y + vector.y }; 333 | 334 | var other = self.grid.cellContent(cell); 335 | 336 | if (other && (other.value === tile.value || (tile.value == 1 && other.value > 2))) { 337 | return true; 338 | } 339 | } 340 | } 341 | } 342 | } 343 | 344 | return false; 345 | }; 346 | 347 | GameManager.prototype.positionsEqual = function (first, second) { 348 | return first.x === second.x && first.y === second.y; 349 | }; 350 | 351 | GameManager.prototype.averagePosition = function (first, second) { 352 | var x = (first.x + second.x) / 2; 353 | var y = (first.y + second.y) / 2; 354 | if (x%1 != 0) x = Math.random()= self.relDuration){ 364 | self.unsetTimer(); 365 | self.actuator.refreshRel(0); 366 | } 367 | else{ 368 | self.actuator.refreshRel(Math.round(self.relDuration - elapsed)); 369 | } 370 | }, 1000); 371 | }; 372 | 373 | GameManager.prototype.unsetTimer = function () { 374 | this.relTime = null; 375 | if(this.timer){ 376 | clearInterval(this.timer); 377 | delete this.timer; 378 | } 379 | if(this.karma <= 0){ 380 | var changes = this.grid.clearRelationship(true); 381 | if(changes){ 382 | this.garbCount += changes; 383 | this.actuate(); 384 | } 385 | } 386 | if (!this.movesAvailable()) { // Determine game over when relationship ends 387 | this.over = true; 388 | this.actuate(); 389 | } 390 | }; 391 | -------------------------------------------------------------------------------- /js/addtohomescreen.min.js: -------------------------------------------------------------------------------- 1 | /* Add to Homescreen v3.2.3 ~ (c) 2015 Matteo Spinelli ~ @license: http://cubiq.org/license */ 2 | (function(window,document){var _eventListener="addEventListener"in window;var _DOMReady=false;if(document.readyState==="complete"){_DOMReady=true}else if(_eventListener){window.addEventListener("load",loaded,false)}function loaded(){window.removeEventListener("load",loaded,false);_DOMReady=true}var _reSmartURL=/\/ath(\/)?$/;var _reQueryString=/([\?&]ath=[^&]*$|&ath=[^&]*(&))/;var _instance;function ath(options){_instance=_instance||new ath.Class(options);return _instance}ath.intl={cs_cs:{ios:"Pro přidáni této webové aplikace na úvodní obrazovku: stlačte %icon a pak Přidat na úvodní obrazovku.",android:'Pro přidáni této webové aplikace na úvodní obrazovku otevřete menu nastavení prohlížeče a stlačte Přidat na úvodní obrazovku. K menu se dostanete stlačením hardwaroveho tlačítka, když ho vaše zařízení má, nebo stlačením pravé horní menu ikony icon.'},de_de:{ios:"Um diese Web-App zum Home-Bildschirm hinzuzufügen, tippen Sie auf %icon und dann Zum Home-Bildschirm.",android:"Um diese Web-App zum Home-Bildschirm hinzuzufügen, öffnen Sie das Menü und tippen dann auf Zum Startbildschirm hinzufügen. Wenn Ihr Gerät eine Menütaste hat, lässt sich das Browsermenü über diese öffnen. Ansonsten tippen Sie auf %icon."},da_dk:{ios:"For at tilføje denne web app til hjemmeskærmen: Tryk %icon og derefter Føj til hjemmeskærm.",android:"For at tilføje denne web app til hjemmeskærmen, åbn browser egenskaber menuen og tryk på Føj til hjemmeskærm. Denne menu kan tilgås ved at trykke på menu knappen, hvis din enhed har en, eller ved at trykke på det øverste højre menu ikon %icon."},el_gr:{ios:"Για να προσθέσετε την εφαρμογή στην αρχική οθόνη: πατήστε το %icon και μετά Πρόσθεσε στην αρχική οθόνη.",android:"Για να προσθέσετε την εφαρμογή στην αρχική οθόνη, ανοίξτε τις επιλογές του browser σας και πατήστε το Προσθήκη στην αρχική οθόνη. Μπορείτε να έχετε πρόσβαση στο μενού, πατώντας το κουμπί του μενού του κινητού σας ή το πάνω δεξιά κουμπί του μενού %icon."},en_us:{ios:"To add this web app to the home screen: tap %icon and then Add to Home Screen.",android:"To add this web app to the home screen open the browser option menu and tap on Add to homescreen. The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon %icon."},es_es:{ios:"Para añadir esta aplicación web a la pantalla de inicio: pulsa %icon y selecciona Añadir a pantalla de inicio.",android:"Para añadir esta aplicación web a la pantalla de inicio, abre las opciones y pulsa Añadir a pantalla inicio. El menú se puede acceder pulsando el botón táctil en caso de tenerlo, o bien el icono de la parte superior derecha de la pantalla %icon."},fi_fi:{ios:"Liitä tämä sovellus kotivalikkoon: klikkaa %icon ja tämän jälkeen Lisää kotivalikkoon.",android:"Lisätäksesi tämän sovelluksen aloitusnäytölle, avaa selaimen valikko ja klikkaa tähti -ikonia tai Lisää aloitusnäytölle tekstiä. Valikkoon pääsee myös painamalla menuvalikkoa, jos laitteessasi on sellainen tai koskettamalla oikealla yläkulmassa menu ikonia %icon."},fr_fr:{ios:"Pour ajouter cette application web sur l'écran d'accueil : Appuyez %icon et sélectionnez Ajouter sur l'écran d'accueil.",android:'Pour ajouter cette application web sur l\'écran d\'accueil : Appuyez sur le bouton "menu", puis sur Ajouter sur l\'écran d\'accueil. Le menu peut-être accessible en appuyant sur le bouton "menu" du téléphone s\'il en possède un . Sinon, il se trouve probablement dans la coin supérieur droit du navigateur %icon.'},he_il:{ios:'להוספת האפליקציה למסך הבית: ללחוץ על %icon ואז הוסף למסך הבית.',android:"To add this web app to the home screen open the browser option menu and tap on Add to homescreen. The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon %icon."},hu_hu:{ios:"Ha hozzá szeretné adni ezt az alkalmazást a kezdőképernyőjéhez, érintse meg a következő ikont: %icon , majd a Hozzáadás a kezdőképernyőhöz menüpontot.",android:"Ha hozzá szeretné adni ezt az alkalmazást a kezdőképernyőjéhez, a böngésző menüjében kattintson a Hozzáadás a kezdőképernyőhöz menüpontra. A böngésző menüjét a következő ikon megérintésével tudja megnyitni: %icon."},it_it:{ios:"Per aggiungere questa web app alla schermata iniziale: premi %icon e poi Aggiungi a Home.",android:"Per aggiungere questa web app alla schermata iniziale, apri il menu opzioni del browser e premi su Aggiungi alla homescreen. Puoi accedere al menu premendo il pulsante hardware delle opzioni se la tua device ne ha uno, oppure premendo l'icona %icon in alto a destra."},ja_jp:{ios:"このウェプアプリをホーム画面に追加するには、%iconをタップしてホーム画面に追加してください。",android:"このウェプアプリをホーム画面に追加するには、ブラウザのオプションメニューからホーム画面に追加をタップしてください。オプションメニューは、一部の機種ではデバイスのメニューボタンから、それ以外では画面右上の%iconからアクセスできます。"},ko_kr:{ios:"홈 화면에 바로가기 생성: %icon 을 클릭한 후 홈 화면에 추가.",android:"브라우저 옵션 메뉴의 홈 화면에 추가를 클릭하여 홈화면에 바로가기를 생성할 수 있습니다. 옵션 메뉴는 장치의 메뉴 버튼을 누르거나 오른쪽 상단의 메뉴 아이콘 %icon을 클릭하여 접근할 수 있습니다."},nb_no:{ios:"For å installere denne appen på hjem-skjermen: trykk på %icon og deretter Legg til på Hjem-skjerm.",android:"For å legge til denne webappen på startsiden åpner en nettlesermenyen og velger Legg til på startsiden. Menyen åpnes ved å trykke på den fysiske menyknappen hvis enheten har det, eller ved å trykke på menyikonet øverst til høyre %icon."},pt_br:{ios:"Para adicionar este app à tela de início: clique %icon e então Tela de início.",android:'Para adicionar este app à tela de início, abra o menu de opções do navegador e selecione Adicionar à tela inicial. O menu pode ser acessado pressionando o "menu" button se o seu dispositivo tiver um, ou selecionando o ícone %icon no canto superior direito.'},pt_pt:{ios:"Para adicionar esta app ao ecrã principal: clique %icon e depois Ecrã principal.",android:'Para adicionar esta app web ecrã principal, abra o menu de opções do navegador e selecione Adicionar à tela inicial. O menu pode ser acessado pressionando o "menu" button se o seu dispositivo tiver um, ou selecionando o ícone %icon no canto superior direito.'},nl_nl:{ios:"Om deze webapp aan je startscherm toe te voegen, klik op %icon en dan Zet in startscherm.",android:'Om deze webapp aan je startscherm toe te voegen, open de browserinstellingen en tik op Toevoegen aan startscherm. Gebruik de "menu" knop als je telefoon die heeft, anders het menu-icoon rechtsbovenin %icon.'},ru_ru:{ios:'Чтобы добавить этот сайт на свой домашний экран, нажмите на иконку %icon и затем На экран "Домой".',android:"Чтобы добавить сайт на свой домашний экран, откройте меню браузера и нажмите на Добавить на главный экран. Меню можно вызвать, нажав на кнопку меню вашего телефона, если она есть. Или найдите иконку сверху справа %icon[иконка]."},sk_sk:{ios:"Pre pridanie tejto webovej aplikácie na úvodnú obrazovku: stlačte %icon a potom Pridať na úvodnú obrazovku.",android:'Pre pridanie tejto webovej aplikácie na úvodnú obrazovku otvorte menu nastavenia prehliadača a stlačte Pridať na úvodnú obrazovku. K menu sa dostanete stlačením hardwaroveho tlačidla, ak ho vaše zariadenie má, alebo stlačením pravej hornej menu ikony icon.'},sv_se:{ios:"För att lägga till denna webbapplikation på hemskärmen: tryck på %icon och därefter Lägg till på hemskärmen.",android:"För att lägga till den här webbappen på hemskärmen öppnar du webbläsarens alternativ-meny och väljer Lägg till på startskärmen. Man hittar menyn genom att trycka på hårdvaruknappen om din enhet har en sådan, eller genom att trycka på menyikonen högst upp till höger %icon."},tr_tr:{ios:"Uygulamayı ana ekrana eklemek için, %icon ve ardından ana ekrana ekle butonunu tıklayın.",android:"Uygulamayı ana ekrana eklemek için, menüye girin ve ana ekrana ekle butonunu tıklayın. Cihazınız menü tuşuna sahip ise menüye girmek için menü tuşunu tıklayın. Aksi takdirde %icon butonunu tıklayın."},uk_ua:{ios:"Щоб додати цей сайт на початковий екран, натисніть %icon, а потім На початковий екран.",android:"Щоб додати цей сайт на домашній екран, відкрийте меню браузера та виберіть Додати на головний екран. Це можливо зробити, натиснувши кнопку меню на вашому смартфоні, якщо така є. Або ж на іконці зверху справа %icon."},zh_cn:{ios:"如要把应用程序加至主屏幕,请点击%icon, 然后添加到主屏幕",android:"To add this web app to the home screen open the browser option menu and tap on Add to homescreen. The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon %icon."},zh_tw:{ios:"如要把應用程式加至主屏幕, 請點擊%icon, 然後加至主屏幕.",android:"To add this web app to the home screen open the browser option menu and tap on Add to homescreen. The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon %icon."}};for(var lang in ath.intl){ath.intl[lang.substr(0,2)]=ath.intl[lang]}ath.defaults={appID:"org.cubiq.addtohome",fontSize:15,debug:false,logging:false,modal:false,mandatory:false,autostart:true,skipFirstVisit:false,startDelay:1,lifespan:15,displayPace:1440,maxDisplayCount:0,icon:true,message:"",validLocation:[],onInit:null,onShow:null,onRemove:null,onAdd:null,onPrivate:null,privateModeOverride:false,detectHomescreen:false};var _ua=window.navigator.userAgent;var _nav=window.navigator;_extend(ath,{hasToken:document.location.hash=="#ath"||_reSmartURL.test(document.location.href)||_reQueryString.test(document.location.search),isRetina:window.devicePixelRatio&&window.devicePixelRatio>1,isIDevice:/iphone|ipod|ipad/i.test(_ua),isMobileChrome:_ua.indexOf("Android")>-1&&/Chrome\/[.0-9]*/.test(_ua)&&_ua.indexOf("Version")==-1,isMobileIE:_ua.indexOf("Windows Phone")>-1,language:_nav.language&&_nav.language.toLowerCase().replace("-","_")||""});ath.language=ath.language&&ath.language in ath.intl?ath.language:"en_us";ath.isMobileSafari=ath.isIDevice&&_ua.indexOf("Safari")>-1&&_ua.indexOf("CriOS")<0;ath.OS=ath.isIDevice?"ios":ath.isMobileChrome?"android":ath.isMobileIE?"windows":"unsupported";ath.OSVersion=_ua.match(/(OS|Android) (\d+[_\.]\d+)/);ath.OSVersion=ath.OSVersion&&ath.OSVersion[2]?+ath.OSVersion[2].replace("_","."):0;ath.isStandalone="standalone"in window.navigator&&window.navigator.standalone;ath.isTablet=ath.isMobileSafari&&_ua.indexOf("iPad")>-1||ath.isMobileChrome&&_ua.indexOf("Mobile")<0;ath.isCompatible=ath.isMobileSafari&&ath.OSVersion>=6||ath.isMobileChrome;var _defaultSession={lastDisplayTime:0,returningVisitor:false,displayCount:0,optedout:false,added:false};ath.removeSession=function(appID){try{if(!localStorage){throw new Error("localStorage is not defined")}localStorage.removeItem(appID||ath.defaults.appID)}catch(e){}};ath.doLog=function(logStr){if(this.options.logging){console.log(logStr)}};ath.Class=function(options){this.doLog=ath.doLog;this.options=_extend({},ath.defaults);_extend(this.options,options);if(this.options&&this.options.debug&&typeof this.options.logging==="undefined"){this.options.logging=true}if(!_eventListener){return}this.options.mandatory=this.options.mandatory&&("standalone"in window.navigator||this.options.debug);this.options.modal=this.options.modal||this.options.mandatory;if(this.options.mandatory){this.options.startDelay=-.5}this.options.detectHomescreen=this.options.detectHomescreen===true?"hash":this.options.detectHomescreen;if(this.options.debug){ath.isCompatible=true;ath.OS=typeof this.options.debug=="string"?this.options.debug:ath.OS=="unsupported"?"android":ath.OS;ath.OSVersion=ath.OS=="ios"?"8":"4"}this.container=document.body;this.session=this.getItem(this.options.appID);this.session=this.session?JSON.parse(this.session):undefined;if(ath.hasToken&&(!ath.isCompatible||!this.session)){ath.hasToken=false;_removeToken()}if(!ath.isCompatible){this.doLog("Add to homescreen: not displaying callout because device not supported");return}this.session=this.session||_defaultSession;try{if(!localStorage){throw new Error("localStorage is not defined")}localStorage.setItem(this.options.appID,JSON.stringify(this.session));ath.hasLocalStorage=true}catch(e){ath.hasLocalStorage=false;if(this.options.onPrivate){this.options.onPrivate.call(this)}}var isValidLocation=!this.options.validLocation.length;for(var i=this.options.validLocation.length;i--;){if(this.options.validLocation[i].test(document.location.href)){isValidLocation=true;break}}if(this.getItem("addToHome")){this.optOut()}if(this.session.optedout){this.doLog("Add to homescreen: not displaying callout because user opted out");return}if(this.session.added){this.doLog("Add to homescreen: not displaying callout because already added to the homescreen");return}if(!isValidLocation){this.doLog("Add to homescreen: not displaying callout because not a valid location");return}if(ath.isStandalone){if(!this.session.added){this.session.added=true;this.updateSession();if(this.options.onAdd&&ath.hasLocalStorage){this.options.onAdd.call(this)}}this.doLog("Add to homescreen: not displaying callout because in standalone mode");return}if(this.options.detectHomescreen){if(ath.hasToken){_removeToken();if(!this.session.added){this.session.added=true;this.updateSession();if(this.options.onAdd&&ath.hasLocalStorage){this.options.onAdd.call(this)}}this.doLog("Add to homescreen: not displaying callout because URL has token, so we are likely coming from homescreen");return}if(this.options.detectHomescreen=="hash"){history.replaceState("",window.document.title,document.location.href+"#ath")}else if(this.options.detectHomescreen=="smartURL"){history.replaceState("",window.document.title,document.location.href.replace(/(\/)?$/,"/ath$1"))}else{history.replaceState("",window.document.title,document.location.href+(document.location.search?"&":"?")+"ath=")}}if(!this.session.returningVisitor){this.session.returningVisitor=true;this.updateSession();if(this.options.skipFirstVisit){this.doLog("Add to homescreen: not displaying callout because skipping first visit");return}}if(!this.options.privateModeOverride&&!ath.hasLocalStorage){this.doLog("Add to homescreen: not displaying callout because browser is in private mode");return}this.ready=true;if(this.options.onInit){this.options.onInit.call(this)}if(this.options.autostart){this.doLog("Add to homescreen: autostart displaying callout");this.show()}};ath.Class.prototype={events:{load:"_delayedShow",error:"_delayedShow",orientationchange:"resize",resize:"resize",scroll:"resize",click:"remove",touchmove:"_preventDefault",transitionend:"_removeElements",webkitTransitionEnd:"_removeElements",MSTransitionEnd:"_removeElements"},handleEvent:function(e){var type=this.events[e.type];if(type){this[type](e)}},show:function(force){if(this.options.autostart&&!_DOMReady){setTimeout(this.show.bind(this),50);return}if(this.shown){this.doLog("Add to homescreen: not displaying callout because already shown on screen");return}var now=Date.now();var lastDisplayTime=this.session.lastDisplayTime;if(force!==true){if(!this.ready){this.doLog("Add to homescreen: not displaying callout because not ready");return}if(now-lastDisplayTime=this.options.maxDisplayCount){this.doLog("Add to homescreen: not displaying callout because displayed too many times already");return}}this.shown=true;this.session.lastDisplayTime=now;this.session.displayCount++;this.updateSession();if(!this.applicationIcon){if(ath.OS=="ios"){this.applicationIcon=document.querySelector('head link[rel^=apple-touch-icon][sizes="152x152"],head link[rel^=apple-touch-icon][sizes="144x144"],head link[rel^=apple-touch-icon][sizes="120x120"],head link[rel^=apple-touch-icon][sizes="114x114"],head link[rel^=apple-touch-icon]')}else{this.applicationIcon=document.querySelector('head link[rel^="shortcut icon"][sizes="196x196"],head link[rel^=apple-touch-icon]')}}var message="";if(typeof this.options.message=="object"&&ath.language in this.options.message){message=this.options.message[ath.language][ath.OS]}else if(typeof this.options.message=="object"&&ath.OS in this.options.message){message=this.options.message[ath.OS]}else if(this.options.message in ath.intl){message=ath.intl[this.options.message][ath.OS]}else if(this.options.message!==""){message=this.options.message}else if(ath.OS in ath.intl[ath.language]){message=ath.intl[ath.language][ath.OS]}message="

"+message.replace(/%icon(?:\[([^\]]+)\])?/gi,function(matches,group1){return''+(!!group1?group1:"icon")+""})+"

";this.viewport=document.createElement("div");this.viewport.className="ath-viewport";if(this.options.modal){this.viewport.className+=" ath-modal"}if(this.options.mandatory){this.viewport.className+=" ath-mandatory"}this.viewport.style.position="absolute";this.element=document.createElement("div");this.element.className="ath-container ath-"+ath.OS+" ath-"+ath.OS+(parseInt(ath.OSVersion)||"")+" ath-"+(ath.isTablet?"tablet":"phone");this.element.style.cssText="-webkit-transition-property:-webkit-transform,opacity;-webkit-transition-duration:0s;-webkit-transition-timing-function:ease-out;transition-property:transform,opacity;transition-duration:0s;transition-timing-function:ease-out;";this.element.style.webkitTransform="translate3d(0,-"+window.innerHeight+"px,0)";this.element.style.transform="translate3d(0,-"+window.innerHeight+"px,0)";if(this.options.icon&&this.applicationIcon){this.element.className+=" ath-icon";this.img=document.createElement("img");this.img.className="ath-application-icon";this.img.addEventListener("load",this,false);this.img.addEventListener("error",this,false);this.img.src=this.applicationIcon.href;this.element.appendChild(this.img)}this.element.innerHTML+=message;this.viewport.style.left="-99999em";this.viewport.appendChild(this.element);this.container.appendChild(this.viewport);if(this.img){this.doLog("Add to homescreen: not displaying callout because waiting for img to load")}else{this._delayedShow()}},_delayedShow:function(e){setTimeout(this._show.bind(this),this.options.startDelay*1e3+500)},_show:function(){var that=this;this.updateViewport();window.addEventListener("resize",this,false);window.addEventListener("scroll",this,false);window.addEventListener("orientationchange",this,false);if(this.options.modal){document.addEventListener("touchmove",this,true)}if(!this.options.mandatory){setTimeout(function(){that.element.addEventListener("click",that,true)},1e3)}setTimeout(function(){that.element.style.webkitTransitionDuration="1.2s";that.element.style.transitionDuration="1.2s";that.element.style.webkitTransform="translate3d(0,0,0)";that.element.style.transform="translate3d(0,0,0)"},0);if(this.options.lifespan){this.removeTimer=setTimeout(this.remove.bind(this),this.options.lifespan*1e3)}if(this.options.onShow){this.options.onShow.call(this)}},remove:function(){clearTimeout(this.removeTimer);if(this.img){this.img.removeEventListener("load",this,false);this.img.removeEventListener("error",this,false)}window.removeEventListener("resize",this,false);window.removeEventListener("scroll",this,false);window.removeEventListener("orientationchange",this,false);document.removeEventListener("touchmove",this,true);this.element.removeEventListener("click",this,true);this.element.addEventListener("transitionend",this,false);this.element.addEventListener("webkitTransitionEnd",this,false);this.element.addEventListener("MSTransitionEnd",this,false);this.element.style.webkitTransitionDuration="0.3s";this.element.style.opacity="0"},_removeElements:function(){this.element.removeEventListener("transitionend",this,false);this.element.removeEventListener("webkitTransitionEnd",this,false);this.element.removeEventListener("MSTransitionEnd",this,false);this.container.removeChild(this.viewport);this.shown=false;if(this.options.onRemove){this.options.onRemove.call(this)}},updateViewport:function(){if(!this.shown){return}this.viewport.style.width=window.innerWidth+"px";this.viewport.style.height=window.innerHeight+"px";this.viewport.style.left=window.scrollX+"px";this.viewport.style.top=window.scrollY+"px";var clientWidth=document.documentElement.clientWidth;this.orientation=clientWidth>document.documentElement.clientHeight?"landscape":"portrait";var screenWidth=ath.OS=="ios"?this.orientation=="portrait"?screen.width:screen.height:screen.width;this.scale=screen.width>clientWidth?1:screenWidth/window.innerWidth;this.element.style.fontSize=this.options.fontSize/this.scale+"px"},resize:function(){clearTimeout(this.resizeTimer);this.resizeTimer=setTimeout(this.updateViewport.bind(this),100)},updateSession:function(){if(ath.hasLocalStorage===false){return}if(localStorage){localStorage.setItem(this.options.appID,JSON.stringify(this.session))}},clearSession:function(){this.session=_defaultSession;this.updateSession()},getItem:function(item){try{if(!localStorage){throw new Error("localStorage is not defined")}return localStorage.getItem(item)}catch(e){ath.hasLocalStorage=false}},optOut:function(){this.session.optedout=true;this.updateSession()},optIn:function(){this.session.optedout=false;this.updateSession()},clearDisplayCount:function(){this.session.displayCount=0;this.updateSession()},_preventDefault:function(e){e.preventDefault();e.stopPropagation()}};function _extend(target,obj){for(var i in obj){target[i]=obj[i]}return target}function _removeToken(){if(document.location.hash=="#ath"){history.replaceState("",window.document.title,document.location.href.split("#")[0])}if(_reSmartURL.test(document.location.href)){history.replaceState("",window.document.title,document.location.href.replace(_reSmartURL,"$1"))}if(_reQueryString.test(document.location.search)){history.replaceState("",window.document.title,document.location.href.replace(_reQueryString,"$2"))}}window.addToHomescreen=ath})(window,document); -------------------------------------------------------------------------------- /style/main.css: -------------------------------------------------------------------------------- 1 | @import url(fonts/clear-sans.css); 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | background: #faf8ef; 6 | color: #776E65; 7 | font-family: "Clear Sans", "Helvetica Neue", Arial, "Hiragino Sans GB", "PingFang SC", "Microsoft Yahei", "Source Han Sans GB", sans-serif; 8 | font-size: 18px; } 9 | 10 | body { 11 | margin: 80px 0; } 12 | 13 | .heading:after { 14 | content: ""; 15 | display: block; 16 | clear: both; } 17 | 18 | h1.title { 19 | font-size: 80px; 20 | font-weight: bold; 21 | margin: 0; 22 | display: block; 23 | float: left; } 24 | 25 | @-webkit-keyframes move-up { 26 | 0% { 27 | top: 25px; 28 | opacity: 1; } 29 | 100% { 30 | top: -50px; 31 | opacity: 0; } } 32 | @-moz-keyframes move-up { 33 | 0% { 34 | top: 25px; 35 | opacity: 1; } 36 | 100% { 37 | top: -50px; 38 | opacity: 0; } } 39 | @keyframes move-up { 40 | 0% { 41 | top: 25px; 42 | opacity: 1; } 43 | 100% { 44 | top: -50px; 45 | opacity: 0; } } 46 | .scores-container { 47 | float: right; 48 | text-align: right; } 49 | 50 | .score-container, .best-container { 51 | position: relative; 52 | display: inline-block; 53 | background: #bbada0; 54 | padding: 15px 25px; 55 | font-size: 25px; 56 | height: 25px; 57 | line-height: 47px; 58 | font-weight: bold; 59 | border-radius: 3px; 60 | color: white; 61 | margin-top: 8px; 62 | text-align: center; 63 | min-width: 50px; } 64 | .score-container:after, .best-container:after { 65 | position: absolute; 66 | width: 100%; 67 | top: 10px; 68 | left: 0; 69 | text-transform: uppercase; 70 | font-size: 13px; 71 | line-height: 13px; 72 | text-align: center; 73 | color: #eee4da; } 74 | .score-container .score-addition, .best-container .score-addition { 75 | position: absolute; 76 | right: 30px; 77 | color: red; 78 | font-size: 25px; 79 | line-height: 25px; 80 | font-weight: bold; 81 | color: rgba(119, 110, 101, 0.9); 82 | z-index: 100; 83 | -webkit-animation: move-up 600ms ease-in; 84 | -moz-animation: move-up 600ms ease-in; 85 | animation: move-up 600ms ease-in; 86 | -webkit-animation-fill-mode: both; 87 | -moz-animation-fill-mode: both; 88 | animation-fill-mode: both; } 89 | 90 | .score-container:after { 91 | content: "Hair loss"; } 92 | 93 | .best-container:after { 94 | content: "Max"; } 95 | 96 | p { 97 | margin-top: 0; 98 | margin-bottom: 10px; 99 | line-height: 1.65; } 100 | 101 | a { 102 | color: #776E65; 103 | font-weight: bold; 104 | text-decoration: underline; 105 | cursor: pointer; } 106 | 107 | strong.important { 108 | text-transform: uppercase; } 109 | 110 | hr { 111 | border: none; 112 | border-bottom: 1px solid #d8d4d0; 113 | margin-top: 20px; 114 | margin-bottom: 30px; } 115 | 116 | .container { 117 | width: 500px; 118 | margin: 0 auto; } 119 | 120 | @-webkit-keyframes fade-in { 121 | 0% { 122 | opacity: 0; } 123 | 100% { 124 | opacity: 1; } } 125 | @-moz-keyframes fade-in { 126 | 0% { 127 | opacity: 0; } 128 | 100% { 129 | opacity: 1; } } 130 | @keyframes fade-in { 131 | 0% { 132 | opacity: 0; } 133 | 100% { 134 | opacity: 1; } } 135 | .game-container { 136 | margin-top: 40px; 137 | position: relative; 138 | padding: 15px; 139 | cursor: default; 140 | -webkit-touch-callout: none; 141 | -ms-touch-callout: none; 142 | -webkit-user-select: none; 143 | -moz-user-select: none; 144 | -ms-user-select: none; 145 | -ms-touch-action: none; 146 | touch-action: none; 147 | background: #bbada0; 148 | border-radius: 6px; 149 | width: 500px; 150 | height: 500px; 151 | -webkit-box-sizing: border-box; 152 | -moz-box-sizing: border-box; 153 | box-sizing: border-box; } 154 | .game-container .game-message { 155 | display: none; 156 | position: absolute; 157 | top: 0; 158 | right: 0; 159 | bottom: 0; 160 | left: 0; 161 | background: rgba(238, 228, 218, 0.5); 162 | z-index: 100; 163 | text-align: center; 164 | -webkit-animation: fade-in 800ms ease 1200ms; 165 | -moz-animation: fade-in 800ms ease 1200ms; 166 | animation: fade-in 800ms ease 1200ms; 167 | -webkit-animation-fill-mode: both; 168 | -moz-animation-fill-mode: both; 169 | animation-fill-mode: both; } 170 | .game-container .game-message p { 171 | font-size: 60px; 172 | font-weight: bold; 173 | height: 60px; 174 | line-height: 60px; 175 | margin-top: 222px; } 176 | .game-container .game-message .lower { 177 | display: block; 178 | margin-top: 59px; } 179 | .game-container .game-message a { 180 | display: inline-block; 181 | background: #8f7a66; 182 | border-radius: 3px; 183 | padding: 0 20px; 184 | text-decoration: none; 185 | color: #f9f6f2; 186 | height: 40px; 187 | line-height: 42px; 188 | margin-left: 9px; } 189 | .game-container .game-message a.keep-playing-button { 190 | display: none; } 191 | .game-container .game-message.game-won { 192 | background: rgba(237, 194, 46, 0.5); 193 | color: #f9f6f2; } 194 | .game-container .game-message.game-won a.keep-playing-button { 195 | display: inline-block; } 196 | .game-container .game-message.game-won, .game-container .game-message.game-over { 197 | display: block; } 198 | 199 | .grid-container { 200 | position: absolute; 201 | z-index: 1; } 202 | 203 | .grid-row { 204 | margin-bottom: 15px; } 205 | .grid-row:last-child { 206 | margin-bottom: 0; } 207 | .grid-row:after { 208 | content: ""; 209 | display: block; 210 | clear: both; } 211 | 212 | .grid-cell { 213 | width: 106.25px; 214 | height: 106.25px; 215 | margin-right: 15px; 216 | float: left; 217 | border-radius: 3px; 218 | background: rgba(238, 228, 218, 0.35); } 219 | .grid-cell:last-child { 220 | margin-right: 0; } 221 | 222 | .tile-container { 223 | position: absolute; 224 | z-index: 2; } 225 | 226 | .tile, .tile .tile-inner { 227 | width: 107px; 228 | height: 107px; 229 | line-height: 107px; } 230 | .tile.tile-position-1-1 { 231 | -webkit-transform: translate(0px, 0px); 232 | -moz-transform: translate(0px, 0px); 233 | -ms-transform: translate(0px, 0px); 234 | transform: translate(0px, 0px); } 235 | .tile.tile-position-1-2 { 236 | -webkit-transform: translate(0px, 121px); 237 | -moz-transform: translate(0px, 121px); 238 | -ms-transform: translate(0px, 121px); 239 | transform: translate(0px, 121px); } 240 | .tile.tile-position-1-3 { 241 | -webkit-transform: translate(0px, 242px); 242 | -moz-transform: translate(0px, 242px); 243 | -ms-transform: translate(0px, 242px); 244 | transform: translate(0px, 242px); } 245 | .tile.tile-position-1-4 { 246 | -webkit-transform: translate(0px, 363px); 247 | -moz-transform: translate(0px, 363px); 248 | -ms-transform: translate(0px, 363px); 249 | transform: translate(0px, 363px); } 250 | .tile.tile-position-2-1 { 251 | -webkit-transform: translate(121px, 0px); 252 | -moz-transform: translate(121px, 0px); 253 | -ms-transform: translate(121px, 0px); 254 | transform: translate(121px, 0px); } 255 | .tile.tile-position-2-2 { 256 | -webkit-transform: translate(121px, 121px); 257 | -moz-transform: translate(121px, 121px); 258 | -ms-transform: translate(121px, 121px); 259 | transform: translate(121px, 121px); } 260 | .tile.tile-position-2-3 { 261 | -webkit-transform: translate(121px, 242px); 262 | -moz-transform: translate(121px, 242px); 263 | -ms-transform: translate(121px, 242px); 264 | transform: translate(121px, 242px); } 265 | .tile.tile-position-2-4 { 266 | -webkit-transform: translate(121px, 363px); 267 | -moz-transform: translate(121px, 363px); 268 | -ms-transform: translate(121px, 363px); 269 | transform: translate(121px, 363px); } 270 | .tile.tile-position-3-1 { 271 | -webkit-transform: translate(242px, 0px); 272 | -moz-transform: translate(242px, 0px); 273 | -ms-transform: translate(242px, 0px); 274 | transform: translate(242px, 0px); } 275 | .tile.tile-position-3-2 { 276 | -webkit-transform: translate(242px, 121px); 277 | -moz-transform: translate(242px, 121px); 278 | -ms-transform: translate(242px, 121px); 279 | transform: translate(242px, 121px); } 280 | .tile.tile-position-3-3 { 281 | -webkit-transform: translate(242px, 242px); 282 | -moz-transform: translate(242px, 242px); 283 | -ms-transform: translate(242px, 242px); 284 | transform: translate(242px, 242px); } 285 | .tile.tile-position-3-4 { 286 | -webkit-transform: translate(242px, 363px); 287 | -moz-transform: translate(242px, 363px); 288 | -ms-transform: translate(242px, 363px); 289 | transform: translate(242px, 363px); } 290 | .tile.tile-position-4-1 { 291 | -webkit-transform: translate(363px, 0px); 292 | -moz-transform: translate(363px, 0px); 293 | -ms-transform: translate(363px, 0px); 294 | transform: translate(363px, 0px); } 295 | .tile.tile-position-4-2 { 296 | -webkit-transform: translate(363px, 121px); 297 | -moz-transform: translate(363px, 121px); 298 | -ms-transform: translate(363px, 121px); 299 | transform: translate(363px, 121px); } 300 | .tile.tile-position-4-3 { 301 | -webkit-transform: translate(363px, 242px); 302 | -moz-transform: translate(363px, 242px); 303 | -ms-transform: translate(363px, 242px); 304 | transform: translate(363px, 242px); } 305 | .tile.tile-position-4-4 { 306 | -webkit-transform: translate(363px, 363px); 307 | -moz-transform: translate(363px, 363px); 308 | -ms-transform: translate(363px, 363px); 309 | transform: translate(363px, 363px); } 310 | 311 | .tile { 312 | position: absolute; 313 | -webkit-transition: 100ms ease-in-out; 314 | -moz-transition: 100ms ease-in-out; 315 | transition: 100ms ease-in-out; 316 | -webkit-transition-property: -webkit-transform; 317 | -moz-transition-property: -moz-transform; 318 | transition-property: transform; } 319 | .tile .tile-inner { 320 | border-radius: 3px; 321 | background: #eee4da; 322 | text-align: center; 323 | font-weight: bold; 324 | z-index: 10; 325 | font-size: 30px; } 326 | .tile.tile-0 .tile-inner { 327 | background: #776E65; 328 | color: #eee4da; 329 | box-shadow: 0 0 50px 15px rgba(0,0,0,0.2), inset 0 0 0 1px rgba(255, 255, 255, 0); } 330 | @media screen and (max-width: 520px) { 331 | .tile.tile-0 .tile-inner { 332 | font-size: 14px; } } 333 | .tile.tile-1 .tile-inner { 334 | background: #a867b4; 335 | color: white; 336 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); } 337 | @media screen and (max-width: 520px) { 338 | .tile.tile-1 .tile-inner { 339 | font-size: 14px; } } 340 | .tile.tile-2 .tile-inner { 341 | background: #eee4da; 342 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); } 343 | @media screen and (max-width: 520px) { 344 | .tile.tile-2 .tile-inner { 345 | font-size: 14px; } } 346 | .tile.tile-4 .tile-inner { 347 | background: #eee1c9; 348 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); } 349 | @media screen and (max-width: 520px) { 350 | .tile.tile-4 .tile-inner { 351 | font-size: 14px; } } 352 | .tile.tile-8 .tile-inner { 353 | color: #f9f6f2; 354 | background: #f3b27a; } 355 | @media screen and (max-width: 520px) { 356 | .tile.tile-8 .tile-inner { 357 | font-size: 14px; } } 358 | .tile.tile-16 .tile-inner { 359 | color: #f9f6f2; 360 | background: #f69664; } 361 | @media screen and (max-width: 520px) { 362 | .tile.tile-16 .tile-inner { 363 | font-size: 14px; } } 364 | .tile.tile-32 .tile-inner { 365 | color: #f9f6f2; 366 | background: #f77c5f; } 367 | @media screen and (max-width: 520px) { 368 | .tile.tile-32 .tile-inner { 369 | font-size: 14px; } } 370 | .tile.tile-64 .tile-inner { 371 | color: #f9f6f2; 372 | background: #f75f3b; } 373 | @media screen and (max-width: 520px) { 374 | .tile.tile-64 .tile-inner { 375 | font-size: 14px; } } 376 | .tile.tile-128 .tile-inner { 377 | color: #f9f6f2; 378 | background: #edd073; 379 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.2381), inset 0 0 0 1px rgba(255, 255, 255, 0.14286); } 380 | @media screen and (max-width: 520px) { 381 | .tile.tile-128 .tile-inner { 382 | font-size: 14px; } } 383 | .tile.tile-256 .tile-inner { 384 | color: #f9f6f2; 385 | background: #edcc62; 386 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.31746), inset 0 0 0 1px rgba(255, 255, 255, 0.19048); } 387 | @media screen and (max-width: 520px) { 388 | .tile.tile-256 .tile-inner { 389 | font-size: 14px; } } 390 | .tile.tile-512 .tile-inner { 391 | color: #f9f6f2; 392 | background: #edc950; 393 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.39683), inset 0 0 0 1px rgba(255, 255, 255, 0.2381); } 394 | @media screen and (max-width: 520px) { 395 | .tile.tile-512 .tile-inner { 396 | font-size: 14px; } } 397 | .tile.tile-1024 .tile-inner { 398 | color: #f9f6f2; 399 | background: #edc53f; 400 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.47619), inset 0 0 0 1px rgba(255, 255, 255, 0.28571); } 401 | @media screen and (max-width: 520px) { 402 | .tile.tile-1024 .tile-inner { 403 | font-size: 14px; } } 404 | .tile.tile-2048 .tile-inner { 405 | color: #f9f6f2; 406 | background: #edc22e; 407 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333); } 408 | @media screen and (max-width: 520px) { 409 | .tile.tile-2048 .tile-inner { 410 | font-size: 14px; } } 411 | .tile.tile-super .tile-inner { 412 | color: #f9f6f2; 413 | background: #3c3a33; 414 | font-size: 30px; } 415 | @media screen and (max-width: 520px) { 416 | .tile.tile-super .tile-inner { 417 | font-size: 10px; } } 418 | 419 | .karma {font-size: smaller;} 420 | #progress { 421 | background-color: green; 422 | position: absolute; 423 | color: white; 424 | height: 100px; 425 | } 426 | @media screen and (max-width: 520px) { 427 | #progress {height: 40px;} 428 | } 429 | 430 | @-webkit-keyframes appear { 431 | 0% { 432 | opacity: 0; 433 | -webkit-transform: scale(0); 434 | -moz-transform: scale(0); 435 | -ms-transform: scale(0); 436 | transform: scale(0); } 437 | 100% { 438 | opacity: 1; 439 | -webkit-transform: scale(1); 440 | -moz-transform: scale(1); 441 | -ms-transform: scale(1); 442 | transform: scale(1); } } 443 | @-moz-keyframes appear { 444 | 0% { 445 | opacity: 0; 446 | -webkit-transform: scale(0); 447 | -moz-transform: scale(0); 448 | -ms-transform: scale(0); 449 | transform: scale(0); } 450 | 100% { 451 | opacity: 1; 452 | -webkit-transform: scale(1); 453 | -moz-transform: scale(1); 454 | -ms-transform: scale(1); 455 | transform: scale(1); } } 456 | @keyframes appear { 457 | 0% { 458 | opacity: 0; 459 | -webkit-transform: scale(0); 460 | -moz-transform: scale(0); 461 | -ms-transform: scale(0); 462 | transform: scale(0); } 463 | 100% { 464 | opacity: 1; 465 | -webkit-transform: scale(1); 466 | -moz-transform: scale(1); 467 | -ms-transform: scale(1); 468 | transform: scale(1); } } 469 | .tile-new .tile-inner { 470 | -webkit-animation: appear 200ms ease 100ms; 471 | -moz-animation: appear 200ms ease 100ms; 472 | animation: appear 200ms ease 100ms; 473 | -webkit-animation-fill-mode: backwards; 474 | -moz-animation-fill-mode: backwards; 475 | animation-fill-mode: backwards; } 476 | 477 | @-webkit-keyframes pop { 478 | 0% { 479 | -webkit-transform: scale(0); 480 | -moz-transform: scale(0); 481 | -ms-transform: scale(0); 482 | transform: scale(0); } 483 | 50% { 484 | -webkit-transform: scale(1.2); 485 | -moz-transform: scale(1.2); 486 | -ms-transform: scale(1.2); 487 | transform: scale(1.2); } 488 | 100% { 489 | -webkit-transform: scale(1); 490 | -moz-transform: scale(1); 491 | -ms-transform: scale(1); 492 | transform: scale(1); } } 493 | @-moz-keyframes pop { 494 | 0% { 495 | -webkit-transform: scale(0); 496 | -moz-transform: scale(0); 497 | -ms-transform: scale(0); 498 | transform: scale(0); } 499 | 50% { 500 | -webkit-transform: scale(1.2); 501 | -moz-transform: scale(1.2); 502 | -ms-transform: scale(1.2); 503 | transform: scale(1.2); } 504 | 100% { 505 | -webkit-transform: scale(1); 506 | -moz-transform: scale(1); 507 | -ms-transform: scale(1); 508 | transform: scale(1); } } 509 | @keyframes pop { 510 | 0% { 511 | -webkit-transform: scale(0); 512 | -moz-transform: scale(0); 513 | -ms-transform: scale(0); 514 | transform: scale(0); } 515 | 50% { 516 | -webkit-transform: scale(1.2); 517 | -moz-transform: scale(1.2); 518 | -ms-transform: scale(1.2); 519 | transform: scale(1.2); } 520 | 100% { 521 | -webkit-transform: scale(1); 522 | -moz-transform: scale(1); 523 | -ms-transform: scale(1); 524 | transform: scale(1); } } 525 | .tile-merged .tile-inner { 526 | z-index: 20; 527 | -webkit-animation: pop 200ms ease 100ms; 528 | -moz-animation: pop 200ms ease 100ms; 529 | animation: pop 200ms ease 100ms; 530 | -webkit-animation-fill-mode: backwards; 531 | -moz-animation-fill-mode: backwards; 532 | animation-fill-mode: backwards; } 533 | 534 | .above-game:after { 535 | content: ""; 536 | display: block; 537 | clear: both; } 538 | 539 | .game-intro { 540 | float: left; 541 | line-height: 42px; 542 | margin-bottom: 0; } 543 | 544 | .restart-button { 545 | display: inline-block; 546 | background: #8f7a66; 547 | border-radius: 3px; 548 | padding: 0 20px; 549 | text-decoration: none; 550 | color: #f9f6f2; 551 | height: 40px; 552 | line-height: 42px; 553 | display: block; 554 | text-align: center; 555 | float: right; } 556 | 557 | .game-explanation { 558 | margin-top: 50px; } 559 | 560 | @media screen and (max-width: 520px) { 561 | html, body { 562 | font-size: 15px; } 563 | 564 | body { 565 | margin: 20px 0; 566 | padding: 0 20px; } 567 | 568 | h1.title { 569 | font-size: 27px; 570 | margin-top: 15px; } 571 | 572 | .container { 573 | width: 280px; 574 | margin: 0 auto; } 575 | 576 | .score-container, .best-container { 577 | margin-top: 0; 578 | padding: 15px 10px; 579 | min-width: 30px; } 580 | 581 | .score-container { 582 | min-width: 47px; 583 | } 584 | 585 | .score-container:after { 586 | content: "Hair loss"; 587 | font-size: 11px; } 588 | 589 | .best-container:after { 590 | content: "Max"; } 591 | 592 | .heading { 593 | margin-bottom: 10px; } 594 | 595 | .game-intro { 596 | width: 67%; 597 | display: block; 598 | box-sizing: border-box; 599 | line-height: 1.65; } 600 | 601 | .restart-button { 602 | width: 30%; 603 | padding: 0; 604 | display: block; 605 | box-sizing: border-box; 606 | margin-top: 2px; } 607 | 608 | .game-container { 609 | margin-top: 17px; 610 | position: relative; 611 | padding: 10px; 612 | cursor: default; 613 | -webkit-touch-callout: none; 614 | -ms-touch-callout: none; 615 | -webkit-user-select: none; 616 | -moz-user-select: none; 617 | -ms-user-select: none; 618 | -ms-touch-action: none; 619 | touch-action: none; 620 | background: #bbada0; 621 | border-radius: 6px; 622 | width: 280px; 623 | height: 280px; 624 | -webkit-box-sizing: border-box; 625 | -moz-box-sizing: border-box; 626 | box-sizing: border-box; } 627 | .game-container .game-message { 628 | display: none; 629 | position: absolute; 630 | top: 0; 631 | right: 0; 632 | bottom: 0; 633 | left: 0; 634 | background: rgba(238, 228, 218, 0.5); 635 | z-index: 100; 636 | text-align: center; 637 | -webkit-animation: fade-in 800ms ease 1200ms; 638 | -moz-animation: fade-in 800ms ease 1200ms; 639 | animation: fade-in 800ms ease 1200ms; 640 | -webkit-animation-fill-mode: both; 641 | -moz-animation-fill-mode: both; 642 | animation-fill-mode: both; } 643 | .game-container .game-message p { 644 | font-size: 60px; 645 | font-weight: bold; 646 | height: 60px; 647 | line-height: 60px; 648 | margin-top: 222px; } 649 | .game-container .game-message .lower { 650 | display: block; 651 | line-height: 3; 652 | margin-top: 59px; } 653 | .game-container .game-message a { 654 | display: inline-block; 655 | background: #8f7a66; 656 | border-radius: 3px; 657 | padding: 0 20px; 658 | text-decoration: none; 659 | color: #f9f6f2; 660 | height: 40px; 661 | line-height: 42px; 662 | margin-left: 9px; } 663 | .game-container .game-message a.keep-playing-button { 664 | display: none; } 665 | .game-container .game-message.game-won { 666 | background: rgba(237, 194, 46, 0.5); 667 | color: #f9f6f2; } 668 | .game-container .game-message.game-won a.keep-playing-button { 669 | display: inline-block; } 670 | .game-container .game-message.game-won, .game-container .game-message.game-over { 671 | display: block; } 672 | 673 | .grid-container { 674 | position: absolute; 675 | z-index: 1; } 676 | 677 | .grid-row { 678 | margin-bottom: 10px; } 679 | .grid-row:last-child { 680 | margin-bottom: 0; } 681 | .grid-row:after { 682 | content: ""; 683 | display: block; 684 | clear: both; } 685 | 686 | .grid-cell { 687 | width: 57.5px; 688 | height: 57.5px; 689 | margin-right: 10px; 690 | float: left; 691 | border-radius: 3px; 692 | background: rgba(238, 228, 218, 0.35); } 693 | .grid-cell:last-child { 694 | margin-right: 0; } 695 | 696 | .tile-container { 697 | position: absolute; 698 | z-index: 2; } 699 | 700 | .tile, .tile .tile-inner { 701 | width: 58px; 702 | height: 58px; 703 | line-height: 58px; } 704 | .tile.tile-position-1-1 { 705 | -webkit-transform: translate(0px, 0px); 706 | -moz-transform: translate(0px, 0px); 707 | -ms-transform: translate(0px, 0px); 708 | transform: translate(0px, 0px); } 709 | .tile.tile-position-1-2 { 710 | -webkit-transform: translate(0px, 67px); 711 | -moz-transform: translate(0px, 67px); 712 | -ms-transform: translate(0px, 67px); 713 | transform: translate(0px, 67px); } 714 | .tile.tile-position-1-3 { 715 | -webkit-transform: translate(0px, 135px); 716 | -moz-transform: translate(0px, 135px); 717 | -ms-transform: translate(0px, 135px); 718 | transform: translate(0px, 135px); } 719 | .tile.tile-position-1-4 { 720 | -webkit-transform: translate(0px, 202px); 721 | -moz-transform: translate(0px, 202px); 722 | -ms-transform: translate(0px, 202px); 723 | transform: translate(0px, 202px); } 724 | .tile.tile-position-2-1 { 725 | -webkit-transform: translate(67px, 0px); 726 | -moz-transform: translate(67px, 0px); 727 | -ms-transform: translate(67px, 0px); 728 | transform: translate(67px, 0px); } 729 | .tile.tile-position-2-2 { 730 | -webkit-transform: translate(67px, 67px); 731 | -moz-transform: translate(67px, 67px); 732 | -ms-transform: translate(67px, 67px); 733 | transform: translate(67px, 67px); } 734 | .tile.tile-position-2-3 { 735 | -webkit-transform: translate(67px, 135px); 736 | -moz-transform: translate(67px, 135px); 737 | -ms-transform: translate(67px, 135px); 738 | transform: translate(67px, 135px); } 739 | .tile.tile-position-2-4 { 740 | -webkit-transform: translate(67px, 202px); 741 | -moz-transform: translate(67px, 202px); 742 | -ms-transform: translate(67px, 202px); 743 | transform: translate(67px, 202px); } 744 | .tile.tile-position-3-1 { 745 | -webkit-transform: translate(135px, 0px); 746 | -moz-transform: translate(135px, 0px); 747 | -ms-transform: translate(135px, 0px); 748 | transform: translate(135px, 0px); } 749 | .tile.tile-position-3-2 { 750 | -webkit-transform: translate(135px, 67px); 751 | -moz-transform: translate(135px, 67px); 752 | -ms-transform: translate(135px, 67px); 753 | transform: translate(135px, 67px); } 754 | .tile.tile-position-3-3 { 755 | -webkit-transform: translate(135px, 135px); 756 | -moz-transform: translate(135px, 135px); 757 | -ms-transform: translate(135px, 135px); 758 | transform: translate(135px, 135px); } 759 | .tile.tile-position-3-4 { 760 | -webkit-transform: translate(135px, 202px); 761 | -moz-transform: translate(135px, 202px); 762 | -ms-transform: translate(135px, 202px); 763 | transform: translate(135px, 202px); } 764 | .tile.tile-position-4-1 { 765 | -webkit-transform: translate(202px, 0px); 766 | -moz-transform: translate(202px, 0px); 767 | -ms-transform: translate(202px, 0px); 768 | transform: translate(202px, 0px); } 769 | .tile.tile-position-4-2 { 770 | -webkit-transform: translate(202px, 67px); 771 | -moz-transform: translate(202px, 67px); 772 | -ms-transform: translate(202px, 67px); 773 | transform: translate(202px, 67px); } 774 | .tile.tile-position-4-3 { 775 | -webkit-transform: translate(202px, 135px); 776 | -moz-transform: translate(202px, 135px); 777 | -ms-transform: translate(202px, 135px); 778 | transform: translate(202px, 135px); } 779 | .tile.tile-position-4-4 { 780 | -webkit-transform: translate(202px, 202px); 781 | -moz-transform: translate(202px, 202px); 782 | -ms-transform: translate(202px, 202px); 783 | transform: translate(202px, 202px); } 784 | 785 | .tile .tile-inner { 786 | font-size: 35px; } 787 | 788 | .game-message p { 789 | font-size: 30px !important; 790 | height: 30px !important; 791 | line-height: 30px !important; 792 | margin-top: 90px !important; } 793 | .game-message .lower { 794 | margin-top: 30px !important; } } 795 | -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | --------------------------------------------------------------------------------