21 |
28 |
29 |
30 |
Join the numbers and get to the 2048 tile!
31 |
New Game
32 |
33 |
34 |
35 |
42 |
43 |
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 |
--------------------------------------------------------------------------------
/html/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 |
--------------------------------------------------------------------------------
/html/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 |
--------------------------------------------------------------------------------
/html/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 |
--------------------------------------------------------------------------------
/html/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 |
--------------------------------------------------------------------------------
/html/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: 107px; }
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: 58px; }
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 |
--------------------------------------------------------------------------------
/html/style/fonts/ClearSans-Bold-webfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |