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 - [](https://ci-apps.yunohost.org/ci/apps/243/)
+* x86-64 - [](https://ci-apps.yunohost.org/ci/apps/243/)
* ARMv8-A - [](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 - [](https://ci-apps.yunohost.org/ci/apps/243/)
+* x86-64 - [](https://ci-apps.yunohost.org/ci/apps/243/)
* ARMv8-A - [](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
+
+[](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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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;
+ }
+ }
+}