├── .gitignore ├── favicon.ico ├── meta ├── apple-touch-icon.png ├── apple-touch-startup-image-640x1096.png └── apple-touch-startup-image-640x920.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 ├── main.scss └── main.css ├── js ├── application.js ├── bind_polyfill.js ├── tile.js ├── animframe_polyfill.js ├── local_storage_manager.js ├── classlist_polyfill.js ├── grid.js ├── keyboard_input_manager.js ├── html_actuator.js └── game_manager.js ├── Rakefile ├── .jshintrc ├── LICENSE.txt ├── README.md ├── CONTRIBUTING.md └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache/ 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/favicon.ico -------------------------------------------------------------------------------- /meta/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/meta/apple-touch-icon.png -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/style/fonts/ClearSans-Bold-webfont.eot -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/style/fonts/ClearSans-Bold-webfont.woff -------------------------------------------------------------------------------- /style/fonts/ClearSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/style/fonts/ClearSans-Light-webfont.eot -------------------------------------------------------------------------------- /style/fonts/ClearSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/style/fonts/ClearSans-Light-webfont.woff -------------------------------------------------------------------------------- /meta/apple-touch-startup-image-640x1096.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/meta/apple-touch-startup-image-640x1096.png -------------------------------------------------------------------------------- /meta/apple-touch-startup-image-640x920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/meta/apple-touch-startup-image-640x920.png -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/style/fonts/ClearSans-Regular-webfont.eot -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/2048/master/style/fonts/ClearSans-Regular-webfont.woff -------------------------------------------------------------------------------- /js/application.js: -------------------------------------------------------------------------------- 1 | // Wait till the browser is ready to render the game (avoids glitches) 2 | window.requestAnimationFrame(function () { 3 | new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager); 4 | }); 5 | -------------------------------------------------------------------------------- /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 || 2; 5 | 6 | this.previousPosition = null; 7 | this.mergedFrom = null; // Tracks tiles that merged together 8 | } 9 | 10 | Tile.prototype.savePosition = function () { 11 | this.previousPosition = { x: this.x, y: this.y }; 12 | }; 13 | 14 | Tile.prototype.updatePosition = function (position) { 15 | this.x = position.x; 16 | this.y = position.y; 17 | }; 18 | 19 | Tile.prototype.serialize = function () { 20 | return { 21 | position: { 22 | x: this.x, 23 | y: this.y 24 | }, 25 | value: this.value 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /js/animframe_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var lastTime = 0; 3 | var vendors = ['webkit', 'moz']; 4 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 5 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 6 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || 7 | window[vendors[x] + 'CancelRequestAnimationFrame']; 8 | } 9 | 10 | if (!window.requestAnimationFrame) { 11 | window.requestAnimationFrame = function (callback) { 12 | var currTime = new Date().getTime(); 13 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 14 | var id = window.setTimeout(function () { 15 | callback(currTime + timeToCall); 16 | }, 17 | timeToCall); 18 | lastTime = currTime + timeToCall; 19 | return id; 20 | }; 21 | } 22 | 23 | if (!window.cancelAnimationFrame) { 24 | window.cancelAnimationFrame = function (id) { 25 | clearTimeout(id); 26 | }; 27 | } 28 | }()); 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Gabriele Cirulli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /style/fonts/clear-sans.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Clear Sans"; 3 | src: url("ClearSans-Light-webfont.eot"); 4 | src: url("ClearSans-Light-webfont.eot?#iefix") format("embedded-opentype"), 5 | url("ClearSans-Light-webfont.svg#clear_sans_lightregular") format("svg"), 6 | url("ClearSans-Light-webfont.woff") format("woff"); 7 | font-weight: 200; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: "Clear Sans"; 13 | src: url("ClearSans-Regular-webfont.eot"); 14 | src: url("ClearSans-Regular-webfont.eot?#iefix") format("embedded-opentype"), 15 | url("ClearSans-Regular-webfont.svg#clear_sansregular") format("svg"), 16 | url("ClearSans-Regular-webfont.woff") format("woff"); 17 | font-weight: normal; 18 | font-style: normal; 19 | } 20 | 21 | @font-face { 22 | font-family: "Clear Sans"; 23 | src: url("ClearSans-Bold-webfont.eot"); 24 | src: url("ClearSans-Bold-webfont.eot?#iefix") format("embedded-opentype"), 25 | url("ClearSans-Bold-webfont.svg#clear_sansbold") format("svg"), 26 | url("ClearSans-Bold-webfont.woff") format("woff"); 27 | font-weight: 700; 28 | font-style: normal; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /js/local_storage_manager.js: -------------------------------------------------------------------------------- 1 | window.fakeStorage = { 2 | _data: {}, 3 | 4 | setItem: function (id, val) { 5 | return this._data[id] = String(val); 6 | }, 7 | 8 | getItem: function (id) { 9 | return this._data.hasOwnProperty(id) ? this._data[id] : undefined; 10 | }, 11 | 12 | removeItem: function (id) { 13 | return delete this._data[id]; 14 | }, 15 | 16 | clear: function () { 17 | return this._data = {}; 18 | } 19 | }; 20 | 21 | function LocalStorageManager() { 22 | this.bestScoreKey = "bestScore"; 23 | this.gameStateKey = "gameState"; 24 | 25 | var supported = this.localStorageSupported(); 26 | this.storage = supported ? window.localStorage : window.fakeStorage; 27 | } 28 | 29 | LocalStorageManager.prototype.localStorageSupported = function () { 30 | var testKey = "test"; 31 | var storage = window.localStorage; 32 | 33 | try { 34 | storage.setItem(testKey, "1"); 35 | storage.removeItem(testKey); 36 | return true; 37 | } catch (error) { 38 | return false; 39 | } 40 | }; 41 | 42 | // Best score getters/setters 43 | LocalStorageManager.prototype.getBestScore = function () { 44 | return this.storage.getItem(this.bestScoreKey) || 0; 45 | }; 46 | 47 | LocalStorageManager.prototype.setBestScore = function (score) { 48 | this.storage.setItem(this.bestScoreKey, score); 49 | }; 50 | 51 | // Game state getters/setters and clearing 52 | LocalStorageManager.prototype.getGameState = function () { 53 | var stateJSON = this.storage.getItem(this.gameStateKey); 54 | return stateJSON ? JSON.parse(stateJSON) : null; 55 | }; 56 | 57 | LocalStorageManager.prototype.setGameState = function (gameState) { 58 | this.storage.setItem(this.gameStateKey, JSON.stringify(gameState)); 59 | }; 60 | 61 | LocalStorageManager.prototype.clearGameState = function () { 62 | this.storage.removeItem(this.gameStateKey); 63 | }; 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2048 2 | A small clone of [1024](https://play.google.com/store/apps/details?id=com.veewo.a1024), based on [Saming's 2048](http://saming.fr/p/2048/) (also a clone). 3 | 4 | Made just for fun. [Play it here!](http://gabrielecirulli.github.io/2048/) 5 | 6 | ### Contributions 7 | 8 | - [TimPetricola](https://github.com/TimPetricola) added best score storage 9 | - [chrisprice](https://github.com/chrisprice) added custom code for swipe handling on mobile 10 | - [elektryk](https://github.com/elektryk) made swipes work on Windows Phone 11 | - [mgarciaisaia](https://github.com/mgarciaisaia) addes support for Android 2.3 12 | 13 | Many thanks to [rayhaanj](https://github.com/rayhaanj), [Mechazawa](https://github.com/Mechazawa), [grant](https://github.com/grant), [remram44](https://github.com/remram44) and [ghoullier](https://github.com/ghoullier) for the many other good contributions. 14 | 15 | ### Screenshot 16 | 17 |

