diff --git a/README.md b/README.md index 80c0169..136fbbe 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Clone of 2048 game. You need to reach 243. #### Supported architectures -* x86-64b - [![Build Status](https://ci-apps.yunohost.org/ci/logs/243%20%28Apps%29.svg)](https://ci-apps.yunohost.org/ci/apps/243/) +* x86-64 - [![Build Status](https://ci-apps.yunohost.org/ci/logs/243%20%28Apps%29.svg)](https://ci-apps.yunohost.org/ci/apps/243/) * ARMv8-A - [![Build Status](https://ci-apps-arm.yunohost.org/ci/logs/243%20%28Apps%29.svg)](https://ci-apps-arm.yunohost.org/ci/apps/243/) ## Links @@ -33,8 +33,7 @@ Clone of 2048 game. You need to reach 243. --- -Developers info ----------------- +## Developers info Please do your pull request to the [testing branch](https://github.com/YunoHost-Apps/243_ynh/tree/testing). diff --git a/README_fr.md b/README_fr.md index 4b17c99..a1b39df 100644 --- a/README_fr.md +++ b/README_fr.md @@ -5,7 +5,7 @@ *[Read this readme in english.](./README.md)* -> *Ce package vous permet d’installer 243 rapidement et simplement sur un serveur Yunohost. +> *Ce package vous permet d’installer 243 rapidement et simplement sur un serveur YunoHost. Si vous n’avez pas YunoHost, regardez [ici](https://yunohost.org/#/install) pour savoir comment l’installer et en profiter.* ## Vue d’ensemble @@ -22,7 +22,7 @@ Clone du jeu 2048. Il vous faut atteindre 243. #### Architectures supportées -* x86-64b - [![Build Status](https://ci-apps.yunohost.org/ci/logs/243%20%28Apps%29.svg)](https://ci-apps.yunohost.org/ci/apps/243/) +* x86-64 - [![Build Status](https://ci-apps.yunohost.org/ci/logs/243%20%28Apps%29.svg)](https://ci-apps.yunohost.org/ci/apps/243/) * ARMv8-A - [![Build Status](https://ci-apps-arm.yunohost.org/ci/logs/243%20%28Apps%29.svg)](https://ci-apps-arm.yunohost.org/ci/apps/243/) ## Liens @@ -33,8 +33,7 @@ Clone du jeu 2048. Il vous faut atteindre 243. --- -Informations pour les développeurs ----------------- +## Informations pour les développeurs Merci de faire vos pull request sur la [branche testing](https://github.com/YunoHost-Apps/243_ynh/tree/testing). diff --git a/manifest.json b/manifest.json index 4c8e762..8563baf 100644 --- a/manifest.json +++ b/manifest.json @@ -6,7 +6,7 @@ "en": "2048 game clone", "fr": "Clone du jeu 2048" }, - "version": "1.0~ynh1", + "version": "1.0~ynh2", "url": "https://github.com/hgentry/81/", "license": "MIT", "maintainer": { diff --git a/scripts/backup b/scripts/backup index c3cec4b..99f4abc 100644 --- a/scripts/backup +++ b/scripts/backup @@ -6,7 +6,6 @@ # IMPORT GENERIC HELPERS #================================================= -#Keep this path for calling _common.sh inside the execution's context of backup and restore scripts source /usr/share/yunohost/helpers #================================================= diff --git a/scripts/change_url b/scripts/change_url index 7fcf700..d02c11b 100644 --- a/scripts/change_url +++ b/scripts/change_url @@ -67,23 +67,23 @@ fi #================================================= # MODIFY URL IN NGINX CONF #================================================= -ynh_script_progression --message="Updating Nginx web server configuration..." --weight=3 +ynh_script_progression --message="Updating NGINX web server configuration..." --weight=3 nginx_conf_path=/etc/nginx/conf.d/$old_domain.d/$app.conf -# Change the path in the nginx config file +# Change the path in the NGINX config file if [ $change_path -eq 1 ] then - # Make a backup of the original nginx config file if modified + # Make a backup of the original NGINX config file if modified ynh_backup_if_checksum_is_different --file="$nginx_conf_path" - # Set global variables for nginx helper + # Set global variables for NGINX helper domain="$old_domain" path_url="$new_path" - # Create a dedicated nginx config + # Create a dedicated NGINX config ynh_add_nginx_config fi -# Change the domain for nginx +# Change the domain for NGINX if [ $change_domain -eq 1 ] then # Delete file checksum for the old conf file location @@ -96,7 +96,7 @@ fi #================================================= # RELOAD NGINX #================================================= -ynh_script_progression --message="Reloading Nginx web server..." --weight=2 +ynh_script_progression --message="Reloading NGINX web server..." --weight=2 ynh_systemd_action --service_name=nginx --action=reload diff --git a/scripts/install b/scripts/install index c560a3e..ffdcac6 100755 --- a/scripts/install +++ b/scripts/install @@ -62,7 +62,7 @@ ynh_setup_source --dest_dir="$final_path" #================================================= # NGINX CONFIGURATION #================================================= -ynh_script_progression --message="Configuring Nginx web server..." --weight=2 +ynh_script_progression --message="Configuring NGINX web server..." --weight=2 # Create a dedicated nginx config ynh_add_nginx_config @@ -87,7 +87,7 @@ ynh_script_progression --message="Configuring SSOwat..." --weight=1 #================================================= # RELOAD NGINX #================================================= -ynh_script_progression --message="Reloading Nginx web server..." --weight=1 +ynh_script_progression --message="Reloading NGINX web server..." --weight=1 ynh_systemd_action --service_name=nginx --action=reload diff --git a/scripts/remove b/scripts/remove index 2c06727..a660b98 100755 --- a/scripts/remove +++ b/scripts/remove @@ -31,9 +31,9 @@ ynh_secure_remove --file="$final_path" #================================================= # REMOVE NGINX CONFIGURATION #================================================= -ynh_script_progression --message="Removing Nginx web server configuration..." --weight=1 +ynh_script_progression --message="Removing NGINX web server configuration..." --weight=1 -# Remove the dedicated nginx config +# Remove the dedicated NGINX config ynh_remove_nginx_config #================================================= diff --git a/scripts/restore b/scripts/restore index 61703fb..a958e3e 100644 --- a/scripts/restore +++ b/scripts/restore @@ -71,7 +71,7 @@ chown -R root: $final_path #================================================= # RELOAD NGINX #================================================= -ynh_script_progression --message="Reloading nginx web server..." --weight=3 +ynh_script_progression --message="Reloading NGINX web server..." --weight=3 ynh_systemd_action --service_name=nginx --action=reload diff --git a/scripts/upgrade b/scripts/upgrade index a252a5a..6ecfa8a 100644 --- a/scripts/upgrade +++ b/scripts/upgrade @@ -81,9 +81,9 @@ fi #================================================= # NGINX CONFIGURATION #================================================= -ynh_script_progression --message="Upgrading nginx web server configuration..." --weight=2 +ynh_script_progression --message="Upgrading NGINX web server configuration..." --weight=2 -# Create a dedicated nginx config +# Create a dedicated NGINX config ynh_add_nginx_config #================================================= @@ -104,7 +104,7 @@ ynh_script_progression --message="Upgrading SSOwat configuration..." --weight=1 #================================================= # RELOAD NGINX #================================================= -ynh_script_progression --message="Reloading nginx web server..." --weight=1 +ynh_script_progression --message="Reloading NGINX web server..." --weight=1 ynh_systemd_action --service_name=nginx --action=reload diff --git a/sources/CONTRIBUTING.md b/sources/CONTRIBUTING.md new file mode 100644 index 0000000..968baa9 --- /dev/null +++ b/sources/CONTRIBUTING.md @@ -0,0 +1,2 @@ +## Contributing +Changes and improvements are more than welcome! Feel free to fork and open a pull request. Please make your changes in a specifically made branch and request to pull on `master`! If you can, please make sure the game fully works before sending the PR, as that will help speed up the process. diff --git a/sources/LICENSE.txt b/sources/LICENSE.txt new file mode 100644 index 0000000..b0dbfa4 --- /dev/null +++ b/sources/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Gabriele Cirulli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/sources/README.md b/sources/README.md new file mode 100644 index 0000000..96a85a7 --- /dev/null +++ b/sources/README.md @@ -0,0 +1,29 @@ +# 2048 +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). + +Made just for fun. [Play it here!](http://gabrielecirulli.github.io/2048/) + +### Contributions + + - [TimPetricola](https://github.com/TimPetricola) added best score storage + - [chrisprice](https://github.com/chrisprice) added custom code for swipe handling on mobile + - [elektryk](https://github.com/elektryk) made swipes work on Windows Phone + +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. + +### Screenshot + +[![Screenshot](http://pictures.gabrielecirulli.com/2048-20140309-234100.png)](http://pictures.gabrielecirulli.com/2048-20140309-234100.png) + +That screenshot is fake, by the way. I never reached 2048 :smile: + +## Contributing +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. + +You can find the same information in the [contributing guide.](https://github.com/gabrielecirulli/2048/blob/master/CONTRIBUTING.md) + +## License +2048 is licensed under the [MIT license.](https://github.com/gabrielecirulli/2048/blob/master/LICENSE.txt) + +## Donations +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! diff --git a/sources/Rakefile b/sources/Rakefile new file mode 100644 index 0000000..673b273 --- /dev/null +++ b/sources/Rakefile @@ -0,0 +1,11 @@ +require "date" + +namespace :appcache do + desc "update the date in the appcache file (in the gh-pages branch)" + task :update do + appcache = File.read("cache.appcache") + updated = "# Updated: #{DateTime.now}\n" + + File.write("cache.appcache", appcache.sub(/\A(#.*$\n)?/, updated)) + end +end diff --git a/sources/favicon.ico b/sources/favicon.ico new file mode 100644 index 0000000..22109e0 Binary files /dev/null and b/sources/favicon.ico differ diff --git a/sources/index.html b/sources/index.html new file mode 100644 index 0000000..fd18813 --- /dev/null +++ b/sources/index.html @@ -0,0 +1,97 @@ + + + + + 243 + + + + + + + + + + + +
+
+

