From b20e26e3bd5940355846a1308120e2d3adc40acd Mon Sep 17 00:00:00 2001 From: Gabriele Cirulli Date: Sun, 9 Mar 2014 14:32:30 +0100 Subject: [PATCH] implement tile movement --- index.html | 1 + js/application.js | 6 +- js/game_manager.js | 110 +++++++++++++++++++++++++++++++---- js/grid.js | 32 +++++++--- js/html_actuator.js | 45 ++++++++++---- js/keyboard_input_manager.js | 43 ++++++++++++++ js/tile.js | 4 ++ style/helpers.scss | 23 ++++++++ style/main.css | 37 +++++++++++- style/main.scss | 21 ++++++- 10 files changed, 286 insertions(+), 36 deletions(-) create mode 100644 js/keyboard_input_manager.js diff --git a/index.html b/index.html index 263bf72..e741dd4 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,7 @@ + diff --git a/js/application.js b/js/application.js index dde4cc4..1ce870e 100644 --- a/js/application.js +++ b/js/application.js @@ -1,4 +1,4 @@ -document.addEventListener("DOMContentLoaded", function () { - var actuator = new HTMLActuator; - var manager = new GameManager(4, actuator); +// Wait till the browser is ready to render the game (avoids glitches) +window.requestAnimationFrame(function () { + var manager = new GameManager(4, KeyboardInputManager, HTMLActuator); }); diff --git a/js/game_manager.js b/js/game_manager.js index f64660a..9264b97 100644 --- a/js/game_manager.js +++ b/js/game_manager.js @@ -1,9 +1,12 @@ -function GameManager(size, actuator) { - this.size = size; // Grid size - this.actuator = actuator; +function GameManager(size, InputManager, Actuator) { + this.size = size; // Size of the grid + this.inputManager = new InputManager; + this.actuator = new Actuator; - this.startTiles = 2; - this.grid = new Grid(this.size); + this.startTiles = 2; + this.grid = new Grid(this.size); + + this.inputManager.on("move", this.move.bind(this)); this.setup(); } @@ -25,10 +28,12 @@ GameManager.prototype.addStartTiles = function () { // Adds a tile in a random position GameManager.prototype.addRandomTile = function () { - var value = Math.random() < 0.9 ? 2 : 4; - var tile = new Tile(this.grid.randomAvailableCell(), value); - - this.grid.insertTile(tile); + if (this.grid.cellsAvailable()) { + var value = Math.random() < 0.9 ? 2 : 4; + var tile = new Tile(this.grid.randomAvailableCell(), value); + + this.grid.insertTile(tile); + } }; // Sends the updated grid to the actuator @@ -36,9 +41,94 @@ GameManager.prototype.actuate = function () { this.actuator.actuate(this.grid); }; -// Move the grid in the specified direction +// Saves all the current tile positions +GameManager.prototype.saveTilePositions = function () { + this.grid.eachCell(function (x, y, tile) { + if (tile) { + 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.x = cell.x; + tile.y = cell.y; +}; + +// 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; + var cell, tile; + + var vector = this.getVector(direction); + var traversals = this.buildTraversals(vector); + + // Save the current tile positions (for actuator awareness) + this.saveTilePositions(); + + // 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) { + var pos = self.findFarthestPosition(cell, vector); + console.log(pos); + self.moveTile(tile, pos); + } + }); + }); + + this.addRandomTile(); + console.log(16 - this.grid.availableCells().length); 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 (cell.x >= 0 && cell.x < this.size && + cell.y >= 0 && cell.y < this.size && + this.grid.cellAvailable(cell)); + + return previous; +}; diff --git a/js/grid.js b/js/grid.js index 4dfad06..79fe9d9 100644 --- a/js/grid.js +++ b/js/grid.js @@ -29,26 +29,40 @@ Grid.prototype.randomAvailableCell = function () { Grid.prototype.availableCells = function () { var cells = []; - for (var x = 0; x < this.size; x++) { - for (var y = 0; y < this.size; y++) { - var cell = { x: x, y: y }; - - if (this.cellAvailable(cell)) { - cells.push(cell); - } + 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.cells[cell.x][cell.y]; + return !!this.cellContent(cell); +}; + +Grid.prototype.cellContent = function (cell) { + return this.cells[cell.x][cell.y]; }; // Inserts a tile at its position diff --git a/js/html_actuator.js b/js/html_actuator.js index cd6e2bb..0e1975b 100644 --- a/js/html_actuator.js +++ b/js/html_actuator.js @@ -5,13 +5,15 @@ function HTMLActuator() { HTMLActuator.prototype.actuate = function (grid) { var self = this; - this.clearContainer(); + window.requestAnimationFrame(function () { + self.clearContainer(); - grid.cells.forEach(function (column) { - column.forEach(function (cell) { - if (cell) { - self.addTile(cell); - } + grid.cells.forEach(function (column) { + column.forEach(function (cell) { + if (cell) { + self.addTile(cell); + } + }); }); }); }; @@ -23,14 +25,35 @@ HTMLActuator.prototype.clearContainer = function () { }; HTMLActuator.prototype.addTile = function (tile) { - var element = document.createElement("div"); + var self = this; - var x = tile.x + 1; - var y = tile.y + 1; - var position = "tile-position-" + x + "-" + y; + var element = document.createElement("div"); + var position = tile.previousPosition || { x: tile.x, y: tile.y }; + positionClass = this.positionClass(position); - element.classList.add("tile", "tile-" + tile.value, position); + element.classList.add("tile", "tile-" + tile.value, positionClass); element.textContent = tile.value; this.tileContainer.appendChild(element); + + + if (tile.previousPosition) { + window.requestAnimationFrame(function () { + // console.log( + " === " + positionClass); + element.classList.remove(element.classList[2]); + element.classList.add(self.positionClass({ x: tile.x, y: tile.y })); + }); + } else { + element.classList.add("tile-new"); + } + +}; + +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; }; diff --git a/js/keyboard_input_manager.js b/js/keyboard_input_manager.js new file mode 100644 index 0000000..1a1851f --- /dev/null +++ b/js/keyboard_input_manager.js @@ -0,0 +1,43 @@ +function KeyboardInputManager() { + this.events = {}; + + 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 + }; + + document.addEventListener("keydown", function (event) { + var modifiers = event.altKey && event.ctrlKey && event.metaKey && + event.shiftKey; + var mapped = map[event.which]; + + if (!modifiers && mapped !== undefined) { + event.preventDefault(); + self.emit("move", mapped); + } + }); +}; diff --git a/js/tile.js b/js/tile.js index 25664e5..c19b277 100644 --- a/js/tile.js +++ b/js/tile.js @@ -5,3 +5,7 @@ function Tile(position, value) { this.previousPosition = null; } + +Tile.prototype.savePosition = function () { + this.previousPosition = { x: this.x, y: this.y }; +}; diff --git a/style/helpers.scss b/style/helpers.scss index 1a31a11..6bab89e 100644 --- a/style/helpers.scss +++ b/style/helpers.scss @@ -30,3 +30,26 @@ -webkit-transition-property: $args; -moz-transition-property: $args; } + +// Keyframe animations +@mixin keyframes($animation-name) { + @-webkit-keyframes $animation-name { + @content; + } + @-moz-keyframes $animation-name { + @content; + } + @keyframes $animation-name { + @content; + } +} + +@mixin animation($str) { + -webkit-animation: #{$str}; + -moz-animation: #{$str}; +} + +@mixin animation-fill-mode($str) { + -webkit-animation-fill-mode: #{$str}; + -moz-animation-fill-mode: #{$str}; +} diff --git a/style/main.css b/style/main.css index ef6b2be..ecda2d0 100644 --- a/style/main.css +++ b/style/main.css @@ -89,8 +89,8 @@ hr { line-height: 116.25px; font-size: 55px; font-weight: bold; - -webkit-transition: 200ms ease; - -moz-transition: 200ms ease; + -webkit-transition: 100ms ease-in-out; + -moz-transition: 100ms ease-in-out; -webkit-transition-property: top, left; -moz-transition-property: top, left; } .tile.tile-position-1-1 { @@ -205,6 +205,39 @@ hr { box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333); font-size: 35px; } +@-webkit-keyframes appear { + 0% { + -webkit-transform: scale(1.5); + opacity: 0; } + + 100% { + -webkit-transform: scale(1); + opacity: 1; } } + +@-moz-keyframes appear { + 0% { + -webkit-transform: scale(1.5); + opacity: 0; } + + 100% { + -webkit-transform: scale(1); + opacity: 1; } } + +@keyframes appear { + 0% { + -webkit-transform: scale(1.5); + opacity: 0; } + + 100% { + -webkit-transform: scale(1); + opacity: 1; } } + +.tile-new { + -webkit-animation: appear 200ms ease 100ms; + -moz-animation: appear 200ms ease 100ms; + -webkit-animation-fill-mode: both; + -moz-animation-fill-mode: both; } + .game-intro { margin-bottom: 0; } diff --git a/style/main.scss b/style/main.scss index 89e8565..5ae25cf 100644 --- a/style/main.scss +++ b/style/main.scss @@ -14,6 +14,8 @@ $tile-color: #eee4da; $tile-gold-color: #edc22e; $tile-gold-glow-color: lighten($tile-gold-color, 15%); +$transition-speed: 100ms; + html, body { margin: 0; padding: 0; @@ -132,7 +134,7 @@ hr { font-size: 55px; font-weight: bold; - @include transition(200ms ease); + @include transition($transition-speed ease-in-out); @include transition-property(top, left); // Build position classes @@ -205,6 +207,23 @@ hr { } } +@include keyframes(appear) { + 0% { + -webkit-transform: scale(1.5); + opacity: 0; + } + + 100% { + -webkit-transform: scale(1); + opacity: 1; + } +} + +.tile-new { + @include animation(appear 200ms ease $transition-speed); + @include animation-fill-mode(both); +} + .game-intro { margin-bottom: 0; }