18 | Screenshot 19 |

20 | 21 | That screenshot is fake, by the way. I never reached 2048 :smile: 22 | 23 | ## Contributing 24 | Changes and improvements are more than welcome! Feel free to fork and open a pull request. Please make your changes in a specific branch and request to pull into `master`! If you can, please make sure the game fully works before sending the PR, as that will help speed up the process. 25 | 26 | You can find the same information in the [contributing guide.](https://github.com/gabrielecirulli/2048/blob/master/CONTRIBUTING.md) 27 | 28 | ## License 29 | 2048 is licensed under the [MIT license.](https://github.com/gabrielecirulli/2048/blob/master/LICENSE.txt) 30 | 31 | ## Donations 32 | I made this in my spare time, and it's hosted on GitHub (which means I don't have any hosting costs), but if you enjoyed the game and feel like buying me coffee, you can donate at my BTC address: `1Ec6onfsQmoP9kkL3zkpB6c5sA4PVcXU2i`. Thank you very much! 33 | -------------------------------------------------------------------------------- /style/helpers.scss: -------------------------------------------------------------------------------- 1 | // Exponent 2 | // From: https://github.com/Team-Sass/Sassy-math/blob/master/sass/math.scss#L36 3 | 4 | @function exponent($base, $exponent) { 5 | // reset value 6 | $value: $base; 7 | // positive intergers get multiplied 8 | @if $exponent > 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 | transform: $args; 52 | } 53 | 54 | // Keyframe animations 55 | @mixin keyframes($animation-name) { 56 | @-webkit-keyframes $animation-name { 57 | @content; 58 | } 59 | @-moz-keyframes $animation-name { 60 | @content; 61 | } 62 | @keyframes $animation-name { 63 | @content; 64 | } 65 | } 66 | 67 | // Media queries 68 | @mixin smaller($width) { 69 | @media screen and (max-width: $width) { 70 | @content; 71 | } 72 | } 73 | 74 | // Clearfix 75 | @mixin clearfix { 76 | &:after { 77 | content: ""; 78 | display: block; 79 | clear: both; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Changes and improvements are more than welcome! Feel free to fork and open a pull request. 3 | 4 | Please follow the house rules to have a bigger chance of your contribution being merged. 5 | 6 | ## House rules 7 | 8 | ### How to make changes 9 | - To make changes, create a new branch based on `master` (do not create one from `gh-pages` unless strictly necessary) and make them there, then create a Pull Request to master. 10 | `gh-pages` is different from master in that it contains sharing features, analytics and other things that have no direct bearing with the game. `master` is the "pure" version of the game. 11 | - If you want to modify the CSS, please edit the SCSS files present in `style/`: `main.scss` and others. Don't edit the `main.css`, because it's supposed to be generated. 12 | In order to compile your SCSS modifications, you need to use the `sass` gem (install it by running `gem install sass` once Ruby is installed). 13 | To run SASS, simply use the following command: 14 | `sass --watch style/main.scss` 15 | SASS will automatically recompile your css when changed. 16 | - `Rakefile` contains some tasks that help during development. Feel free to add useful tasks if needed. 17 | - Please use 2-space indentation when editing the JavaScript. A `.jshintrc` file is present, which will help your code to follow the guidelines if you install and run `jshint`. 18 | - Please test your modification thouroughly before submitting your Pull Request. 19 | 20 | ### Changes that might not be accepted 21 | We have to be conservative with the core game. This means that some modifications won't be merged, or will have to be evaluated carefully before being merged: 22 | 23 | - Undo/redo features 24 | - Save/reload features 25 | - Changes to how the tiles look or their contents 26 | - Changes to the layout 27 | - Changes to the grid size 28 | 29 | ### Changes that are welcome 30 | - Bug fixes 31 | - Compatibility improvements 32 | - "Under the hood" enhancements 33 | - Small changes that don't have an impact on the core gameplay 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 tile = state[x][y]; 29 | row.push(tile ? new Tile(tile.position, tile.value) : null); 30 | } 31 | } 32 | 33 | return cells; 34 | }; 35 | 36 | // Find the first available random position 37 | Grid.prototype.randomAvailableCell = function () { 38 | var cells = this.availableCells(); 39 | 40 | if (cells.length) { 41 | return cells[Math.floor(Math.random() * cells.length)]; 42 | } 43 | }; 44 | 45 | Grid.prototype.availableCells = function () { 46 | var cells = []; 47 | 48 | this.eachCell(function (x, y, tile) { 49 | if (!tile) { 50 | cells.push({ x: x, y: y }); 51 | } 52 | }); 53 | 54 | return cells; 55 | }; 56 | 57 | // Call callback for every cell 58 | Grid.prototype.eachCell = function (callback) { 59 | for (var x = 0; x < this.size; x++) { 60 | for (var y = 0; y < this.size; y++) { 61 | callback(x, y, this.cells[x][y]); 62 | } 63 | } 64 | }; 65 | 66 | // Check if there are any cells available 67 | Grid.prototype.cellsAvailable = function () { 68 | return !!this.availableCells().length; 69 | }; 70 | 71 | // Check if the specified cell is taken 72 | Grid.prototype.cellAvailable = function (cell) { 73 | return !this.cellOccupied(cell); 74 | }; 75 | 76 | Grid.prototype.cellOccupied = function (cell) { 77 | return !!this.cellContent(cell); 78 | }; 79 | 80 | Grid.prototype.cellContent = function (cell) { 81 | if (this.withinBounds(cell)) { 82 | return this.cells[cell.x][cell.y]; 83 | } else { 84 | return null; 85 | } 86 | }; 87 | 88 | // Inserts a tile at its position 89 | Grid.prototype.insertTile = function (tile) { 90 | this.cells[tile.x][tile.y] = tile; 91 | }; 92 | 93 | Grid.prototype.removeTile = function (tile) { 94 | this.cells[tile.x][tile.y] = null; 95 | }; 96 | 97 | Grid.prototype.withinBounds = function (position) { 98 | return position.x >= 0 && position.x < this.size && 99 | position.y >= 0 && position.y < this.size; 100 | }; 101 | 102 | Grid.prototype.serialize = function () { 103 | var cellState = []; 104 | 105 | for (var x = 0; x < this.size; x++) { 106 | var row = cellState[x] = []; 107 | 108 | for (var y = 0; y < this.size; y++) { 109 | row.push(this.cells[x][y] ? this.cells[x][y].serialize() : null); 110 | } 111 | } 112 | 113 | return { 114 | size: this.size, 115 | cells: cellState 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2048 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