243

+
+
0
+
0
+
+
+

Join the numbers and get to the 243 tile!

+ +
+
+

+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+ +

+ How to play: Use your arrow keys to move the tiles. When three tiles with the same number touch, they merge into one! +

+
+

+ Note: This site is NOT the official version of 2048.

+
+

+ The original 2048 was created by Gabriele Cirulli. Based on 1024 by Veewo Studio and conceptually similar to Threes by Asher Vollmer. +

+
+ + + + + + + + + + + diff --git a/sources/js/animframe_polyfill.js b/sources/js/animframe_polyfill.js new file mode 100644 index 0000000..c45a13e --- /dev/null +++ b/sources/js/animframe_polyfill.js @@ -0,0 +1,26 @@ +(function() { + var lastTime = 0; + var vendors = ['webkit', 'moz']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = + window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) { + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } + + if (!window.cancelAnimationFrame) { + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + } +}()); diff --git a/sources/js/application.js b/sources/js/application.js new file mode 100644 index 0000000..b5ee5f1 --- /dev/null +++ b/sources/js/application.js @@ -0,0 +1,4 @@ +// Wait till the browser is ready to render the game (avoids glitches) +window.requestAnimationFrame(function () { + new GameManager(5, KeyboardInputManager, HTMLActuator, LocalScoreManager); +}); diff --git a/sources/js/game_manager.js b/sources/js/game_manager.js new file mode 100644 index 0000000..98c6f36 --- /dev/null +++ b/sources/js/game_manager.js @@ -0,0 +1,267 @@ +function GameManager(size, InputManager, Actuator, ScoreManager) { + this.size = size; // Size of the grid + this.inputManager = new InputManager; + this.scoreManager = new ScoreManager; + this.actuator = new Actuator; + + this.startTiles = 2; + + this.inputManager.on("move", this.move.bind(this)); + this.inputManager.on("restart", this.restart.bind(this)); + this.inputManager.on("keepPlaying", this.keepPlaying.bind(this)); + + this.setup(); +} + +// Restart the game +GameManager.prototype.restart = function () { + this.actuator.continue(); + this.setup(); +}; + +// Keep playing after winning +GameManager.prototype.keepPlaying = function () { + this.keepPlaying = true; + this.actuator.continue(); +}; + +GameManager.prototype.isGameTerminated = function () { + if (this.over || (this.won && !this.keepPlaying)) { + return true; + } else { + return false; + } +}; + +// Set up the game +GameManager.prototype.setup = function () { + this.grid = new Grid(this.size); + + this.score = 0; + this.over = false; + this.won = false; + this.keepPlaying = false; + + // Add the initial tiles + this.addStartTiles(); + + // Update the actuator + this.actuate(); +}; + +// Set up the initial tiles to start the game with +GameManager.prototype.addStartTiles = function () { + for (var i = 0; i < this.startTiles; i++) { + this.addRandomTile(); + } +}; + +// Adds a tile in a random position +GameManager.prototype.addRandomTile = function () { + if (this.grid.cellsAvailable()) { + var value = Math.random() < 0.9 ? 1 : 3; + var tile = new Tile(this.grid.randomAvailableCell(), value); + + this.grid.insertTile(tile); + } +}; + +// Sends the updated grid to the actuator +GameManager.prototype.actuate = function () { + if (this.scoreManager.get() < this.score) { + this.scoreManager.set(this.score); + } + + this.actuator.actuate(this.grid, { + score: this.score, + over: this.over, + won: this.won, + bestScore: this.scoreManager.get(), + terminated: this.isGameTerminated() + }); + +}; + +// Save all tile positions and remove merger info +GameManager.prototype.prepareTiles = function () { + this.grid.eachCell(function (x, y, tile) { + if (tile) { + tile.mergedFrom = null; + tile.savePosition(); + } + }); +}; + +// Move a tile and its representation +GameManager.prototype.moveTile = function (tile, cell) { + this.grid.cells[tile.x][tile.y] = null; + this.grid.cells[cell.x][cell.y] = tile; + tile.updatePosition(cell); +}; + +// Move tiles on the grid in the specified direction +GameManager.prototype.move = function (direction) { + // 0: up, 1: right, 2:down, 3: left + var self = this; + + if (this.isGameTerminated()) return; // Don't do anything if the game's over + + var cell, tile; + + var vector = this.getVector(direction); + var traversals = this.buildTraversals(vector); + var moved = false; + + // Save the current tile positions and remove merger information + this.prepareTiles(); + + // Traverse the grid in the right direction and move tiles + traversals.x.forEach(function (x) { + traversals.y.forEach(function (y) { + cell = { x: x, y: y }; + tile = self.grid.cellContent(cell); + + if(tile && tile.merged) { + self.grid.removeTile(tile); + } + else if (tile) { + var positions = self.findFarthestPosition(cell, vector); + var next = self.grid.cellContent(positions.next); + var pos2 = null; + var next2 = null; + if(next) + { + pos2 = self.findFarthestPosition({x: next.x, y: next.y}, vector); + next2 = self.grid.cellContent(pos2.next); + } + // Only one merger per row traversal? + if (next && next.value === tile.value && next2 && next2.value === tile.value && !next.mergedFrom && next != next2) { + var merged = new Tile(pos2.next, tile.value * 3); + merged.mergedFrom = [next2, next, tile]; + tile.merged = true; + next.merged = true; + next2.merged = true; + + self.grid.removeTile(tile); + self.grid.removeTile(next); + self.grid.removeTile(next2); + + self.grid.insertTile(merged); + // Converge the two tiles' positions + tile.updatePosition(pos2.next); + next.updatePosition(pos2.next); + // Update the score + self.score += merged.value; + + // The mighty 2048 tile + if (merged.value === 243) self.won = true; + } + + else if (!tile.merged){ + self.moveTile(tile, positions.farthest); + } + + if (tile && !self.positionsEqual(cell, tile)) { + moved = true; // The tile moved from its original cell! + } + } + + }); + }); + + if (moved) { + this.addRandomTile(); + + if (!this.movesAvailable()) { + this.over = true; // Game over! + } + + this.actuate(); + } +}; + +// Get the vector representing the chosen direction +GameManager.prototype.getVector = function (direction) { + // Vectors representing tile movement + var map = { + 0: { x: 0, y: -1 }, // up + 1: { x: 1, y: 0 }, // right + 2: { x: 0, y: 1 }, // down + 3: { x: -1, y: 0 } // left + }; + + return map[direction]; +}; + +// Build a list of positions to traverse in the right order +GameManager.prototype.buildTraversals = function (vector) { + var traversals = { x: [], y: [] }; + + for (var pos = 0; pos < this.size; pos++) { + traversals.x.push(pos); + traversals.y.push(pos); + } + + // Always traverse from the farthest cell in the chosen direction + if (vector.x === 1) traversals.x = traversals.x.reverse(); + if (vector.y === 1) traversals.y = traversals.y.reverse(); + + return traversals; +}; + +GameManager.prototype.findFarthestPosition = function (cell, vector) { + var previous; + + // Progress towards the vector direction until an obstacle is found + do { + previous = cell; + cell = { x: previous.x + vector.x, y: previous.y + vector.y }; + } while (this.grid.withinBounds(cell) && + this.grid.cellAvailable(cell)); + + return { + farthest: previous, + next: cell // Used to check if a merge is required + }; +}; + +GameManager.prototype.movesAvailable = function () { + return this.grid.cellsAvailable() || this.tileMatchesAvailable(); +}; + +// Check for available matches between tiles (more expensive check) +GameManager.prototype.tileMatchesAvailable = function () { + var self = this; + + var tile; + var i = 0; + while(i < 4){ + for (var x = 0; x < this.size; x++) { + for (var y = 0; y < this.size; y++) { + var vector = this.getVector(i); + tile = this.grid.cellContent({ x: x, y: y }); + + cell = { x: x, y: y }; + tile = self.grid.cellContent(cell); + + if (tile) { + var positions = self.findFarthestPosition(cell, vector); + var next = self.grid.cellContent(positions.next); + var pos2 = self.findFarthestPosition({x: cell.x + vector.x, y: cell.y + vector.y}, vector); + var next2 = self.grid.cellContent(pos2.next); + // Only one merger per row traversal? + if (next && next.value === tile.value && next2 && next2.value === tile.value && !next.mergedFrom && next != next2) { + return true; + } + } + } + } + i += 1; + } + + return false; +}; + +GameManager.prototype.positionsEqual = function (first, second) { + return first.x === second.x && first.y === second.y; +}; diff --git a/sources/js/grid.js b/sources/js/grid.js new file mode 100644 index 0000000..73b86a2 --- /dev/null +++ b/sources/js/grid.js @@ -0,0 +1,84 @@ +function Grid(size) { + this.size = size; + + this.cells = []; + + this.build(); +} + +// Build a grid of the specified size +Grid.prototype.build = function () { + for (var x = 0; x < this.size; x++) { + var row = this.cells[x] = []; + + for (var y = 0; y < this.size; y++) { + row.push(null); + } + } +}; + +// Find the first available random position +Grid.prototype.randomAvailableCell = function () { + var cells = this.availableCells(); + + if (cells.length) { + return cells[Math.floor(Math.random() * cells.length)]; + } +}; + +Grid.prototype.availableCells = function () { + var cells = []; + + this.eachCell(function (x, y, tile) { + if (!tile) { + cells.push({ x: x, y: y }); + } + }); + + return cells; +}; + +// Call callback for every cell +Grid.prototype.eachCell = function (callback) { + for (var x = 0; x < this.size; x++) { + for (var y = 0; y < this.size; y++) { + callback(x, y, this.cells[x][y]); + } + } +}; + +// Check if there are any cells available +Grid.prototype.cellsAvailable = function () { + return !!this.availableCells().length; +}; + +// Check if the specified cell is taken +Grid.prototype.cellAvailable = function (cell) { + return !this.cellOccupied(cell); +}; + +Grid.prototype.cellOccupied = function (cell) { + return !!this.cellContent(cell); +}; + +Grid.prototype.cellContent = function (cell) { + if (this.withinBounds(cell)) { + return this.cells[cell.x][cell.y]; + } else { + return null; + } +}; + +// Inserts a tile at its position +Grid.prototype.insertTile = function (tile) { + this.cells[tile.x][tile.y] = tile; +}; + +Grid.prototype.removeTile = function (tile) { + this.cells[tile.x][tile.y] = null; +}; + +Grid.prototype.withinBounds = function (position) { + return position && position.x >= 0 && position.x < this.size && + position.y >= 0 && position.y < this.size; +}; diff --git a/sources/js/html_actuator.js b/sources/js/html_actuator.js new file mode 100644 index 0000000..d840068 --- /dev/null +++ b/sources/js/html_actuator.js @@ -0,0 +1,139 @@ +function HTMLActuator() { + this.tileContainer = document.querySelector(".tile-container"); + this.scoreContainer = document.querySelector(".score-container"); + this.bestContainer = document.querySelector(".best-container"); + this.messageContainer = document.querySelector(".game-message"); + + this.score = 0; +} + +HTMLActuator.prototype.actuate = function (grid, metadata) { + var self = this; + + window.requestAnimationFrame(function () { + self.clearContainer(self.tileContainer); + + grid.cells.forEach(function (column) { + column.forEach(function (cell) { + if (cell) { + self.addTile(cell); + } + }); + }); + + self.updateScore(metadata.score); + self.updateBestScore(metadata.bestScore); + + if (metadata.terminated) { + if (metadata.over) { + self.message(false); // You lose + } else if (metadata.won) { + self.message(true); // You win! + } + } + + }); +}; + +// Continues the game (both restart and keep playing) +HTMLActuator.prototype.continue = function () { + this.clearMessage(); +}; + +HTMLActuator.prototype.clearContainer = function (container) { + while (container.firstChild) { + container.removeChild(container.firstChild); + } +}; + +HTMLActuator.prototype.addTile = function (tile) { + var self = this; + + var wrapper = document.createElement("div"); + var inner = document.createElement("div"); + var position = tile.previousPosition || { x: tile.x, y: tile.y }; + var positionClass = this.positionClass(position); + + // We can't use classlist because it somehow glitches when replacing classes + var classes = ["tile", "tile-" + tile.value, positionClass]; + + if (tile.value > 2048) classes.push("tile-super"); + + this.applyClasses(wrapper, classes); + + inner.classList.add("tile-inner"); + inner.textContent = tile.value; + + if (tile.previousPosition) { + // Make sure that the tile gets rendered in the previous position first + window.requestAnimationFrame(function () { + classes[2] = self.positionClass({ x: tile.x, y: tile.y }); + self.applyClasses(wrapper, classes); // Update the position + }); + } else if (tile.mergedFrom) { + classes.push("tile-merged"); + this.applyClasses(wrapper, classes); + + // Render the tiles that merged + tile.mergedFrom.forEach(function (merged) { + self.addTile(merged); + }); + } else { + classes.push("tile-new"); + this.applyClasses(wrapper, classes); + } + + // Add the inner part of the tile to the wrapper + wrapper.appendChild(inner); + + // Put the tile on the board + this.tileContainer.appendChild(wrapper); +}; + +HTMLActuator.prototype.applyClasses = function (element, classes) { + element.setAttribute("class", classes.join(" ")); +}; + +HTMLActuator.prototype.normalizePosition = function (position) { + return { x: position.x + 1, y: position.y + 1 }; +}; + +HTMLActuator.prototype.positionClass = function (position) { + position = this.normalizePosition(position); + return "tile-position-" + position.x + "-" + position.y; +}; + +HTMLActuator.prototype.updateScore = function (score) { + this.clearContainer(this.scoreContainer); + + var difference = score - this.score; + this.score = score; + + this.scoreContainer.textContent = this.score; + + if (difference > 0) { + var addition = document.createElement("div"); + addition.classList.add("score-addition"); + addition.textContent = "+" + difference; + + this.scoreContainer.appendChild(addition); + } +}; + +HTMLActuator.prototype.updateBestScore = function (bestScore) { + this.bestContainer.textContent = bestScore; +}; + +HTMLActuator.prototype.message = function (won) { + var type = won ? "game-won" : "game-over"; + var message = won ? "You win!" : "Game over!"; + + this.messageContainer.classList.add(type); + this.messageContainer.getElementsByTagName("p")[0].textContent = message; +}; + +HTMLActuator.prototype.clearMessage = function () { + // IE only takes one value to remove at a time. + this.messageContainer.classList.remove("game-won"); + this.messageContainer.classList.remove("game-over"); +}; diff --git a/sources/js/keyboard_input_manager.js b/sources/js/keyboard_input_manager.js new file mode 100644 index 0000000..a29744c --- /dev/null +++ b/sources/js/keyboard_input_manager.js @@ -0,0 +1,130 @@ +function KeyboardInputManager() { + this.events = {}; + + if (window.navigator.msPointerEnabled) { + //Internet Explorer 10 style + this.eventTouchstart = "MSPointerDown"; + this.eventTouchmove = "MSPointerMove"; + this.eventTouchend = "MSPointerUp"; + } else { + this.eventTouchstart = "touchstart"; + this.eventTouchmove = "touchmove"; + this.eventTouchend = "touchend"; + } + + this.listen(); +} + +KeyboardInputManager.prototype.on = function (event, callback) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(callback); +}; + +KeyboardInputManager.prototype.emit = function (event, data) { + var callbacks = this.events[event]; + if (callbacks) { + callbacks.forEach(function (callback) { + callback(data); + }); + } +}; + +KeyboardInputManager.prototype.listen = function () { + var self = this; + + var map = { + 38: 0, // Up + 39: 1, // Right + 40: 2, // Down + 37: 3, // Left + 75: 0, // vim keybindings + 76: 1, + 74: 2, + 72: 3, + 87: 0, // W + 68: 1, // D + 83: 2, // S + 65: 3 // A + }; + + document.addEventListener("keydown", function (event) { + var modifiers = event.altKey || event.ctrlKey || event.metaKey || + event.shiftKey; + var mapped = map[event.which]; + + if (!modifiers) { + if (mapped !== undefined) { + event.preventDefault(); + self.emit("move", mapped); + } + + if (event.which === 32) self.restart.bind(self)(event); + } + }); + + var retry = document.querySelector(".retry-button"); + retry.addEventListener("click", this.restart.bind(this)); + retry.addEventListener(this.eventTouchend, this.restart.bind(this)); + + var keepPlaying = document.querySelector(".keep-playing-button"); + keepPlaying.addEventListener("click", this.keepPlaying.bind(this)); + keepPlaying.addEventListener("touchend", this.keepPlaying.bind(this)); + + // Listen to swipe events + var touchStartClientX, touchStartClientY; + var gameContainer = document.getElementsByClassName("game-container")[0]; + + gameContainer.addEventListener(this.eventTouchstart, function (event) { + if (( !window.navigator.msPointerEnabled && event.touches.length > 1) || event.targetTouches > 1) return; + + if(window.navigator.msPointerEnabled){ + touchStartClientX = event.pageX; + touchStartClientY = event.pageY; + } else { + touchStartClientX = event.touches[0].clientX; + touchStartClientY = event.touches[0].clientY; + } + + event.preventDefault(); + }); + + gameContainer.addEventListener(this.eventTouchmove, function (event) { + event.preventDefault(); + }); + + gameContainer.addEventListener(this.eventTouchend, function (event) { + if (( !window.navigator.msPointerEnabled && event.touches.length > 0) || event.targetTouches > 0) return; + + var touchEndClientX, touchEndClientY; + if(window.navigator.msPointerEnabled){ + touchEndClientX = event.pageX; + touchEndClientY = event.pageY; + } else { + touchEndClientX = event.changedTouches[0].clientX; + touchEndClientY = event.changedTouches[0].clientY; + } + + var dx = touchEndClientX - touchStartClientX; + var absDx = Math.abs(dx); + + var dy = touchEndClientY - touchStartClientY; + var absDy = Math.abs(dy); + + if (Math.max(absDx, absDy) > 10) { + // (right : left) : (down : up) + self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); + } + }); +}; + +KeyboardInputManager.prototype.restart = function (event) { + event.preventDefault(); + this.emit("restart"); +}; + +KeyboardInputManager.prototype.keepPlaying = function (event) { + event.preventDefault(); + this.emit("keepPlaying"); +}; diff --git a/sources/js/local_score_manager.js b/sources/js/local_score_manager.js new file mode 100644 index 0000000..ec4575d --- /dev/null +++ b/sources/js/local_score_manager.js @@ -0,0 +1,48 @@ +window.fakeStorage = { + _data: {}, + + setItem: function (id, val) { + return this._data[id] = String(val); + }, + + getItem: function (id) { + return this._data.hasOwnProperty(id) ? this._data[id] : undefined; + }, + + removeItem: function (id) { + return delete this._data[id]; + }, + + clear: function () { + return this._data = {}; + } +}; + +function LocalScoreManager() { + this.key = "bestScore"; + + var supported = this.localStorageSupported(); + this.storage = supported ? window.localStorage : window.fakeStorage; +} + +LocalScoreManager.prototype.localStorageSupported = function () { + var testKey = "test"; + var storage = window.localStorage; + + try { + storage.setItem(testKey, "1"); + storage.removeItem(testKey); + return true; + } catch (error) { + return false; + } +}; + +LocalScoreManager.prototype.get = function () { + return this.storage.getItem(this.key) || 0; +}; + +LocalScoreManager.prototype.set = function (score) { + this.storage.setItem(this.key, score); +}; + diff --git a/sources/js/tile.js b/sources/js/tile.js new file mode 100644 index 0000000..96f92d7 --- /dev/null +++ b/sources/js/tile.js @@ -0,0 +1,17 @@ +function Tile(position, value) { + this.x = position.x; + this.y = position.y; + this.value = value || 2; + this.merged = false; + this.previousPosition = null; + this.mergedFrom = null; // Tracks tiles that merged together +} + +Tile.prototype.savePosition = function () { + this.previousPosition = { x: this.x, y: this.y }; +}; + +Tile.prototype.updatePosition = function (position) { + this.x = position.x; + this.y = position.y; +}; diff --git a/sources/meta/apple-touch-icon.png b/sources/meta/apple-touch-icon.png new file mode 100644 index 0000000..3fd20f6 Binary files /dev/null and b/sources/meta/apple-touch-icon.png differ diff --git a/sources/style/fonts/ClearSans-Bold-webfont.eot b/sources/style/fonts/ClearSans-Bold-webfont.eot new file mode 100755 index 0000000..3678ef2 Binary files /dev/null and b/sources/style/fonts/ClearSans-Bold-webfont.eot differ diff --git a/sources/style/fonts/ClearSans-Bold-webfont.svg b/sources/style/fonts/ClearSans-Bold-webfont.svg new file mode 100755 index 0000000..aa985ae --- /dev/null +++ b/sources/style/fonts/ClearSans-Bold-webfont.svg @@ -0,0 +1,640 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sources/style/fonts/ClearSans-Bold-webfont.woff b/sources/style/fonts/ClearSans-Bold-webfont.woff new file mode 100755 index 0000000..184a945 Binary files /dev/null and b/sources/style/fonts/ClearSans-Bold-webfont.woff differ diff --git a/sources/style/fonts/ClearSans-Light-webfont.eot b/sources/style/fonts/ClearSans-Light-webfont.eot new file mode 100755 index 0000000..0dc609d Binary files /dev/null and b/sources/style/fonts/ClearSans-Light-webfont.eot differ diff --git a/sources/style/fonts/ClearSans-Light-webfont.svg b/sources/style/fonts/ClearSans-Light-webfont.svg new file mode 100755 index 0000000..1d5d2ec --- /dev/null +++ b/sources/style/fonts/ClearSans-Light-webfont.svg @@ -0,0 +1,670 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sources/style/fonts/ClearSans-Light-webfont.woff b/sources/style/fonts/ClearSans-Light-webfont.woff new file mode 100755 index 0000000..44555e0 Binary files /dev/null and b/sources/style/fonts/ClearSans-Light-webfont.woff differ diff --git a/sources/style/fonts/ClearSans-Regular-webfont.eot b/sources/style/fonts/ClearSans-Regular-webfont.eot new file mode 100755 index 0000000..b020e05 Binary files /dev/null and b/sources/style/fonts/ClearSans-Regular-webfont.eot differ diff --git a/sources/style/fonts/ClearSans-Regular-webfont.svg b/sources/style/fonts/ClearSans-Regular-webfont.svg new file mode 100755 index 0000000..1e2cffd --- /dev/null +++ b/sources/style/fonts/ClearSans-Regular-webfont.svg @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sources/style/fonts/ClearSans-Regular-webfont.woff b/sources/style/fonts/ClearSans-Regular-webfont.woff new file mode 100755 index 0000000..9d58858 Binary files /dev/null and b/sources/style/fonts/ClearSans-Regular-webfont.woff differ diff --git a/sources/style/fonts/clear-sans.css b/sources/style/fonts/clear-sans.css new file mode 100755 index 0000000..de2811d --- /dev/null +++ b/sources/style/fonts/clear-sans.css @@ -0,0 +1,30 @@ +@font-face { + font-family: "Clear Sans"; + src: url("ClearSans-Light-webfont.eot"); + src: url("ClearSans-Light-webfont.eot?#iefix") format("embedded-opentype"), + url("ClearSans-Light-webfont.svg#clear_sans_lightregular") format("svg"), + url("ClearSans-Light-webfont.woff") format("woff"); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: "Clear Sans"; + src: url("ClearSans-Regular-webfont.eot"); + src: url("ClearSans-Regular-webfont.eot?#iefix") format("embedded-opentype"), + url("ClearSans-Regular-webfont.svg#clear_sansregular") format("svg"), + url("ClearSans-Regular-webfont.woff") format("woff"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "Clear Sans"; + src: url("ClearSans-Bold-webfont.eot"); + src: url("ClearSans-Bold-webfont.eot?#iefix") format("embedded-opentype"), + url("ClearSans-Bold-webfont.svg#clear_sansbold") format("svg"), + url("ClearSans-Bold-webfont.woff") format("woff"); + font-weight: 700; + font-style: normal; +} + diff --git a/sources/style/helpers.scss b/sources/style/helpers.scss new file mode 100644 index 0000000..53b9dc1 --- /dev/null +++ b/sources/style/helpers.scss @@ -0,0 +1,72 @@ +// Exponent +// From: https://github.com/Team-Sass/Sassy-math/blob/master/sass/math.scss#L36 + +@function exponent($base, $exponent) { + // reset value + $value: $base; + // positive intergers get multiplied + @if $exponent > 1 { + @for $i from 2 through $exponent { + $value: $value * $base; } } + // negitive intergers get divided. A number divided by itself is 1 + @if $exponent < 1 { + @for $i from 0 through -$exponent { + $value: $value / $base; } } + // return the last value written + @return $value; +} + +@function pow($base, $exponent) { + @return exponent($base, $exponent); +} + +// Transition mixins +@mixin transition($args...) { + -webkit-transition: $args; + -moz-transition: $args; + transition: $args; +} + +@mixin transition-property($args...) { + -webkit-transition-property: $args; + -moz-transition-property: $args; + transition-property: $args; +} + +@mixin animation($args...) { + -webkit-animation: $args; + -moz-animation: $args; + animation: $args; +} + +@mixin animation-fill-mode($args...) { + -webkit-animation-fill-mode: $args; + -moz-animation-fill-mode: $args; + animation-fill-mode: $args; +} + +@mixin transform($args...) { + -webkit-transform: $args; + -moz-transform: $args; + transform: $args; +} + +// Keyframe animations +@mixin keyframes($animation-name) { + @-webkit-keyframes $animation-name { + @content; + } + @-moz-keyframes $animation-name { + @content; + } + @keyframes $animation-name { + @content; + } +} + +// Media queries +@mixin smaller($width) { + @media screen and (max-width: $width) { + @content; + } +} diff --git a/sources/style/main.css b/sources/style/main.css new file mode 100644 index 0000000..8761bd4 --- /dev/null +++ b/sources/style/main.css @@ -0,0 +1,753 @@ +@import url(fonts/clear-sans.css); +html, body { + margin: 0; + padding: 0; + background: #faf8ef; + color: #776e65; + font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; + font-size: 18px; } + +body { + margin: 80px 0; } + +.heading:after { + content: ""; + display: block; + clear: both; } + +h1.title { + font-size: 80px; + font-weight: bold; + margin: 0; + display: block; + float: left; } + +@-webkit-keyframes move-up { + 0% { + top: 25px; + opacity: 1; } + + 100% { + top: -50px; + opacity: 0; } } +@-moz-keyframes move-up { + 0% { + top: 25px; + opacity: 1; } + + 100% { + top: -50px; + opacity: 0; } } +@keyframes move-up { + 0% { + top: 25px; + opacity: 1; } + + 100% { + top: -50px; + opacity: 0; } } +.scores-container { + float: right; + text-align: right; } + +.score-container, .best-container { + position: relative; + display: inline-block; + background: #bbada0; + padding: 15px 25px; + font-size: 25px; + height: 25px; + line-height: 47px; + font-weight: bold; + border-radius: 3px; + color: white; + margin-top: 8px; + text-align: center; } + .score-container:after, .best-container:after { + position: absolute; + width: 100%; + top: 10px; + left: 0; + text-transform: uppercase; + font-size: 13px; + line-height: 13px; + text-align: center; + color: #dddddd; } + .score-container .score-addition, .best-container .score-addition { + position: absolute; + right: 30px; + color: red; + font-size: 25px; + line-height: 25px; + font-weight: bold; + color: rgba(119, 110, 101, 0.9); + z-index: 100; + -webkit-animation: move-up 600ms ease-in; + -moz-animation: move-up 600ms ease-in; + animation: move-up 600ms ease-in; + -webkit-animation-fill-mode: both; + -moz-animation-fill-mode: both; + animation-fill-mode: both; } + +.score-container:after { + content: "Score"; } + +.best-container:after { + content: "Best"; } + +p { + margin-top: 0; + margin-bottom: 10px; + line-height: 1.65; } + +a { + color: #776e65; + font-weight: bold; + text-decoration: underline; + cursor: pointer; } + +strong.important { + text-transform: uppercase; } + +hr { + border: none; + border-bottom: 1px solid #d8d4d0; + margin-top: 20px; + margin-bottom: 30px; } + +.container { + width: 500px; + margin: 0 auto; } + +@-webkit-keyframes fade-in { + 0% { + opacity: 0; } + + 100% { + opacity: 1; } } +@-moz-keyframes fade-in { + 0% { + opacity: 0; } + + 100% { + opacity: 1; } } +@keyframes fade-in { + 0% { + opacity: 0; } + + 100% { + opacity: 1; } } +.game-container { + margin-top: 40px; + position: relative; + padding: 15px; + cursor: default; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + background: #bbada0; + border-radius: 6px; + width: 500px; + height: 500px; + -ms-touch-action: none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + .game-container .game-message { + display: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(221, 221, 221, 0.5); + z-index: 100; + text-align: center; + -webkit-animation: fade-in 800ms ease 1200ms; + -moz-animation: fade-in 800ms ease 1200ms; + animation: fade-in 800ms ease 1200ms; + -webkit-animation-fill-mode: both; + -moz-animation-fill-mode: both; + animation-fill-mode: both; } + .game-container .game-message p { + font-size: 60px; + font-weight: bold; + height: 60px; + line-height: 60px; + margin-top: 222px; } + .game-container .game-message .lower { + display: block; + margin-top: 59px; } + .game-container .game-message a { + display: inline-block; + background: #8f7a66; + border-radius: 3px; + padding: 0 20px; + text-decoration: none; + color: #f9f6f2; + height: 40px; + line-height: 42px; + margin-left: 9px; } + .game-container .game-message a.keep-playing-button { + display: none; } + .game-container .game-message.game-won { + background: rgba(203, 242, 233, 0.5); + color: #f9f6f2; } + .game-container .game-message.game-won a.keep-playing-button { + display: inline-block; } + .game-container .game-message.game-won, .game-container .game-message.game-over { + display: block; } + +.grid-container { + position: absolute; + z-index: 1; } + +.grid-row { + margin-bottom: 15px; } + .grid-row:last-child { + margin-bottom: 0; } + .grid-row:after { + content: ""; + display: block; + clear: both; } + +.grid-cell { + width: 82px; + height: 82px; + margin-right: 15px; + float: left; + border-radius: 3px; + background: rgba(221, 221, 221, 0.35); } + .grid-cell:last-child { + margin-right: 0; } + +.tile-container { + position: absolute; + z-index: 2; } + +.tile, .tile .tile-inner { + width: 82px; + height: 82px; + line-height: 92px; } +.tile.tile-position-1-1 { + -webkit-transform: translate(0px, 0px); + -moz-transform: translate(0px, 0px); + transform: translate(0px, 0px); } +.tile.tile-position-1-2 { + -webkit-transform: translate(0px, 97px); + -moz-transform: translate(0px, 97px); + transform: translate(0px, 97px); } +.tile.tile-position-1-3 { + -webkit-transform: translate(0px, 194px); + -moz-transform: translate(0px, 194px); + transform: translate(0px, 194px); } +.tile.tile-position-1-4 { + -webkit-transform: translate(0px, 291px); + -moz-transform: translate(0px, 291px); + transform: translate(0px, 291px); } +.tile.tile-position-1-5 { + -webkit-transform: translate(0px, 388px); + -moz-transform: translate(0px, 388px); + transform: translate(0px, 388px); } +.tile.tile-position-2-1 { + -webkit-transform: translate(97px, 0px); + -moz-transform: translate(97px, 0px); + transform: translate(97px, 0px); } +.tile.tile-position-2-2 { + -webkit-transform: translate(97px, 97px); + -moz-transform: translate(97px, 97px); + transform: translate(97px, 97px); } +.tile.tile-position-2-3 { + -webkit-transform: translate(97px, 194px); + -moz-transform: translate(97px, 194px); + transform: translate(97px, 194px); } +.tile.tile-position-2-4 { + -webkit-transform: translate(97px, 291px); + -moz-transform: translate(97px, 291px); + transform: translate(97px, 291px); } +.tile.tile-position-2-5 { + -webkit-transform: translate(97px, 388px); + -moz-transform: translate(97px, 388px); + transform: translate(97px, 388px); } +.tile.tile-position-3-1 { + -webkit-transform: translate(194px, 0px); + -moz-transform: translate(194px, 0px); + transform: translate(194px, 0px); } +.tile.tile-position-3-2 { + -webkit-transform: translate(194px, 97px); + -moz-transform: translate(194px, 97px); + transform: translate(194px, 97px); } +.tile.tile-position-3-3 { + -webkit-transform: translate(194px, 194px); + -moz-transform: translate(194px, 194px); + transform: translate(194px, 194px); } +.tile.tile-position-3-4 { + -webkit-transform: translate(194px, 291px); + -moz-transform: translate(194px, 291px); + transform: translate(194px, 291px); } +.tile.tile-position-3-5 { + -webkit-transform: translate(194px, 388px); + -moz-transform: translate(194px, 388px); + transform: translate(194px, 388px); } +.tile.tile-position-4-1 { + -webkit-transform: translate(291px, 0px); + -moz-transform: translate(291px, 0px); + transform: translate(291px, 0px); } +.tile.tile-position-4-2 { + -webkit-transform: translate(291px, 97px); + -moz-transform: translate(291px, 97px); + transform: translate(291px, 97px); } +.tile.tile-position-4-3 { + -webkit-transform: translate(291px, 194px); + -moz-transform: translate(291px, 194px); + transform: translate(291px, 194px); } +.tile.tile-position-4-4 { + -webkit-transform: translate(291px, 291px); + -moz-transform: translate(291px, 291px); + transform: translate(291px, 291px); } +.tile.tile-position-4-5 { + -webkit-transform: translate(291px, 388px); + -moz-transform: translate(291px, 388px); + transform: translate(291px, 388px); } +.tile.tile-position-5-1 { + -webkit-transform: translate(388px, 0px); + -moz-transform: translate(388px, 0px); + transform: translate(388px, 0px); } +.tile.tile-position-5-2 { + -webkit-transform: translate(388px, 97px); + -moz-transform: translate(388px, 97px); + transform: translate(388px, 97px); } +.tile.tile-position-5-3 { + -webkit-transform: translate(388px, 194px); + -moz-transform: translate(388px, 194px); + transform: translate(388px, 194px); } +.tile.tile-position-5-4 { + -webkit-transform: translate(388px, 291px); + -moz-transform: translate(388px, 291px); + transform: translate(388px, 291px); } +.tile.tile-position-5-5 { + -webkit-transform: translate(388px, 388px); + -moz-transform: translate(388px, 388px); + transform: translate(388px, 388px); } + +.tile { + position: absolute; + -webkit-transition: 100ms ease-in-out; + -moz-transition: 100ms ease-in-out; + transition: 100ms ease-in-out; + -webkit-transition-property: -webkit-transform; + -moz-transition-property: -moz-transform; + transition-property: transform; } + .tile .tile-inner { + border-radius: 3px; + background: #dddddd; + text-align: center; + font-weight: bold; + z-index: 10; + font-size: 55px; } + .tile.tile-3 .tile-inner { + background: #d8c1d3; } + .tile.tile-9 .tile-inner { + background: #bcd7de; } + .tile.tile-27 .tile-inner { + color: #f9f6f2; + background: #ca96c3; } + .tile.tile-81 .tile-inner { + color: #f9f6f2; + background: #83aec1; } + .tile.tile-243 .tile-inner { + color: #f9f6f2; + background: #ae7ca9; + font-size: 45px; } + @media screen and (max-width: 520px) { + .tile.tile-243 .tile-inner { + font-size: 25px; } } + .tile.tile-729 .tile-inner { + color: #f9f6f2; + background: #6dcdb5; + font-size: 45px; } + @media screen and (max-width: 520px) { + .tile.tile-729 .tile-inner { + font-size: 25px; } } + .tile.tile-2187 .tile-inner { + color: #f9f6f2; + background: #d2e9e4; + box-shadow: 0 0 30px 10px rgba(255, 255, 255, 0.2381), inset 0 0 0 1px rgba(255, 255, 255, 0.14286); + font-size: 35px; } + @media screen and (max-width: 520px) { + .tile.tile-2187 .tile-inner { + font-size: 15px; } } + .tile.tile-6561 .tile-inner { + color: #f9f6f2; + background: #d0ebe5; + box-shadow: 0 0 30px 10px rgba(255, 255, 255, 0.31746), inset 0 0 0 1px rgba(255, 255, 255, 0.19048); + font-size: 35px; } + @media screen and (max-width: 520px) { + .tile.tile-6561 .tile-inner { + font-size: 15px; } } + .tile.tile-19683 .tile-inner { + color: #f9f6f2; + background: #ceede6; + box-shadow: 0 0 30px 10px rgba(255, 255, 255, 0.39683), inset 0 0 0 1px rgba(255, 255, 255, 0.2381); + font-size: 35px; } + @media screen and (max-width: 520px) { + .tile.tile-19683 .tile-inner { + font-size: 15px; } } + .tile.tile-59049 .tile-inner { + color: #f9f6f2; + background: #ccefe7; + box-shadow: 0 0 30px 10px rgba(255, 255, 255, 0.47619), inset 0 0 0 1px rgba(255, 255, 255, 0.28571); + font-size: 35px; } + @media screen and (max-width: 520px) { + .tile.tile-59049 .tile-inner { + font-size: 15px; } } + .tile.tile-177147 .tile-inner { + color: #f9f6f2; + background: #cbf2e9; + box-shadow: 0 0 30px 10px rgba(255, 255, 255, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333); + font-size: 35px; } + @media screen and (max-width: 520px) { + .tile.tile-177147 .tile-inner { + font-size: 15px; } } + .tile.tile-super .tile-inner { + color: #f9f6f2; + background: #9e2187; + font-size: 30px; } + @media screen and (max-width: 520px) { + .tile.tile-super .tile-inner { + font-size: 10px; } } + +@-webkit-keyframes appear { + 0% { + opacity: 0; + -webkit-transform: scale(0); + -moz-transform: scale(0); + transform: scale(0); } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + -moz-transform: scale(1); + transform: scale(1); } } +@-moz-keyframes appear { + 0% { + opacity: 0; + -webkit-transform: scale(0); + -moz-transform: scale(0); + transform: scale(0); } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + -moz-transform: scale(1); + transform: scale(1); } } +@keyframes appear { + 0% { + opacity: 0; + -webkit-transform: scale(0); + -moz-transform: scale(0); + transform: scale(0); } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + -moz-transform: scale(1); + transform: scale(1); } } +.tile-new .tile-inner { + -webkit-animation: appear 200ms ease 100ms; + -moz-animation: appear 200ms ease 100ms; + animation: appear 200ms ease 100ms; + -webkit-animation-fill-mode: backwards; + -moz-animation-fill-mode: backwards; + animation-fill-mode: backwards; } + +@-webkit-keyframes pop { + 0% { + -webkit-transform: scale(0); + -moz-transform: scale(0); + transform: scale(0); } + + 50% { + -webkit-transform: scale(1.2); + -moz-transform: scale(1.2); + transform: scale(1.2); } + + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + transform: scale(1); } } +@-moz-keyframes pop { + 0% { + -webkit-transform: scale(0); + -moz-transform: scale(0); + transform: scale(0); } + + 50% { + -webkit-transform: scale(1.2); + -moz-transform: scale(1.2); + transform: scale(1.2); } + + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + transform: scale(1); } } +@keyframes pop { + 0% { + -webkit-transform: scale(0); + -moz-transform: scale(0); + transform: scale(0); } + + 50% { + -webkit-transform: scale(1.2); + -moz-transform: scale(1.2); + transform: scale(1.2); } + + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + transform: scale(1); } } +.tile-merged .tile-inner { + z-index: 20; + -webkit-animation: pop 200ms ease 100ms; + -moz-animation: pop 200ms ease 100ms; + animation: pop 200ms ease 100ms; + -webkit-animation-fill-mode: backwards; + -moz-animation-fill-mode: backwards; + animation-fill-mode: backwards; } + +.game-intro { + margin-bottom: 0; } + +.game-explanation { + margin-top: 50px; } + +@media screen and (max-width: 520px) { + html, body { + font-size: 15px; } + + body { + margin: 20px 0; + padding: 0 20px; } + + h1.title { + font-size: 27px; + margin-top: 15px; } + + .container { + width: 280px; + margin: 0 auto; } + + .score-container, .best-container { + margin-top: 0; + padding: 15px 10px; + min-width: 40px; } + + .heading { + margin-bottom: 10px; } + + .game-container { + margin-top: 40px; + position: relative; + padding: 10px; + cursor: default; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + background: #bbada0; + border-radius: 6px; + width: 280px; + height: 280px; + -ms-touch-action: none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + .game-container .game-message { + display: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(221, 221, 221, 0.5); + z-index: 100; + text-align: center; + -webkit-animation: fade-in 800ms ease 1200ms; + -moz-animation: fade-in 800ms ease 1200ms; + animation: fade-in 800ms ease 1200ms; + -webkit-animation-fill-mode: both; + -moz-animation-fill-mode: both; + animation-fill-mode: both; } + .game-container .game-message p { + font-size: 60px; + font-weight: bold; + height: 60px; + line-height: 60px; + margin-top: 222px; } + .game-container .game-message .lower { + display: block; + margin-top: 59px; } + .game-container .game-message a { + display: inline-block; + background: #8f7a66; + border-radius: 3px; + padding: 0 20px; + text-decoration: none; + color: #f9f6f2; + height: 40px; + line-height: 42px; + margin-left: 9px; } + .game-container .game-message a.keep-playing-button { + display: none; } + .game-container .game-message.game-won { + background: rgba(203, 242, 233, 0.5); + color: #f9f6f2; } + .game-container .game-message.game-won a.keep-playing-button { + display: inline-block; } + .game-container .game-message.game-won, .game-container .game-message.game-over { + display: block; } + + .grid-container { + position: absolute; + z-index: 1; } + + .grid-row { + margin-bottom: 10px; } + .grid-row:last-child { + margin-bottom: 0; } + .grid-row:after { + content: ""; + display: block; + clear: both; } + + .grid-cell { + width: 44px; + height: 44px; + margin-right: 10px; + float: left; + border-radius: 3px; + background: rgba(221, 221, 221, 0.35); } + .grid-cell:last-child { + margin-right: 0; } + + .tile-container { + position: absolute; + z-index: 2; } + + .tile, .tile .tile-inner { + width: 44px; + height: 44px; + line-height: 54px; } + .tile.tile-position-1-1 { + -webkit-transform: translate(0px, 0px); + -moz-transform: translate(0px, 0px); + transform: translate(0px, 0px); } + .tile.tile-position-1-2 { + -webkit-transform: translate(0px, 54px); + -moz-transform: translate(0px, 54px); + transform: translate(0px, 54px); } + .tile.tile-position-1-3 { + -webkit-transform: translate(0px, 108px); + -moz-transform: translate(0px, 108px); + transform: translate(0px, 108px); } + .tile.tile-position-1-4 { + -webkit-transform: translate(0px, 162px); + -moz-transform: translate(0px, 162px); + transform: translate(0px, 162px); } + .tile.tile-position-1-5 { + -webkit-transform: translate(0px, 216px); + -moz-transform: translate(0px, 216px); + transform: translate(0px, 216px); } + .tile.tile-position-2-1 { + -webkit-transform: translate(54px, 0px); + -moz-transform: translate(54px, 0px); + transform: translate(54px, 0px); } + .tile.tile-position-2-2 { + -webkit-transform: translate(54px, 54px); + -moz-transform: translate(54px, 54px); + transform: translate(54px, 54px); } + .tile.tile-position-2-3 { + -webkit-transform: translate(54px, 108px); + -moz-transform: translate(54px, 108px); + transform: translate(54px, 108px); } + .tile.tile-position-2-4 { + -webkit-transform: translate(54px, 162px); + -moz-transform: translate(54px, 162px); + transform: translate(54px, 162px); } + .tile.tile-position-2-5 { + -webkit-transform: translate(54px, 216px); + -moz-transform: translate(54px, 216px); + transform: translate(54px, 216px); } + .tile.tile-position-3-1 { + -webkit-transform: translate(108px, 0px); + -moz-transform: translate(108px, 0px); + transform: translate(108px, 0px); } + .tile.tile-position-3-2 { + -webkit-transform: translate(108px, 54px); + -moz-transform: translate(108px, 54px); + transform: translate(108px, 54px); } + .tile.tile-position-3-3 { + -webkit-transform: translate(108px, 108px); + -moz-transform: translate(108px, 108px); + transform: translate(108px, 108px); } + .tile.tile-position-3-4 { + -webkit-transform: translate(108px, 162px); + -moz-transform: translate(108px, 162px); + transform: translate(108px, 162px); } + .tile.tile-position-3-5 { + -webkit-transform: translate(108px, 216px); + -moz-transform: translate(108px, 216px); + transform: translate(108px, 216px); } + .tile.tile-position-4-1 { + -webkit-transform: translate(162px, 0px); + -moz-transform: translate(162px, 0px); + transform: translate(162px, 0px); } + .tile.tile-position-4-2 { + -webkit-transform: translate(162px, 54px); + -moz-transform: translate(162px, 54px); + transform: translate(162px, 54px); } + .tile.tile-position-4-3 { + -webkit-transform: translate(162px, 108px); + -moz-transform: translate(162px, 108px); + transform: translate(162px, 108px); } + .tile.tile-position-4-4 { + -webkit-transform: translate(162px, 162px); + -moz-transform: translate(162px, 162px); + transform: translate(162px, 162px); } + .tile.tile-position-4-5 { + -webkit-transform: translate(162px, 216px); + -moz-transform: translate(162px, 216px); + transform: translate(162px, 216px); } + .tile.tile-position-5-1 { + -webkit-transform: translate(216px, 0px); + -moz-transform: translate(216px, 0px); + transform: translate(216px, 0px); } + .tile.tile-position-5-2 { + -webkit-transform: translate(216px, 54px); + -moz-transform: translate(216px, 54px); + transform: translate(216px, 54px); } + .tile.tile-position-5-3 { + -webkit-transform: translate(216px, 108px); + -moz-transform: translate(216px, 108px); + transform: translate(216px, 108px); } + .tile.tile-position-5-4 { + -webkit-transform: translate(216px, 162px); + -moz-transform: translate(216px, 162px); + transform: translate(216px, 162px); } + .tile.tile-position-5-5 { + -webkit-transform: translate(216px, 216px); + -moz-transform: translate(216px, 216px); + transform: translate(216px, 216px); } + + .game-container { + margin-top: 20px; } + + .tile .tile-inner { + font-size: 35px; } + + .game-message p { + font-size: 30px !important; + height: 30px !important; + line-height: 30px !important; + margin-top: 90px !important; } + .game-message .lower { + margin-top: 30px !important; } } diff --git a/sources/style/main.scss b/sources/style/main.scss new file mode 100644 index 0000000..58082ac --- /dev/null +++ b/sources/style/main.scss @@ -0,0 +1,520 @@ +@import "helpers"; +@import "fonts/clear-sans.css"; + +$field-width: 500px; +$grid-spacing: 15px; +$grid-row-cells: 5; +$tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; +$tile-border-radius: 3px; + +$mobile-threshold: $field-width + 20px; + +$text-color: #776E65; +$bright-text-color: #f9f6f2; + +$tile-color: #dddddd; +$tile-gold-color: #CBF2E9; +$tile-super-color: #9E2187; +$tile-gold-glow-color: lighten($tile-gold-color, 15%); + +$game-container-background: #bbada0; + +$transition-speed: 100ms; + +html, body { + margin: 0; + padding: 0; + + background: #faf8ef; + color: $text-color; + font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; + font-size: 18px; +} + +body { + margin: 80px 0; +} + +.heading:after { + content: ""; + display: block; + clear: both; +} + +h1.title { + font-size: 80px; + font-weight: bold; + margin: 0; + display: block; + float: left; +} + +@include keyframes(move-up) { + 0% { + top: 25px; + opacity: 1; + } + + 100% { + top: -50px; + opacity: 0; + } +} + +.scores-container { + float: right; + text-align: right; +} + +.score-container, .best-container { + $height: 25px; + + position: relative; + display: inline-block; + background: $game-container-background; + padding: 15px 25px; + font-size: $height; + height: $height; + line-height: $height + 22px; + font-weight: bold; + border-radius: 3px; + color: white; + margin-top: 8px; + text-align: center; + + &:after { + position: absolute; + width: 100%; + top: 10px; + left: 0; + text-transform: uppercase; + font-size: 13px; + line-height: 13px; + text-align: center; + color: $tile-color; + } + + .score-addition { + position: absolute; + right: 30px; + color: red; + font-size: $height; + line-height: $height; + font-weight: bold; + color: rgba($text-color, .9); + z-index: 100; + @include animation(move-up 600ms ease-in); + @include animation-fill-mode(both); + } +} + +.score-container:after { + content: "Score"; +} + +.best-container:after { + content: "Best" +} + +p { + margin-top: 0; + margin-bottom: 10px; + line-height: 1.65; +} + +a { + color: $text-color; + font-weight: bold; + text-decoration: underline; + cursor: pointer; +} + +strong { + &.important { + text-transform: uppercase; + } +} + +hr { + border: none; + border-bottom: 1px solid lighten($text-color, 40%); + margin-top: 20px; + margin-bottom: 30px; +} + +.container { + width: $field-width; + margin: 0 auto; +} + +@include keyframes(fade-in) { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +// Styles for buttons +@mixin button { + display: inline-block; + background: darken($game-container-background, 20%); + border-radius: 3px; + padding: 0 20px; + text-decoration: none; + color: $bright-text-color; + height: 40px; + line-height: 42px; +} + +// Game field mixin used to render CSS at different width +@mixin game-field { + .game-container { + margin-top: 40px; + position: relative; + padding: $grid-spacing; + + cursor: default; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + + background: $game-container-background; + border-radius: $tile-border-radius * 2; + width: $field-width; + height: $field-width; + -ms-touch-action: none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + .game-message { + display: none; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba($tile-color, .5); + z-index: 100; + + text-align: center; + + p { + font-size: 60px; + font-weight: bold; + height: 60px; + line-height: 60px; + margin-top: 222px; + // height: $field-width; + // line-height: $field-width; + } + + .lower { + display: block; + margin-top: 59px; + } + + a { + @include button; + margin-left: 9px; + // margin-top: 59px; + + &.keep-playing-button { + display: none; + } + } + + @include animation(fade-in 800ms ease $transition-speed * 12); + @include animation-fill-mode(both); + + &.game-won { + background: rgba($tile-gold-color, .5); + color: $bright-text-color; + + a.keep-playing-button { + display: inline-block; + } + } + + &.game-won, &.game-over { + display: block; + } + } + } + + .grid-container { + position: absolute; + z-index: 1; + } + + .grid-row { + margin-bottom: $grid-spacing; + + &:last-child { + margin-bottom: 0; + } + + &:after { + content: ""; + display: block; + clear: both; + } + } + + .grid-cell { + width: $tile-size; + height: $tile-size; + margin-right: $grid-spacing; + float: left; + + border-radius: $tile-border-radius; + + background: rgba($tile-color, .35); + + &:last-child { + margin-right: 0; + } + } + + .tile-container { + position: absolute; + z-index: 2; + } + + .tile { + &, .tile-inner { + width: ceil($tile-size); + height: ceil($tile-size); + line-height: $tile-size + 10px; + } + + // Build position classes + @for $x from 1 through $grid-row-cells { + @for $y from 1 through $grid-row-cells { + &.tile-position-#{$x}-#{$y} { + $xPos: floor(($tile-size + $grid-spacing) * ($x - 1)); + $yPos: floor(($tile-size + $grid-spacing) * ($y - 1)); + @include transform(translate($xPos, $yPos)); + } + } + } + } +} + +// End of game-field mixin +@include game-field; + +.tile { + position: absolute; // Makes transforms relative to the top-left corner + + .tile-inner { + border-radius: $tile-border-radius; + + background: $tile-color; + text-align: center; + font-weight: bold; + z-index: 10; + + font-size: 55px; + } + + // Movement transition + @include transition($transition-speed ease-in-out); + -webkit-transition-property: -webkit-transform; + -moz-transition-property: -moz-transform; + transition-property: transform; + + $base: 3; + $exponent: 1; + $limit: 11; + + // Colors for all 11 states, false = no special color + $special-colors: #D4ABCC false, // 2 + #A4D1DE false, // 4 + #BF5AAD true, // 8 + #4084A8 true, // 16 + #8F277C true, // 32 + #1AB890 true, // 64 + false true, // 128 + false true, // 256 + false true, // 512 + false true, // 1024 + false true; // 2048 + + // Build tile colors + @while $exponent <= $limit { + $power: pow($base, $exponent); + + &.tile-#{$power} .tile-inner { + // Calculate base background color + $gold-percent: ($exponent - 1) / ($limit - 1) * 100; + $mixed-background: mix($tile-gold-color, $tile-color, $gold-percent); + + $nth-color: nth($special-colors, $exponent); + + $special-background: nth($nth-color, 1); + $bright-color: nth($nth-color, 2); + + @if $special-background { + $mixed-background: mix($special-background, $mixed-background, 55%); + } + + @if $bright-color { + color: $bright-text-color; + } + + // Set background + background: $mixed-background; + + // Add glow + $glow-opacity: max($exponent - 4, 0) / ($limit - 4); + + @if not $special-background { + box-shadow: 0 0 30px 10px rgba($tile-gold-glow-color, $glow-opacity / 1.8), + inset 0 0 0 1px rgba(white, $glow-opacity / 3); + } + + // Adjust font size for bigger numbers + @if $power >= 100 and $power < 1000 { + font-size: 45px; + + // Media queries placed here to avoid carrying over the rest of the logic + @include smaller($mobile-threshold) { + font-size: 25px; + } + } @else if $power >= 1000 { + font-size: 35px; + + @include smaller($mobile-threshold) { + font-size: 15px; + } + } + } + + $exponent: $exponent + 1; + } + + // Super tiles (above 2048) + &.tile-super .tile-inner { + color: $bright-text-color; + background: $tile-super-color; + + font-size: 30px; + + @include smaller($mobile-threshold) { + font-size: 10px; + } + } +} + +@include keyframes(appear) { + 0% { + opacity: 0; + @include transform(scale(0)); + } + + 100% { + opacity: 1; + @include transform(scale(1)); + } +} + +.tile-new .tile-inner { + @include animation(appear 200ms ease $transition-speed); + @include animation-fill-mode(backwards); +} + +@include keyframes(pop) { + 0% { + @include transform(scale(0)); + } + + 50% { + @include transform(scale(1.2)); + } + + 100% { + @include transform(scale(1)); + } +} + +.tile-merged .tile-inner { + z-index: 20; + @include animation(pop 200ms ease $transition-speed); + @include animation-fill-mode(backwards); +} + +.game-intro { + margin-bottom: 0; +} + +.game-explanation { + margin-top: 50px; +} + +@include smaller($mobile-threshold) { + // Redefine variables for smaller screens + $field-width: 280px; + $grid-spacing: 10px; + $grid-row-cells: 5; + $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; + $tile-border-radius: 3px; + + html, body { + font-size: 15px; + } + + body { + margin: 20px 0; + padding: 0 20px; + } + + h1.title { + font-size: 27px; + margin-top: 15px; + } + + .container { + width: $field-width; + margin: 0 auto; + } + + .score-container, .best-container { + margin-top: 0; + padding: 15px 10px; + min-width: 40px; + } + + .heading { + margin-bottom: 10px; + } + + // Render the game field at the right width + @include game-field; + + .game-container { + margin-top: 20px; + } + + // Rest of the font-size adjustments in the tile class + .tile .tile-inner { + font-size: 35px; + } + + .game-message { + p { + font-size: 30px !important; + height: 30px !important; + line-height: 30px !important; + margin-top: 90px !important; + } + + .lower { + margin-top: 30px !important; + } + } +}