2048

23 |
24 |
0
25 |
0
26 |
27 |
28 | 29 |
30 |

Join the numbers and get to the 2048 tile!

31 | New Game 32 |
33 | 34 |
35 |
36 |

37 |
38 | Keep going 39 | Try again 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 | How to play: Use your arrow keys to move the tiles. When two tiles with the same number touch, they merge into one! 77 |

78 |
79 |

80 | Note: This site is the official version of 2048. You can play it on your phone via http://git.io/2048. All other apps or sites are derivatives or fakes, and should be used with caution. 81 |

82 |
83 |

84 | Created by Gabriele Cirulli. Based on 1024 by Veewo Studio and conceptually similar to Threes by Asher Vollmer. 85 |

86 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /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.getElementsByClassName("game-container")[0]; 79 | 80 | gameContainer.addEventListener(this.eventTouchstart, function (event) { 81 | if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || 82 | event.targetTouches > 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 > 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 | -------------------------------------------------------------------------------- /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 | 7 | this.score = 0; 8 | } 9 | 10 | HTMLActuator.prototype.actuate = function (grid, metadata) { 11 | var self = this; 12 | 13 | window.requestAnimationFrame(function () { 14 | self.clearContainer(self.tileContainer); 15 | 16 | grid.cells.forEach(function (column) { 17 | column.forEach(function (cell) { 18 | if (cell) { 19 | self.addTile(cell); 20 | } 21 | }); 22 | }); 23 | 24 | self.updateScore(metadata.score); 25 | self.updateBestScore(metadata.bestScore); 26 | 27 | if (metadata.terminated) { 28 | if (metadata.over) { 29 | self.message(false); // You lose 30 | } else if (metadata.won) { 31 | self.message(true); // You win! 32 | } 33 | } 34 | 35 | }); 36 | }; 37 | 38 | // Continues the game (both restart and keep playing) 39 | HTMLActuator.prototype.continueGame = function () { 40 | this.clearMessage(); 41 | }; 42 | 43 | HTMLActuator.prototype.clearContainer = function (container) { 44 | while (container.firstChild) { 45 | container.removeChild(container.firstChild); 46 | } 47 | }; 48 | 49 | HTMLActuator.prototype.addTile = function (tile) { 50 | var self = this; 51 | 52 | var wrapper = document.createElement("div"); 53 | var inner = document.createElement("div"); 54 | var position = tile.previousPosition || { x: tile.x, y: tile.y }; 55 | var positionClass = this.positionClass(position); 56 | 57 | // We can't use classlist because it somehow glitches when replacing classes 58 | var classes = ["tile", "tile-" + tile.value, positionClass]; 59 | 60 | if (tile.value > 2048) classes.push("tile-super"); 61 | 62 | this.applyClasses(wrapper, classes); 63 | 64 | inner.classList.add("tile-inner"); 65 | inner.textContent = tile.value; 66 | 67 | if (tile.previousPosition) { 68 | // Make sure that the tile gets rendered in the previous position first 69 | window.requestAnimationFrame(function () { 70 | classes[2] = self.positionClass({ x: tile.x, y: tile.y }); 71 | self.applyClasses(wrapper, classes); // Update the position 72 | }); 73 | } else if (tile.mergedFrom) { 74 | classes.push("tile-merged"); 75 | this.applyClasses(wrapper, classes); 76 | 77 | // Render the tiles that merged 78 | tile.mergedFrom.forEach(function (merged) { 79 | self.addTile(merged); 80 | }); 81 | } else { 82 | classes.push("tile-new"); 83 | this.applyClasses(wrapper, classes); 84 | } 85 | 86 | // Add the inner part of the tile to the wrapper 87 | wrapper.appendChild(inner); 88 | 89 | // Put the tile on the board 90 | this.tileContainer.appendChild(wrapper); 91 | }; 92 | 93 | HTMLActuator.prototype.applyClasses = function (element, classes) { 94 | element.setAttribute("class", classes.join(" ")); 95 | }; 96 | 97 | HTMLActuator.prototype.normalizePosition = function (position) { 98 | return { x: position.x + 1, y: position.y + 1 }; 99 | }; 100 | 101 | HTMLActuator.prototype.positionClass = function (position) { 102 | position = this.normalizePosition(position); 103 | return "tile-position-" + position.x + "-" + position.y; 104 | }; 105 | 106 | HTMLActuator.prototype.updateScore = function (score) { 107 | this.clearContainer(this.scoreContainer); 108 | 109 | var difference = score - this.score; 110 | this.score = score; 111 | 112 | this.scoreContainer.textContent = this.score; 113 | 114 | if (difference > 0) { 115 | var addition = document.createElement("div"); 116 | addition.classList.add("score-addition"); 117 | addition.textContent = "+" + difference; 118 | 119 | this.scoreContainer.appendChild(addition); 120 | } 121 | }; 122 | 123 | HTMLActuator.prototype.updateBestScore = function (bestScore) { 124 | this.bestContainer.textContent = bestScore; 125 | }; 126 | 127 | HTMLActuator.prototype.message = function (won) { 128 | var type = won ? "game-won" : "game-over"; 129 | var message = won ? "You win!" : "Game over!"; 130 | 131 | this.messageContainer.classList.add(type); 132 | this.messageContainer.getElementsByTagName("p")[0].textContent = message; 133 | }; 134 | 135 | HTMLActuator.prototype.clearMessage = function () { 136 | // IE only takes one value to remove at a time. 137 | this.messageContainer.classList.remove("game-won"); 138 | this.messageContainer.classList.remove("game-over"); 139 | }; 140 | -------------------------------------------------------------------------------- /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 | 9 | this.inputManager.on("move", this.move.bind(this)); 10 | this.inputManager.on("restart", this.restart.bind(this)); 11 | this.inputManager.on("keepPlaying", this.keepPlaying.bind(this)); 12 | 13 | this.setup(); 14 | } 15 | 16 | // Restart the game 17 | GameManager.prototype.restart = function () { 18 | this.storageManager.clearGameState(); 19 | this.actuator.continueGame(); // Clear the game won/lost message 20 | this.setup(); 21 | }; 22 | 23 | // Keep playing after winning (allows going over 2048) 24 | GameManager.prototype.keepPlaying = function () { 25 | this.keepPlaying = true; 26 | this.actuator.continueGame(); // Clear the game won/lost message 27 | }; 28 | 29 | // Return true if the game is lost, or has won and the user hasn't kept playing 30 | GameManager.prototype.isGameTerminated = function () { 31 | return this.over || (this.won && !this.keepPlaying); 32 | }; 33 | 34 | // Set up the game 35 | GameManager.prototype.setup = function () { 36 | var previousState = this.storageManager.getGameState(); 37 | 38 | // Reload the game from a previous game if present 39 | if (previousState) { 40 | this.grid = new Grid(previousState.grid.size, 41 | previousState.grid.cells); // Reload grid 42 | this.score = previousState.score; 43 | this.over = previousState.over; 44 | this.won = previousState.won; 45 | this.keepPlaying = previousState.keepPlaying; 46 | } else { 47 | this.grid = new Grid(this.size); 48 | this.score = 0; 49 | this.over = false; 50 | this.won = false; 51 | this.keepPlaying = false; 52 | 53 | // Add the initial tiles 54 | this.addStartTiles(); 55 | } 56 | 57 | // Update the actuator 58 | this.actuate(); 59 | }; 60 | 61 | // Set up the initial tiles to start the game with 62 | GameManager.prototype.addStartTiles = function () { 63 | for (var i = 0; i < this.startTiles; i++) { 64 | this.addRandomTile(); 65 | } 66 | }; 67 | 68 | // Adds a tile in a random position 69 | GameManager.prototype.addRandomTile = function () { 70 | if (this.grid.cellsAvailable()) { 71 | var value = Math.random() < 0.9 ? 2 : 4; 72 | var tile = new Tile(this.grid.randomAvailableCell(), value); 73 | 74 | this.grid.insertTile(tile); 75 | } 76 | }; 77 | 78 | // Sends the updated grid to the actuator 79 | GameManager.prototype.actuate = function () { 80 | if (this.storageManager.getBestScore() < this.score) { 81 | this.storageManager.setBestScore(this.score); 82 | } 83 | 84 | // Clear the state when the game is over (game over only, not win) 85 | if (this.over) { 86 | this.storageManager.clearGameState(); 87 | } else { 88 | this.storageManager.setGameState(this.serialize()); 89 | } 90 | 91 | this.actuator.actuate(this.grid, { 92 | score: this.score, 93 | over: this.over, 94 | won: this.won, 95 | bestScore: this.storageManager.getBestScore(), 96 | terminated: this.isGameTerminated() 97 | }); 98 | 99 | }; 100 | 101 | // Represent the current game as an object 102 | GameManager.prototype.serialize = function () { 103 | return { 104 | grid: this.grid.serialize(), 105 | score: this.score, 106 | over: this.over, 107 | won: this.won, 108 | keepPlaying: this.keepPlaying 109 | }; 110 | }; 111 | 112 | // Save all tile positions and remove merger info 113 | GameManager.prototype.prepareTiles = function () { 114 | this.grid.eachCell(function (x, y, tile) { 115 | if (tile) { 116 | tile.mergedFrom = null; 117 | tile.savePosition(); 118 | } 119 | }); 120 | }; 121 | 122 | // Move a tile and its representation 123 | GameManager.prototype.moveTile = function (tile, cell) { 124 | this.grid.cells[tile.x][tile.y] = null; 125 | this.grid.cells[cell.x][cell.y] = tile; 126 | tile.updatePosition(cell); 127 | }; 128 | 129 | // Move tiles on the grid in the specified direction 130 | GameManager.prototype.move = function (direction) { 131 | // 0: up, 1: right, 2: down, 3: left 132 | var self = this; 133 | 134 | if (this.isGameTerminated()) return; // Don't do anything if the game's over 135 | 136 | var cell, tile; 137 | 138 | var vector = this.getVector(direction); 139 | var traversals = this.buildTraversals(vector); 140 | var moved = false; 141 | 142 | // Save the current tile positions and remove merger information 143 | this.prepareTiles(); 144 | 145 | // Traverse the grid in the right direction and move tiles 146 | traversals.x.forEach(function (x) { 147 | traversals.y.forEach(function (y) { 148 | cell = { x: x, y: y }; 149 | tile = self.grid.cellContent(cell); 150 | 151 | if (tile) { 152 | var positions = self.findFarthestPosition(cell, vector); 153 | var next = self.grid.cellContent(positions.next); 154 | 155 | // Only one merger per row traversal? 156 | if (next && next.value === tile.value && !next.mergedFrom) { 157 | var merged = new Tile(positions.next, tile.value * 2); 158 | merged.mergedFrom = [tile, next]; 159 | 160 | self.grid.insertTile(merged); 161 | self.grid.removeTile(tile); 162 | 163 | // Converge the two tiles' positions 164 | tile.updatePosition(positions.next); 165 | 166 | // Update the score 167 | self.score += merged.value; 168 | 169 | // The mighty 2048 tile 170 | if (merged.value === 2048) self.won = true; 171 | } else { 172 | self.moveTile(tile, positions.farthest); 173 | } 174 | 175 | if (!self.positionsEqual(cell, tile)) { 176 | moved = true; // The tile moved from its original cell! 177 | } 178 | } 179 | }); 180 | }); 181 | 182 | if (moved) { 183 | this.addRandomTile(); 184 | 185 | if (!this.movesAvailable()) { 186 | this.over = true; // Game over! 187 | } 188 | 189 | this.actuate(); 190 | } 191 | }; 192 | 193 | // Get the vector representing the chosen direction 194 | GameManager.prototype.getVector = function (direction) { 195 | // Vectors representing tile movement 196 | var map = { 197 | 0: { x: 0, y: -1 }, // Up 198 | 1: { x: 1, y: 0 }, // Right 199 | 2: { x: 0, y: 1 }, // Down 200 | 3: { x: -1, y: 0 } // Left 201 | }; 202 | 203 | return map[direction]; 204 | }; 205 | 206 | // Build a list of positions to traverse in the right order 207 | GameManager.prototype.buildTraversals = function (vector) { 208 | var traversals = { x: [], y: [] }; 209 | 210 | for (var pos = 0; pos < this.size; pos++) { 211 | traversals.x.push(pos); 212 | traversals.y.push(pos); 213 | } 214 | 215 | // Always traverse from the farthest cell in the chosen direction 216 | if (vector.x === 1) traversals.x = traversals.x.reverse(); 217 | if (vector.y === 1) traversals.y = traversals.y.reverse(); 218 | 219 | return traversals; 220 | }; 221 | 222 | GameManager.prototype.findFarthestPosition = function (cell, vector) { 223 | var previous; 224 | 225 | // Progress towards the vector direction until an obstacle is found 226 | do { 227 | previous = cell; 228 | cell = { x: previous.x + vector.x, y: previous.y + vector.y }; 229 | } while (this.grid.withinBounds(cell) && 230 | this.grid.cellAvailable(cell)); 231 | 232 | return { 233 | farthest: previous, 234 | next: cell // Used to check if a merge is required 235 | }; 236 | }; 237 | 238 | GameManager.prototype.movesAvailable = function () { 239 | return this.grid.cellsAvailable() || this.tileMatchesAvailable(); 240 | }; 241 | 242 | // Check for available matches between tiles (more expensive check) 243 | GameManager.prototype.tileMatchesAvailable = function () { 244 | var self = this; 245 | 246 | var tile; 247 | 248 | for (var x = 0; x < this.size; x++) { 249 | for (var y = 0; y < this.size; y++) { 250 | tile = this.grid.cellContent({ x: x, y: y }); 251 | 252 | if (tile) { 253 | for (var direction = 0; direction < 4; direction++) { 254 | var vector = self.getVector(direction); 255 | var cell = { x: x + vector.x, y: y + vector.y }; 256 | 257 | var other = self.grid.cellContent(cell); 258 | 259 | if (other && other.value === tile.value) { 260 | return true; // These two tiles can be merged 261 | } 262 | } 263 | } 264 | } 265 | } 266 | 267 | return false; 268 | }; 269 | 270 | GameManager.prototype.positionsEqual = function (first, second) { 271 | return first.x === second.x && first.y === second.y; 272 | }; 273 | -------------------------------------------------------------------------------- /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: $tile-size + 10px; 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 | -------------------------------------------------------------------------------- /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, 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 | 30 | 100% { 31 | top: -50px; 32 | opacity: 0; } } 33 | @-moz-keyframes move-up { 34 | 0% { 35 | top: 25px; 36 | opacity: 1; } 37 | 38 | 100% { 39 | top: -50px; 40 | opacity: 0; } } 41 | @keyframes move-up { 42 | 0% { 43 | top: 25px; 44 | opacity: 1; } 45 | 46 | 100% { 47 | top: -50px; 48 | opacity: 0; } } 49 | .scores-container { 50 | float: right; 51 | text-align: right; } 52 | 53 | .score-container, .best-container { 54 | position: relative; 55 | display: inline-block; 56 | background: #bbada0; 57 | padding: 15px 25px; 58 | font-size: 25px; 59 | height: 25px; 60 | line-height: 47px; 61 | font-weight: bold; 62 | border-radius: 3px; 63 | color: white; 64 | margin-top: 8px; 65 | text-align: center; } 66 | .score-container:after, .best-container:after { 67 | position: absolute; 68 | width: 100%; 69 | top: 10px; 70 | left: 0; 71 | text-transform: uppercase; 72 | font-size: 13px; 73 | line-height: 13px; 74 | text-align: center; 75 | color: #eee4da; } 76 | .score-container .score-addition, .best-container .score-addition { 77 | position: absolute; 78 | right: 30px; 79 | color: red; 80 | font-size: 25px; 81 | line-height: 25px; 82 | font-weight: bold; 83 | color: rgba(119, 110, 101, 0.9); 84 | z-index: 100; 85 | -webkit-animation: move-up 600ms ease-in; 86 | -moz-animation: move-up 600ms ease-in; 87 | animation: move-up 600ms ease-in; 88 | -webkit-animation-fill-mode: both; 89 | -moz-animation-fill-mode: both; 90 | animation-fill-mode: both; } 91 | 92 | .score-container:after { 93 | content: "Score"; } 94 | 95 | .best-container:after { 96 | content: "Best"; } 97 | 98 | p { 99 | margin-top: 0; 100 | margin-bottom: 10px; 101 | line-height: 1.65; } 102 | 103 | a { 104 | color: #776e65; 105 | font-weight: bold; 106 | text-decoration: underline; 107 | cursor: pointer; } 108 | 109 | strong.important { 110 | text-transform: uppercase; } 111 | 112 | hr { 113 | border: none; 114 | border-bottom: 1px solid #d8d4d0; 115 | margin-top: 20px; 116 | margin-bottom: 30px; } 117 | 118 | .container { 119 | width: 500px; 120 | margin: 0 auto; } 121 | 122 | @-webkit-keyframes fade-in { 123 | 0% { 124 | opacity: 0; } 125 | 126 | 100% { 127 | opacity: 1; } } 128 | @-moz-keyframes fade-in { 129 | 0% { 130 | opacity: 0; } 131 | 132 | 100% { 133 | opacity: 1; } } 134 | @keyframes fade-in { 135 | 0% { 136 | opacity: 0; } 137 | 138 | 100% { 139 | opacity: 1; } } 140 | .game-container { 141 | margin-top: 40px; 142 | position: relative; 143 | padding: 15px; 144 | cursor: default; 145 | -webkit-touch-callout: none; 146 | -ms-touch-callout: none; 147 | -webkit-user-select: none; 148 | -moz-user-select: none; 149 | -ms-user-select: none; 150 | -ms-touch-action: none; 151 | touch-action: none; 152 | background: #bbada0; 153 | border-radius: 6px; 154 | width: 500px; 155 | height: 500px; 156 | -webkit-box-sizing: border-box; 157 | -moz-box-sizing: border-box; 158 | box-sizing: border-box; } 159 | .game-container .game-message { 160 | display: none; 161 | position: absolute; 162 | top: 0; 163 | right: 0; 164 | bottom: 0; 165 | left: 0; 166 | background: rgba(238, 228, 218, 0.5); 167 | z-index: 100; 168 | text-align: center; 169 | -webkit-animation: fade-in 800ms ease 1200ms; 170 | -moz-animation: fade-in 800ms ease 1200ms; 171 | animation: fade-in 800ms ease 1200ms; 172 | -webkit-animation-fill-mode: both; 173 | -moz-animation-fill-mode: both; 174 | animation-fill-mode: both; } 175 | .game-container .game-message p { 176 | font-size: 60px; 177 | font-weight: bold; 178 | height: 60px; 179 | line-height: 60px; 180 | margin-top: 222px; } 181 | .game-container .game-message .lower { 182 | display: block; 183 | margin-top: 59px; } 184 | .game-container .game-message a { 185 | display: inline-block; 186 | background: #8f7a66; 187 | border-radius: 3px; 188 | padding: 0 20px; 189 | text-decoration: none; 190 | color: #f9f6f2; 191 | height: 40px; 192 | line-height: 42px; 193 | margin-left: 9px; } 194 | .game-container .game-message a.keep-playing-button { 195 | display: none; } 196 | .game-container .game-message.game-won { 197 | background: rgba(237, 194, 46, 0.5); 198 | color: #f9f6f2; } 199 | .game-container .game-message.game-won a.keep-playing-button { 200 | display: inline-block; } 201 | .game-container .game-message.game-won, .game-container .game-message.game-over { 202 | display: block; } 203 | 204 | .grid-container { 205 | position: absolute; 206 | z-index: 1; } 207 | 208 | .grid-row { 209 | margin-bottom: 15px; } 210 | .grid-row:last-child { 211 | margin-bottom: 0; } 212 | .grid-row:after { 213 | content: ""; 214 | display: block; 215 | clear: both; } 216 | 217 | .grid-cell { 218 | width: 106.25px; 219 | height: 106.25px; 220 | margin-right: 15px; 221 | float: left; 222 | border-radius: 3px; 223 | background: rgba(238, 228, 218, 0.35); } 224 | .grid-cell:last-child { 225 | margin-right: 0; } 226 | 227 | .tile-container { 228 | position: absolute; 229 | z-index: 2; } 230 | 231 | .tile, .tile .tile-inner { 232 | width: 107px; 233 | height: 107px; 234 | line-height: 116.25px; } 235 | .tile.tile-position-1-1 { 236 | -webkit-transform: translate(0px, 0px); 237 | -moz-transform: translate(0px, 0px); 238 | transform: translate(0px, 0px); } 239 | .tile.tile-position-1-2 { 240 | -webkit-transform: translate(0px, 121px); 241 | -moz-transform: translate(0px, 121px); 242 | transform: translate(0px, 121px); } 243 | .tile.tile-position-1-3 { 244 | -webkit-transform: translate(0px, 242px); 245 | -moz-transform: translate(0px, 242px); 246 | transform: translate(0px, 242px); } 247 | .tile.tile-position-1-4 { 248 | -webkit-transform: translate(0px, 363px); 249 | -moz-transform: translate(0px, 363px); 250 | transform: translate(0px, 363px); } 251 | .tile.tile-position-2-1 { 252 | -webkit-transform: translate(121px, 0px); 253 | -moz-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 | transform: translate(121px, 121px); } 259 | .tile.tile-position-2-3 { 260 | -webkit-transform: translate(121px, 242px); 261 | -moz-transform: translate(121px, 242px); 262 | transform: translate(121px, 242px); } 263 | .tile.tile-position-2-4 { 264 | -webkit-transform: translate(121px, 363px); 265 | -moz-transform: translate(121px, 363px); 266 | transform: translate(121px, 363px); } 267 | .tile.tile-position-3-1 { 268 | -webkit-transform: translate(242px, 0px); 269 | -moz-transform: translate(242px, 0px); 270 | transform: translate(242px, 0px); } 271 | .tile.tile-position-3-2 { 272 | -webkit-transform: translate(242px, 121px); 273 | -moz-transform: translate(242px, 121px); 274 | transform: translate(242px, 121px); } 275 | .tile.tile-position-3-3 { 276 | -webkit-transform: translate(242px, 242px); 277 | -moz-transform: translate(242px, 242px); 278 | transform: translate(242px, 242px); } 279 | .tile.tile-position-3-4 { 280 | -webkit-transform: translate(242px, 363px); 281 | -moz-transform: translate(242px, 363px); 282 | transform: translate(242px, 363px); } 283 | .tile.tile-position-4-1 { 284 | -webkit-transform: translate(363px, 0px); 285 | -moz-transform: translate(363px, 0px); 286 | transform: translate(363px, 0px); } 287 | .tile.tile-position-4-2 { 288 | -webkit-transform: translate(363px, 121px); 289 | -moz-transform: translate(363px, 121px); 290 | transform: translate(363px, 121px); } 291 | .tile.tile-position-4-3 { 292 | -webkit-transform: translate(363px, 242px); 293 | -moz-transform: translate(363px, 242px); 294 | transform: translate(363px, 242px); } 295 | .tile.tile-position-4-4 { 296 | -webkit-transform: translate(363px, 363px); 297 | -moz-transform: translate(363px, 363px); 298 | transform: translate(363px, 363px); } 299 | 300 | .tile { 301 | position: absolute; 302 | -webkit-transition: 100ms ease-in-out; 303 | -moz-transition: 100ms ease-in-out; 304 | transition: 100ms ease-in-out; 305 | -webkit-transition-property: -webkit-transform; 306 | -moz-transition-property: -moz-transform; 307 | transition-property: transform; } 308 | .tile .tile-inner { 309 | border-radius: 3px; 310 | background: #eee4da; 311 | text-align: center; 312 | font-weight: bold; 313 | z-index: 10; 314 | font-size: 55px; } 315 | .tile.tile-2 .tile-inner { 316 | background: #eee4da; 317 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); } 318 | .tile.tile-4 .tile-inner { 319 | background: #ede0c8; 320 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); } 321 | .tile.tile-8 .tile-inner { 322 | color: #f9f6f2; 323 | background: #f2b179; } 324 | .tile.tile-16 .tile-inner { 325 | color: #f9f6f2; 326 | background: #f59563; } 327 | .tile.tile-32 .tile-inner { 328 | color: #f9f6f2; 329 | background: #f67c5f; } 330 | .tile.tile-64 .tile-inner { 331 | color: #f9f6f2; 332 | background: #f65e3b; } 333 | .tile.tile-128 .tile-inner { 334 | color: #f9f6f2; 335 | background: #edcf72; 336 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.2381), inset 0 0 0 1px rgba(255, 255, 255, 0.14286); 337 | font-size: 45px; } 338 | @media screen and (max-width: 520px) { 339 | .tile.tile-128 .tile-inner { 340 | font-size: 25px; } } 341 | .tile.tile-256 .tile-inner { 342 | color: #f9f6f2; 343 | background: #edcc61; 344 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.31746), inset 0 0 0 1px rgba(255, 255, 255, 0.19048); 345 | font-size: 45px; } 346 | @media screen and (max-width: 520px) { 347 | .tile.tile-256 .tile-inner { 348 | font-size: 25px; } } 349 | .tile.tile-512 .tile-inner { 350 | color: #f9f6f2; 351 | background: #edc850; 352 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.39683), inset 0 0 0 1px rgba(255, 255, 255, 0.2381); 353 | font-size: 45px; } 354 | @media screen and (max-width: 520px) { 355 | .tile.tile-512 .tile-inner { 356 | font-size: 25px; } } 357 | .tile.tile-1024 .tile-inner { 358 | color: #f9f6f2; 359 | background: #edc53f; 360 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.47619), inset 0 0 0 1px rgba(255, 255, 255, 0.28571); 361 | font-size: 35px; } 362 | @media screen and (max-width: 520px) { 363 | .tile.tile-1024 .tile-inner { 364 | font-size: 15px; } } 365 | .tile.tile-2048 .tile-inner { 366 | color: #f9f6f2; 367 | background: #edc22e; 368 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333); 369 | font-size: 35px; } 370 | @media screen and (max-width: 520px) { 371 | .tile.tile-2048 .tile-inner { 372 | font-size: 15px; } } 373 | .tile.tile-super .tile-inner { 374 | color: #f9f6f2; 375 | background: #3c3a32; 376 | font-size: 30px; } 377 | @media screen and (max-width: 520px) { 378 | .tile.tile-super .tile-inner { 379 | font-size: 10px; } } 380 | 381 | @-webkit-keyframes appear { 382 | 0% { 383 | opacity: 0; 384 | -webkit-transform: scale(0); 385 | -moz-transform: scale(0); 386 | transform: scale(0); } 387 | 388 | 100% { 389 | opacity: 1; 390 | -webkit-transform: scale(1); 391 | -moz-transform: scale(1); 392 | transform: scale(1); } } 393 | @-moz-keyframes appear { 394 | 0% { 395 | opacity: 0; 396 | -webkit-transform: scale(0); 397 | -moz-transform: scale(0); 398 | transform: scale(0); } 399 | 400 | 100% { 401 | opacity: 1; 402 | -webkit-transform: scale(1); 403 | -moz-transform: scale(1); 404 | transform: scale(1); } } 405 | @keyframes appear { 406 | 0% { 407 | opacity: 0; 408 | -webkit-transform: scale(0); 409 | -moz-transform: scale(0); 410 | transform: scale(0); } 411 | 412 | 100% { 413 | opacity: 1; 414 | -webkit-transform: scale(1); 415 | -moz-transform: scale(1); 416 | transform: scale(1); } } 417 | .tile-new .tile-inner { 418 | -webkit-animation: appear 200ms ease 100ms; 419 | -moz-animation: appear 200ms ease 100ms; 420 | animation: appear 200ms ease 100ms; 421 | -webkit-animation-fill-mode: backwards; 422 | -moz-animation-fill-mode: backwards; 423 | animation-fill-mode: backwards; } 424 | 425 | @-webkit-keyframes pop { 426 | 0% { 427 | -webkit-transform: scale(0); 428 | -moz-transform: scale(0); 429 | transform: scale(0); } 430 | 431 | 50% { 432 | -webkit-transform: scale(1.2); 433 | -moz-transform: scale(1.2); 434 | transform: scale(1.2); } 435 | 436 | 100% { 437 | -webkit-transform: scale(1); 438 | -moz-transform: scale(1); 439 | transform: scale(1); } } 440 | @-moz-keyframes pop { 441 | 0% { 442 | -webkit-transform: scale(0); 443 | -moz-transform: scale(0); 444 | transform: scale(0); } 445 | 446 | 50% { 447 | -webkit-transform: scale(1.2); 448 | -moz-transform: scale(1.2); 449 | transform: scale(1.2); } 450 | 451 | 100% { 452 | -webkit-transform: scale(1); 453 | -moz-transform: scale(1); 454 | transform: scale(1); } } 455 | @keyframes pop { 456 | 0% { 457 | -webkit-transform: scale(0); 458 | -moz-transform: scale(0); 459 | transform: scale(0); } 460 | 461 | 50% { 462 | -webkit-transform: scale(1.2); 463 | -moz-transform: scale(1.2); 464 | transform: scale(1.2); } 465 | 466 | 100% { 467 | -webkit-transform: scale(1); 468 | -moz-transform: scale(1); 469 | transform: scale(1); } } 470 | .tile-merged .tile-inner { 471 | z-index: 20; 472 | -webkit-animation: pop 200ms ease 100ms; 473 | -moz-animation: pop 200ms ease 100ms; 474 | animation: pop 200ms ease 100ms; 475 | -webkit-animation-fill-mode: backwards; 476 | -moz-animation-fill-mode: backwards; 477 | animation-fill-mode: backwards; } 478 | 479 | .above-game:after { 480 | content: ""; 481 | display: block; 482 | clear: both; } 483 | 484 | .game-intro { 485 | float: left; 486 | line-height: 42px; 487 | margin-bottom: 0; } 488 | 489 | .restart-button { 490 | display: inline-block; 491 | background: #8f7a66; 492 | border-radius: 3px; 493 | padding: 0 20px; 494 | text-decoration: none; 495 | color: #f9f6f2; 496 | height: 40px; 497 | line-height: 42px; 498 | display: block; 499 | text-align: center; 500 | float: right; } 501 | 502 | .game-explanation { 503 | margin-top: 50px; } 504 | 505 | @media screen and (max-width: 520px) { 506 | html, body { 507 | font-size: 15px; } 508 | 509 | body { 510 | margin: 20px 0; 511 | padding: 0 20px; } 512 | 513 | h1.title { 514 | font-size: 27px; 515 | margin-top: 15px; } 516 | 517 | .container { 518 | width: 280px; 519 | margin: 0 auto; } 520 | 521 | .score-container, .best-container { 522 | margin-top: 0; 523 | padding: 15px 10px; 524 | min-width: 40px; } 525 | 526 | .heading { 527 | margin-bottom: 10px; } 528 | 529 | .game-intro { 530 | width: 55%; 531 | display: block; 532 | box-sizing: border-box; 533 | line-height: 1.65; } 534 | 535 | .restart-button { 536 | width: 42%; 537 | padding: 0; 538 | display: block; 539 | box-sizing: border-box; 540 | margin-top: 2px; } 541 | 542 | .game-container { 543 | margin-top: 17px; 544 | position: relative; 545 | padding: 10px; 546 | cursor: default; 547 | -webkit-touch-callout: none; 548 | -ms-touch-callout: none; 549 | -webkit-user-select: none; 550 | -moz-user-select: none; 551 | -ms-user-select: none; 552 | -ms-touch-action: none; 553 | touch-action: none; 554 | background: #bbada0; 555 | border-radius: 6px; 556 | width: 280px; 557 | height: 280px; 558 | -webkit-box-sizing: border-box; 559 | -moz-box-sizing: border-box; 560 | box-sizing: border-box; } 561 | .game-container .game-message { 562 | display: none; 563 | position: absolute; 564 | top: 0; 565 | right: 0; 566 | bottom: 0; 567 | left: 0; 568 | background: rgba(238, 228, 218, 0.5); 569 | z-index: 100; 570 | text-align: center; 571 | -webkit-animation: fade-in 800ms ease 1200ms; 572 | -moz-animation: fade-in 800ms ease 1200ms; 573 | animation: fade-in 800ms ease 1200ms; 574 | -webkit-animation-fill-mode: both; 575 | -moz-animation-fill-mode: both; 576 | animation-fill-mode: both; } 577 | .game-container .game-message p { 578 | font-size: 60px; 579 | font-weight: bold; 580 | height: 60px; 581 | line-height: 60px; 582 | margin-top: 222px; } 583 | .game-container .game-message .lower { 584 | display: block; 585 | margin-top: 59px; } 586 | .game-container .game-message a { 587 | display: inline-block; 588 | background: #8f7a66; 589 | border-radius: 3px; 590 | padding: 0 20px; 591 | text-decoration: none; 592 | color: #f9f6f2; 593 | height: 40px; 594 | line-height: 42px; 595 | margin-left: 9px; } 596 | .game-container .game-message a.keep-playing-button { 597 | display: none; } 598 | .game-container .game-message.game-won { 599 | background: rgba(237, 194, 46, 0.5); 600 | color: #f9f6f2; } 601 | .game-container .game-message.game-won a.keep-playing-button { 602 | display: inline-block; } 603 | .game-container .game-message.game-won, .game-container .game-message.game-over { 604 | display: block; } 605 | 606 | .grid-container { 607 | position: absolute; 608 | z-index: 1; } 609 | 610 | .grid-row { 611 | margin-bottom: 10px; } 612 | .grid-row:last-child { 613 | margin-bottom: 0; } 614 | .grid-row:after { 615 | content: ""; 616 | display: block; 617 | clear: both; } 618 | 619 | .grid-cell { 620 | width: 57.5px; 621 | height: 57.5px; 622 | margin-right: 10px; 623 | float: left; 624 | border-radius: 3px; 625 | background: rgba(238, 228, 218, 0.35); } 626 | .grid-cell:last-child { 627 | margin-right: 0; } 628 | 629 | .tile-container { 630 | position: absolute; 631 | z-index: 2; } 632 | 633 | .tile, .tile .tile-inner { 634 | width: 58px; 635 | height: 58px; 636 | line-height: 67.5px; } 637 | .tile.tile-position-1-1 { 638 | -webkit-transform: translate(0px, 0px); 639 | -moz-transform: translate(0px, 0px); 640 | transform: translate(0px, 0px); } 641 | .tile.tile-position-1-2 { 642 | -webkit-transform: translate(0px, 67px); 643 | -moz-transform: translate(0px, 67px); 644 | transform: translate(0px, 67px); } 645 | .tile.tile-position-1-3 { 646 | -webkit-transform: translate(0px, 135px); 647 | -moz-transform: translate(0px, 135px); 648 | transform: translate(0px, 135px); } 649 | .tile.tile-position-1-4 { 650 | -webkit-transform: translate(0px, 202px); 651 | -moz-transform: translate(0px, 202px); 652 | transform: translate(0px, 202px); } 653 | .tile.tile-position-2-1 { 654 | -webkit-transform: translate(67px, 0px); 655 | -moz-transform: translate(67px, 0px); 656 | transform: translate(67px, 0px); } 657 | .tile.tile-position-2-2 { 658 | -webkit-transform: translate(67px, 67px); 659 | -moz-transform: translate(67px, 67px); 660 | transform: translate(67px, 67px); } 661 | .tile.tile-position-2-3 { 662 | -webkit-transform: translate(67px, 135px); 663 | -moz-transform: translate(67px, 135px); 664 | transform: translate(67px, 135px); } 665 | .tile.tile-position-2-4 { 666 | -webkit-transform: translate(67px, 202px); 667 | -moz-transform: translate(67px, 202px); 668 | transform: translate(67px, 202px); } 669 | .tile.tile-position-3-1 { 670 | -webkit-transform: translate(135px, 0px); 671 | -moz-transform: translate(135px, 0px); 672 | transform: translate(135px, 0px); } 673 | .tile.tile-position-3-2 { 674 | -webkit-transform: translate(135px, 67px); 675 | -moz-transform: translate(135px, 67px); 676 | transform: translate(135px, 67px); } 677 | .tile.tile-position-3-3 { 678 | -webkit-transform: translate(135px, 135px); 679 | -moz-transform: translate(135px, 135px); 680 | transform: translate(135px, 135px); } 681 | .tile.tile-position-3-4 { 682 | -webkit-transform: translate(135px, 202px); 683 | -moz-transform: translate(135px, 202px); 684 | transform: translate(135px, 202px); } 685 | .tile.tile-position-4-1 { 686 | -webkit-transform: translate(202px, 0px); 687 | -moz-transform: translate(202px, 0px); 688 | transform: translate(202px, 0px); } 689 | .tile.tile-position-4-2 { 690 | -webkit-transform: translate(202px, 67px); 691 | -moz-transform: translate(202px, 67px); 692 | transform: translate(202px, 67px); } 693 | .tile.tile-position-4-3 { 694 | -webkit-transform: translate(202px, 135px); 695 | -moz-transform: translate(202px, 135px); 696 | transform: translate(202px, 135px); } 697 | .tile.tile-position-4-4 { 698 | -webkit-transform: translate(202px, 202px); 699 | -moz-transform: translate(202px, 202px); 700 | transform: translate(202px, 202px); } 701 | 702 | .tile .tile-inner { 703 | font-size: 35px; } 704 | 705 | .game-message p { 706 | font-size: 30px !important; 707 | height: 30px !important; 708 | line-height: 30px !important; 709 | margin-top: 90px !important; } 710 | .game-message .lower { 711 | margin-top: 30px !important; } } 712 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------