diff --git a/animations/click.js b/animations/click.js index b33c1d2..1efdb76 100644 --- a/animations/click.js +++ b/animations/click.js @@ -4,9 +4,6 @@ window.requestAnimFrame = (function (callback) { } })(); -const overlayCanvas = document.getElementById('minesweeper-overlay'); -const overlayCtx = overlayCanvas.getContext('2d'); - const particlesPerExplosion = 10; const particlesMinSpeed = 3; const particlesMaxSpeed = 5; @@ -40,7 +37,7 @@ function drawClickAnimation() { now = Date.now(); delta = now - then; - overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); + game.layer1.clearRect(0, 0, game.width, game.height); // New frame if (delta > interval) { @@ -81,11 +78,11 @@ function drawExplosion() { continue; } - overlayCtx.beginPath(); - overlayCtx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false); - overlayCtx.closePath(); - overlayCtx.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')'; - overlayCtx.fill(); + game.layer1.beginPath(); + game.layer1.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false); + game.layer1.closePath(); + game.layer1.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')'; + game.layer1.fill(); // Update particle.x += particle.xv; @@ -158,5 +155,3 @@ function randInt(min, max, positive) { return num; } - -drawClickAnimation(); diff --git a/animations/victory.js b/animations/victory.js index b3545d3..ef14800 100644 --- a/animations/victory.js +++ b/animations/victory.js @@ -1,18 +1,10 @@ -const overlay2Canvas = document.getElementById('minesweeper-overlay2'); -const overlay2Ctx = overlay2Canvas.getContext('2d'); - -let W = window.innerWidth, - H = window.innerHeight, - circles = []; - -overlay2Canvas.width = W; -overlay2Canvas.height = H; +let circles = []; //Random Circles creator -function Create() { +function create() { //Place the circles at the center - this.x = W/2; - this.y = H/2; + this.x = game.width / 2; + this.y = game.height / 2; //Random radius between 2 and 6 this.radius = 2 + Math.random()*3; @@ -30,32 +22,32 @@ function Create() { function initBalls() { circles = []; for (let i = 0; i < 500; i++) { - circles.push(new Create()); + circles.push(new create()); } } function drawVictory() { //Fill overlay2Canvas with black color - overlayCtx.globalCompositeOperation = "source-over"; - overlayCtx.fillStyle = "rgba(0,0,0,0.15)"; - overlayCtx.fillRect(0, 0, W, H); + game.layer1.globalCompositeOperation = "source-over"; + game.layer1.fillStyle = "rgba(0,0,0,0.15)"; + game.layer1.fillRect(0, 0, game.width, game.height); //Fill the overlay2Canvas with circles for(let j = 0; j < circles.length; j++){ const c = circles[j]; - //Create the circles - overlayCtx.beginPath(); - overlayCtx.arc(c.x, c.y, c.radius, 0, Math.PI*2, false); - overlayCtx.fillStyle = "rgba("+c.r+", "+c.g+", "+c.b+", 0.5)"; - overlayCtx.fill(); + //create the circles + game.layer1.beginPath(); + game.layer1.arc(c.x, c.y, c.radius, 0, Math.PI*2, false); + game.layer1.fillStyle = "rgba("+c.r+", "+c.g+", "+c.b+", 0.5)"; + game.layer1.fill(); c.x += c.vx; c.y += c.vy; c.radius -= .02; if(c.radius < 0) - circles[j] = new Create(); + circles[j] = new create(); } } diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..10acc9b --- /dev/null +++ b/css/style.css @@ -0,0 +1,136 @@ +html { + overflow: hidden; +} + +body { + margin: 0; + font-family: Roboto, Helvetica, Arial, sans-serif; + font-size: 1.4em; +} + +.start-background { + width: 100vw; + height: 100vh; + background-color: #333; + -webkit-transition: background-color .5s; + -moz-transition: background-color .5s; + -o-transition: background-color .5s; + transition: background-color .5s; +} + +.start-background.transparent { + background-color: transparent; +} + +.start-container { + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-50%, -50%); + -moz-transform: translate(-50%, -50%); + -ms-transform: translate(-50%, -50%); + -o-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + color: #fff; + text-align: center; +} + +.start-container.slideDown { + animation: slideDown .5s ease-in-out forwards; +} + +@keyframes slideDown { + 0% { + top: 50%; + } + + 100% { + top: 150%; + } +} + +.start-container h3 { + font-weight: lighter; +} + +.start-container button { + border: none; + border-radius: 10px; + box-shadow: none; + background-color: #2272ff; + color: #fff; + padding: 20px 40px; + font-size: 28px; + -webkit-transition: all .2s; + -moz-transition: all .2s; + -o-transition: all .2s; + transition: all .2s; + cursor: pointer; +} + +.start-container button:hover { + box-shadow: 0 10px 20px rgba(0,0,0,.18); + transform: translateY(-5px); +} + +.start-container button:active { + background-color: #1e64cd; +} + +.overlay { + position: absolute; + top: 0; + left: 0; +} + +.game-container { + position: absolute; + bottom: 2.5%; + left: 50%; + transform: translateX(-50%); +} + +.game-stats { + position: absolute; + top: calc(2.5vh / 2); + left: 50%; + height: 5vh; + width: 1592px; + transform: translateX(-50%); +} + +.stat-container { + float: left; + background-color: black; + border-radius: 10px; + color: red; + font-size: 4vh; + font-family: "SF Digital Readout", Roboto, Arial, sans-serif; + margin: 0 10px; +} + +.stat-container.right { + float: right; +} + +#time, #bombs { + margin: 0 15px; +} + +.main-container { + position: absolute; + top: -100%; + width: 100%; + height: 100%; + animation: slideIn .5s ease-in-out forwards; +} + +@keyframes slideIn { + from { + top: -100%; + } + + to { + top: 0; + } +} \ No newline at end of file diff --git a/game.js b/game.js new file mode 100644 index 0000000..530726d --- /dev/null +++ b/game.js @@ -0,0 +1,541 @@ +class Game { + constructor() { + const elements = ` +
+
+
+ + 000 + +
+ +
+ + 00:00 + +
+
+ +
+ + + +
+
`.toDOM(); + document.body.appendChild(elements); + + this.canvas = document.getElementById('minesweeper-game'); + this.ctx = this.canvas.getContext('2d'); + + this.layer1Canvas = document.getElementById('minesweeper-overlay'); + this.layer1 = this.layer1Canvas.getContext('2d'); + + this.layer2Canvas = document.getElementById('minesweeper-overlay2'); + this.layer2 = this.layer2Canvas.getContext('2d'); + + this.statsContainer = document.getElementById('game-stats'); + this.timeEl = document.getElementById('time'); + this.bombsEl = document.getElementById('bombs'); + + this.fieldSize = {x: 16, y: 12}; + this.bombCount = 25; + this.field = []; + this.gameOver = false; + this.scaleFactor = .5; + this.isFirstClick = true; + + this.windowX = 0; + this.windowY = 0; + this.zoomFactor = 1; + + this.startTime = 0; + + this.startClientX = 0; + this.startClientY = 0; + this.startWindowX = 0; + this.startWindowY = 0; + + this.hasClicked = false; + this.isDragging = false; + + this.ctx.scale(this.canvas.width / this.fieldSize.x * this.scaleFactor, this.canvas.height / this.fieldSize.y * this.scaleFactor); + } + + applyScaling() { + this.renderingConfig = this.calcScaling(); + + this.drawGrid(false); + } + + calcScaling() { + const width = Math.ceil(this.fieldSize.x * this.zoomFactor) + 1; + const height = Math.ceil(this.fieldSize.y * this.zoomFactor) + 1; + + const offsetX = Math.floor(this.windowX * this.fieldSize.x); + const offsetY = Math.floor(this.windowY * this.fieldSize.y); + + const tiltX = this.windowX * this.fieldSize.x - offsetX; + const tiltY = this.windowY * this.fieldSize.y - offsetY; + + const sizeX = this.tileSize.x / this.zoomFactor; + const sizeY = this.tileSize.y / this.zoomFactor; + + return { + width, height, offsetX, offsetY, tiltX, tiltY, sizeX, sizeY + }; + } + + countBombs(x, y) { + const tiles = this.getSurroundingTiles(x, y); + return tiles.count(true); + } + + countFlaggedBombs(x, y) { + const tiles = this.getSurroundingTiles(x, y); + return tiles.countFlagged(true); + } + + countClickedTiles() { + let count = 0; + for (let x = 0; x < this.fieldSize.x; x++) { + for (let y = 0; y < this.fieldSize.y; y++) { + if (this.field[x][y].clicked && !this.field[x][y].flagged) + count++; + } + } + return count; + } + + countTotalFlags() { + let count = 0; + for (let x = 0; x < this.fieldSize.x; x++) { + for (let y = 0; y < this.fieldSize.y; y++) { + if (this.field[x][y].flagged) + count++; + } + } + return count; + } + + drawGrid(animations = true) { + this.ctx.clearRect(0, 0, this.width, this.height); + + for (let x = 0; x < this.renderingConfig.width; x++) { + for (let y = 0; y < this.renderingConfig.height; y++) { + this.drawTile(x, y, animations); + } + } + } + + drawTile(x, y, animations = true) { + const virtualX = this.renderingConfig.offsetX + x; + const virtualY = this.renderingConfig.offsetY + y; + + if (virtualX >= this.fieldSize.x || virtualY >= this.fieldSize.y) + return; + + const content = this.field[virtualX][virtualY]; + + const width = .8 * this.renderingConfig.sizeX; + const height = .8 * this.renderingConfig.sizeY; + const drawX = (x + .1 - this.renderingConfig.tiltX) * this.renderingConfig.sizeX; + const drawY = (y + .1 - this.renderingConfig.tiltY) * this.renderingConfig.sizeY; + const radius = this.renderingConfig.sizeX * .1; + + let color = this.getColor(virtualX, virtualY); + const fontSize = this.renderingConfig.sizeY * .5; + let fontFamily = "Roboto"; + let textColor = "white"; + let text = ""; + + this.ctx.textAlign = "center"; + let duration = 0; + if (animations) + duration = 150; + + if (!content.flagged && content.clicked) { + color = "#ddd"; + if (content.tileValue === true) { + text = ""; + fontFamily = "FontAwesome"; + textColor = "#aa2211"; + color = "#333"; + } else if (content.tileValue !== 0) { + text = content.tileValue; + textColor = colors[content.tileValue]; + } + } else if (content.flagged) { + color = "#ff0000"; + fontFamily = "FontAwesome"; + text = ""; + } + + animateTile(this.ctx, drawX, drawY, 0, 0, width, height, 0, radius, new Date().getTime(), duration, color); + if (text !== "") { + animateText(this.ctx, this.renderingConfig, text, x, y, 0, fontSize, new Date().getTime(), duration, textColor, fontFamily); + } + } + + gameOverEvent() { + play = false; + animateBackground(0, 0, this.width, this.height, 0, .75, new Date().getTime(), 200, { + r: 0, + g: 0, + b: 0, + a: 0 + }); + animateText(this.layer2, this.renderingConfig, "Game Over", this.fieldSize.x / 2 - .5, this.fieldSize.y / 2 - .5, 0, this.tileSize.y * 1.33, new Date().getTime(), 200, "orange", "Roboto"); + } + + getColor(x, y) { + x++; + y++; + const pos = x * y; + const limit = this.fieldSize.x * this.fieldSize.y; + + let percentage = pos / limit * 360; + + return `hsl(${percentage},100%,50%)`; + } + + getPosition(e) { + const x = (e.x - (window.innerWidth - this.width) / 2) / (this.width * this.zoomFactor) + this.windowX; + const y = (e.y - (window.innerHeight - this.height) * .75) / (this.height * this.zoomFactor) + this.windowY; + const fieldX = Math.floor(x * this.fieldSize.x); + const fieldY = Math.floor(y * this.fieldSize.y); + + return {x: fieldX, y: fieldY}; + } + + getSurroundingTiles(x, y) { + const tiles = {}; + if (x > 0) { + tiles["left"] = {tileValue: this.field[x - 1][y], x: x - 1, y: y}; + if (y > 0) { + tiles["left-top"] = {tileValue: this.field[x - 1][y - 1], x: x - 1, y: y - 1}; + } + if (y < this.fieldSize.y - 1) { + tiles["left-bottom"] = {tileValue: this.field[x - 1][y + 1], x: x - 1, y: y + 1}; + } + } + if (x < this.fieldSize.x - 1) { + tiles["right"] = {tileValue: this.field[x + 1][y], x: x + 1, y: y}; + if (y > 0) + tiles["right-top"] = {tileValue: this.field[x + 1][y - 1], x: x + 1, y: y - 1}; + if (y < this.fieldSize.y - 1) + tiles["right-bottom"] = {tileValue: this.field[x + 1][y + 1], x: x + 1, y: y + 1}; + } + if (y > 0) + tiles["top"] = {tileValue: this.field[x][y - 1], x: x, y: y - 1}; + if (y < this.fieldSize.y - 1) + tiles["bottom"] = {tileValue: this.field[x][y + 1], x: x, y: y + 1}; + return tiles; + } + + initBombs(startX, startY) { + for (let i = 0; i < this.bombCount; i++) { + const ranX = Math.floor(Math.random() * this.fieldSize.x); + const ranY = Math.floor(Math.random() * this.fieldSize.y); + + if (ranX === startX || ranX === startX - 1 || ranX === startX + 1 || ranY === startY || ranY === startY - 1 || ranY === startY + 1 || this.field[ranX][ranY].tileValue === true) { + i--; + continue; + } + + this.field[ranX][ranY].tileValue = true; + } + + for (let x = 0; x < this.fieldSize.x; x++) { + for (let y = 0; y < this.fieldSize.y; y++) { + if (this.field[x][y].tileValue !== true) { + this.field[x][y].tileValue = this.countBombs(x, y); + } + } + } + } + + initEventListeners() { + this.layer2Canvas.addEventListener("click", (e) => { + if (this.isDragging) + return; + + const pos = this.getPosition(e); + + if (this.isFirstClick) { + this.initBombs(pos.x, pos.y); + this.initTime(); + this.isFirstClick = false; + } + + this.tileClickEvent(pos.x, pos.y); + + this.victoryEvent(); + + clicked(e); + }); + + this.layer2Canvas.addEventListener("dblclick", (e) => { + if (this.isDragging) + return; + + const pos = this.getPosition(e); + + this.tileDoubleClick(pos.x, pos.y); + + this.victoryEvent(); + }); + + this.layer2Canvas.addEventListener("contextmenu", (e) => { + if (this.isDragging) + return; + + e.preventDefault(); + + const pos = this.getPosition(e); + + this.tileFlag(pos.x, pos.y); + + this.updateBombs(); + + this.victoryEvent(); + }); + + window.addEventListener("keyup", (e) => { + e.preventDefault(); + + const changeRate = .05; + + let newZoomFactor = this.zoomFactor; + let newWindowX = this.windowX; + let newWindowY = this.windowY; + + if (e.code === "BracketRight") { + newZoomFactor -= changeRate; + } else if (e.code === "Slash") { + newZoomFactor += changeRate; + } else if (e.code === "ArrowLeft") { + newWindowX -= changeRate; + } else if (e.code === "ArrowRight") { + newWindowX += changeRate; + } else if (e.code === "ArrowUp") { + newWindowY -= changeRate; + } else if (e.code === "ArrowDown") { + newWindowY += changeRate; + } else { + return; + } + + newZoomFactor = Math.min(newZoomFactor, 1); + newZoomFactor = Math.max(newZoomFactor, .1); + + newWindowX = Math.min(newWindowX, 1 - newZoomFactor); + newWindowY = Math.min(newWindowY, 1 - newZoomFactor); + newWindowX = Math.max(newWindowX, 0); + newWindowY = Math.max(newWindowY, 0); + + if (newZoomFactor !== this.zoomFactor || newWindowX !== this.windowX || newWindowY !== this.windowY) { + this.zoomFactor = newZoomFactor; + this.windowX = newWindowX; + this.windowY = newWindowY; + this.applyScaling(); + } + }); + + + document.addEventListener("mousedown", (e) => { + if (e.button === 0) { + this.hasClicked = true; + this.startClientX = e.clientX; + this.startClientY = e.clientY; + this.startWindowX = this.windowX; + this.startWindowY = this.windowY; + } + }); + + document.addEventListener("mouseup", () => { + this.hasClicked = false; + if (this.isDragging) { + setTimeout(() => { + this.isDragging = false; + }, 10); + } + }); + + document.addEventListener("mousemove", (e) => { + if (this.hasClicked) { + this.isDragging = true; + + const deltaX = e.clientX - this.startClientX; + const deltaY = e.clientY - this.startClientY; + + this.windowX = this.startWindowX - deltaX / this.width; + this.windowY = this.startWindowY - deltaY / this.height; + + this.windowX = Math.min(this.windowX, 1 - this.zoomFactor); + this.windowY = Math.min(this.windowY, 1 - this.zoomFactor); + this.windowX = Math.max(this.windowX, 0); + this.windowY = Math.max(this.windowY, 0); + + this.applyScaling(); + } + }); + + window.addEventListener("resize", () => { + this.scaleCanvas(); + }); + } + + /** + * Initializes game by creating the game field and setting bombs + */ + initGame() { + for (let x = 0; x < this.fieldSize.x; x++) { + this.field.push([]); + for (let y = 0; y < this.fieldSize.y; y++) { + this.field[x].push({tileValue: 0, clicked: false, flagged: false}); + } + } + + this.scaleCanvas(); + this.updateBombs(); + drawClickAnimation(); + + this.initEventListeners(); + } + + initTime() { + this.startTime = new Date().getTime(); + this.timer = setInterval(() => { + const duration = (new Date().getTime() - this.startTime) / 1000; + const minutes = Math.floor(duration / 60); + const seconds = Math.floor(duration % 60); + + this.timeEl.innerText = (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds; + }, 1000); + } + + restartGame() { + + } + + scaleCanvas() { + let size = window.innerWidth / this.fieldSize.x * .9; + + if (size * this.fieldSize.y > window.innerHeight) { + size = window.innerHeight / this.fieldSize.y * .9; + } + + this.tileSize = {x: size, y: size}; + + this.width = this.canvas.width = this.layer1Canvas.width = this.layer2Canvas.width = this.fieldSize.x * size; + this.height = this.canvas.height = this.layer1Canvas.height = this.layer2Canvas.height = this.fieldSize.y * size; + + this.statsContainer.style.width = this.width + "px"; + + this.applyScaling(); + + initBalls(); + + if (this.gameOver) { + this.gameOverEvent(); + } else if (this.victoryCheck()) { + this.victoryEvent(); + } + } + + testFlagPositions() { + for (let x = 0; x < this.fieldSize.x; x++) { + for (let y = 0; y < this.fieldSize.y; y++) { + if (this.field[x][y].flagged && this.field[x][y].tileValue !== true) + return false; + } + } + return true; + } + + tileClickEvent(x, y) { + if (this.gameOver || this.victoryCheck()) + return; + this.uncoverTile(x, y); + if (!this.field[x][y].flagged && this.field[x][y].tileValue === true) { + this.gameOver = true; + this.gameOverEvent(); + } + } + + tileDoubleClick(x, y) { + if (this.gameOver) + return; + if (this.field[x][y].clicked && !this.field[x][y].flagged && this.countFlaggedBombs(x, y) === this.field[x][y].tileValue) { + this.uncoverSurroundings(x, y); + } + } + + tileFlag(x, y) { + if (this.gameOver) + return; + if (this.field[x][y].clicked && !this.field[x][y].flagged) + return; + this.field[x][y].flagged = !this.field[x][y].flagged; + this.field[x][y].clicked = this.field[x][y].flagged; + + x -= this.renderingConfig.offsetX; + y -= this.renderingConfig.offsetY; + + const drawX = (x - this.renderingConfig.tiltX) * this.renderingConfig.sizeX; + const drawY = (y - this.renderingConfig.tiltY) * this.renderingConfig.sizeY; + + this.ctx.clearRect(drawX, drawY, this.renderingConfig.sizeX, this.renderingConfig.sizeY); + this.drawTile(x, y); + } + + uncoverSurroundings(x, y) { + const surrounding = this.getSurroundingTiles(x, y); + for (let tile in surrounding) { + if (surrounding.hasOwnProperty(tile)) { + this.uncoverTile(surrounding[tile].x, surrounding[tile].y); + } + } + } + + uncoverTile(x, y) { + if (this.field[x][y].clicked || this.field[x][y].flagged) { + return; + } + this.field[x][y].clicked = true; + this.drawTile(x - this.renderingConfig.offsetX, y - this.renderingConfig.offsetY); + if (this.field[x][y].tileValue === true) { + this.gameOverEvent(); + } + if (this.field[x][y].tileValue === 0) { + setTimeout(() => { + this.uncoverSurroundings(x, y); + }, 100); + } + } + + updateBombs() { + const remainingBombs = this.bombCount - this.countTotalFlags(); + this.bombsEl.innerText = (remainingBombs < 100 ? "0" : 0) + (remainingBombs < 10 ? "0" : "") + remainingBombs; + } + + victoryCheck() { + return !play && (this.countClickedTiles() === this.fieldSize.x * this.fieldSize.y - this.bombCount || (this.countTotalFlags() === this.bombCount && this.testFlagPositions())); + + } + + victoryEvent() { + if (this.victoryCheck()) { + animateVictory(); + play = false; + const fontSize = this.tileSize.y * 1.33; + animateBackground(0, 0, this.canvas.width, this.canvas.height, 0, .01, new Date().getTime(), 300, { + r: 0, + g: 0, + b: 0, + a: 0 + }); + animateText(this.layer2, this.renderingConfig, "Victory!", this.fieldSize.x / 2 - .5, this.fieldSize.y / 2 - .5, 0, fontSize, new Date().getTime(), 300, "green", "Roboto"); + } + } +} \ No newline at end of file diff --git a/index.html b/index.html index 8b3c844..2154f23 100644 --- a/index.html +++ b/index.html @@ -3,78 +3,23 @@ Minesweeper.js + - - + -
-
- - 000 - +
+
+

Minesweeper.js

+

A sweet, tasty, little Minesweeper game written in JavaScript with HTML5 <canvas> Element

+
- -
- - 00:00 - -
-
- -
- - -
+ diff --git a/minesweeper.js b/minesweeper.js index 1a0a476..c514cc0 100644 --- a/minesweeper.js +++ b/minesweeper.js @@ -1,26 +1,3 @@ -const canvas = document.getElementById('minesweeper-game'); -const ctx = canvas.getContext('2d'); -const statsContainer = document.getElementById('game-stats'); -const timeEl = document.getElementById('time'); -const bombsEl = document.getElementById('bombs'); - -const fieldSize = {x: 16, y: 12}; -let tileSize; -const bombCount = 25; -const field = []; -let gameOver = false; -const scaleFactor = .5; -let isFirstClick = true; - -let windowX = 0; -let windowY = 0; -let zoomFactor = 1; - -let renderingConfig; - -let startTime = 0; -let timer; - /** * Defines all possible colors for the tile numbers * @type {{ 1: string, 2: string, 3: string, 4: string, 6: string }} @@ -34,8 +11,6 @@ const colors = { 6: "pink" }; -ctx.scale(canvas.width / fieldSize.x * scaleFactor, canvas.height / fieldSize.y * scaleFactor); - function animateBackground(x, y, width, height, curOpacity, finalOpacity, startTime, duration, color) { const time = (new Date()).getTime() - startTime; @@ -60,7 +35,7 @@ function animateBackground(x, y, width, height, curOpacity, finalOpacity, startT }); } -function animateTile(x, y, curWidth, curHeight, finalWidth, finalHeight, curRadius, finalRadius, startTime, duration, color) { +function animateTile(ctx, x, y, curWidth, curHeight, finalWidth, finalHeight, curRadius, finalRadius, startTime, duration, color) { const time = (new Date()).getTime() - startTime; if (curWidth === finalWidth && curHeight === finalHeight && curRadius === finalRadius) @@ -88,16 +63,13 @@ function animateTile(x, y, curWidth, curHeight, finalWidth, finalHeight, curRadi drawRoundedRect(ctx, x + (finalWidth - curWidth) / 2, y + (finalHeight - curHeight) / 2, curWidth, curHeight, curRadius, color); requestAnimFrame(() => { - animateTile(x, y, curWidth, curHeight, finalWidth, finalHeight, curRadius, finalRadius, startTime, duration, color); + animateTile(ctx, x, y, curWidth, curHeight, finalWidth, finalHeight, curRadius, finalRadius, startTime, duration, color); }); } -function animateText(text, x, y, curFontSize, finalFontSize, startTime, duration, color, font, context) { +function animateText(ctx, renderingConfig, text, x, y, curFontSize, finalFontSize, startTime, duration, color, font) { const time = (new Date()).getTime() - startTime; - if (context === undefined) - context = ctx; - if (curFontSize === finalFontSize) return; @@ -115,82 +87,16 @@ function animateText(text, x, y, curFontSize, finalFontSize, startTime, duration if (font === undefined) font = "Roboto"; - context.fillStyle = color; - context.font = "bold " + curFontSize + "px " + font; - context.textAlign = "center"; - context.fillText(text, textDrawX, textDrawY); + ctx.fillStyle = color; + ctx.font = "bold " + curFontSize + "px " + font; + ctx.textAlign = "center"; + ctx.fillText(text, textDrawX, textDrawY); requestAnimFrame(function () { - animateText(text, x, y, curFontSize, finalFontSize, startTime, duration, color, font, context); + animateText(ctx, renderingConfig, text, x, y, curFontSize, finalFontSize, startTime, duration, color, font); }); } -function applyScaling() { - renderingConfig = calcScaling(); - - drawGrid(false); -} - -function calcScaling(field = fieldSize, tile = tileSize, zoom = zoomFactor) { - const width = Math.ceil(field.x * zoom) + 1; - const height = Math.ceil(field.y * zoom) + 1; - - const offsetX = Math.floor(windowX * field.x); - const offsetY = Math.floor(windowY * field.y); - - const tiltX = windowX * field.x - offsetX; - const tiltY = windowY * field.y - offsetY; - - const sizeX = tile.x / zoom; - const sizeY = tile.y / zoom; - - return { - width, height, offsetX, offsetY, tiltX, tiltY, sizeX, sizeY - }; -} - -function countBombs(x, y) { - const tiles = getSurroundingTiles(x, y); - return tiles.count(true); -} - -function countClickedTiles() { - let count = 0; - for (let x = 0; x < fieldSize.x; x++) { - for (let y = 0; y < fieldSize.y; y++) { - if (field[x][y].clicked && !field[x][y].flagged) - count++; - } - } - return count; -} - -function countFlaggedBombs(x, y) { - const tiles = getSurroundingTiles(x, y); - return tiles.countFlagged(true); -} - -function countTotalFlags() { - let count = 0; - for(let x = 0; x < fieldSize.x; x++) { - for(let y = 0; y < fieldSize.y; y++) { - if(field[x][y].flagged) - count++; - } - } - return count; -} - -function drawGrid(animations = true) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - - for (let x = 0; x < renderingConfig.width; x++) { - for (let y = 0; y < renderingConfig.height; y++) { - drawTile(x, y, animations); - } - } -} - function drawRoundedRect(context, x, y, w, h, r, color) { context.fillStyle = color; context.beginPath(); @@ -205,283 +111,12 @@ function drawRoundedRect(context, x, y, w, h, r, color) { context.fill(); } -function drawTile(x, y, animations = true) { - const virtualX = renderingConfig.offsetX + x; - const virtualY = renderingConfig.offsetY + y; - - if(virtualX >= fieldSize.x || virtualY >= fieldSize.y) - return; - - const content = field[virtualX][virtualY]; - - const width = .8 * renderingConfig.sizeX; - const height = .8 * renderingConfig.sizeY; - const drawX = (x + .1 - renderingConfig.tiltX) * renderingConfig.sizeX; - const drawY = (y + .1 - renderingConfig.tiltY) * renderingConfig.sizeY; - const radius = renderingConfig.sizeX * .1; - - let color = getColor(virtualX, virtualY); - const fontSize = renderingConfig.sizeY * .5; - let fontFamily = "Roboto"; - let textColor = "white"; - let text = ""; - - ctx.textAlign = "center"; - let duration = 0; - if (animations) - duration = 150; - - if (!content.flagged && content.clicked) { - color = "#ddd"; - if(content.tileValue === true) { - text = ""; - fontFamily = "FontAwesome"; - textColor = "#aa2211"; - color = "#333"; - } else if (content.tileValue !== 0) { - text = content.tileValue; - textColor = colors[content.tileValue]; - } - } else if (content.flagged) { - color = "#ff0000"; - fontFamily = "FontAwesome"; - text = ""; - } - - animateTile(drawX, drawY, 0, 0, width, height, 0, radius, new Date().getTime(), duration, color); - if(text !== "") { - animateText(text, x, y, 0, fontSize, new Date().getTime(), duration, textColor, fontFamily, ctx); - } -} - function easeInOutCubic(t, b, c, d) { t /= d; t--; return c * (Math.pow(t, 3) + 1) + b; } -function gameOverEvent() { - play = false; - animateBackground(0, 0, canvas.width, canvas.height, 0, .75, new Date().getTime(), 200, {r: 0, g: 0, b: 0, a: 0}); - animateText("Game Over", fieldSize.x / 2 - .5, fieldSize.y / 2 - .5, 0, tileSize.y * 1.33, new Date().getTime(), 200, "orange", "Roboto", overlay2Ctx); -} - -function getColor(x, y) { - x++; - y++; - const pos = x * y; - const limit = fieldSize.x * fieldSize.y; - - let percentage = pos / limit * 360; - - return `hsl(${percentage},100%,50%)`; -} - -function getPosition(e) { - const x = (e.x - (window.innerWidth - W) / 2) / W * zoomFactor + windowX; - const y = (e.y - (window.innerHeight - H) / 2) / H * zoomFactor + windowY; - const fieldX = Math.floor(x * fieldSize.x); - const fieldY = Math.floor(y * fieldSize.y); - - return {x: fieldX, y: fieldY}; -} - -function getSurroundingTiles(x, y) { - const tiles = {}; - if (x > 0) { - tiles["left"] = {tileValue: field[x - 1][y], x: x - 1, y: y}; - if (y > 0) { - tiles["left-top"] = {tileValue: field[x - 1][y - 1], x: x - 1, y: y - 1}; - } - if (y < fieldSize.y - 1) { - tiles["left-bottom"] = {tileValue: field[x - 1][y + 1], x: x - 1, y: y + 1}; - } - } - if (x < fieldSize.x - 1) { - tiles["right"] = {tileValue: field[x + 1][y], x: x + 1, y: y}; - if (y > 0) - tiles["right-top"] = {tileValue: field[x + 1][y - 1], x: x + 1, y: y - 1}; - if (y < fieldSize.y - 1) - tiles["right-bottom"] = {tileValue: field[x + 1][y + 1], x: x + 1, y: y + 1}; - } - if (y > 0) - tiles["top"] = {tileValue: field[x][y - 1], x: x, y: y - 1}; - if (y < fieldSize.y - 1) - tiles["bottom"] = {tileValue: field[x][y + 1], x: x, y: y + 1}; - return tiles; -} - -function initBombs(startX, startY) { - for (let i = 0; i < bombCount; i++) { - const ranX = Math.floor(Math.random() * fieldSize.x); - const ranY = Math.floor(Math.random() * fieldSize.y); - - if (ranX === startX || ranX === startX - 1 || ranX === startX + 1 || ranY === startY || ranY === startY - 1 || ranY === startY + 1 || field[ranX][ranY].tileValue === true) { - i--; - continue; - } - - field[ranX][ranY].tileValue = true; - } - - for (let x = 0; x < fieldSize.x; x++) { - for (let y = 0; y < fieldSize.y; y++) { - if (field[x][y].tileValue !== true) { - field[x][y].tileValue = countBombs(x, y); - } - } - } -} - -/** - * Initializes game by creating the game field and setting bombs - */ -function initGame() { - for (let x = 0; x < fieldSize.x; x++) { - field.push([]); - for (let y = 0; y < fieldSize.y; y++) { - field[x].push({tileValue: 0, clicked: false, flagged: false}); - } - } - - scaleCanvas(); - updateBombs(); -} - -function initTime() { - startTime = new Date().getTime(); - timer = setInterval(() => { - const duration = (new Date().getTime() - startTime) / 1000; - const minutes = Math.floor(duration / 60); - const seconds = Math.floor(duration % 60); - - timeEl.innerText = (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds; - }, 1000); -} - -function scaleCanvas() { - let size = window.innerWidth / fieldSize.x * .9; - - if(size * fieldSize.y > window.innerHeight) { - size = window.innerHeight / fieldSize.y * .9; - } - - tileSize = {x: size, y: size}; - - W = fieldSize.x * size; - H = fieldSize.y * size; - - canvas.width = W; - canvas.height = H; - overlayCanvas.width = W; - overlayCanvas.height = H; - overlay2Canvas.width = W; - overlay2Canvas.height = H; - - statsContainer.style.width = W + "px"; - - initBalls(); - - applyScaling(); - - if (gameOver) { - gameOverEvent(); - } else if (victoryCheck()) { - victoryEvent(); - } -} - -function testFlagPositions() { - for(let x = 0; x < fieldSize.x; x++) { - for(let y = 0; y < fieldSize.y; y++) { - if(field[x][y].flagged && field[x][y].tileValue !== true) - return false; - } - } - return true; -} - -function tileClickEvent(x, y) { - if (gameOver || victoryCheck()) - return; - uncoverTile(x, y); - if (!field[x][y].flagged && field[x][y].tileValue === true) { - gameOver = true; - gameOverEvent(); - } -} - -function tileDoubleClick(x, y) { - if (gameOver) - return; - if (field[x][y].clicked && !field[x][y].flagged && countFlaggedBombs(x, y) === field[x][y].tileValue) { - uncoverSurroundings(x, y); - } -} - -function tileFlag(x, y) { - if (gameOver) - return; - if (field[x][y].clicked && !field[x][y].flagged) - return; - field[x][y].flagged = !field[x][y].flagged; - field[x][y].clicked = field[x][y].flagged; - - x -= renderingConfig.offsetX; - y -= renderingConfig.offsetY; - - const drawX = (x - renderingConfig.tiltX) * renderingConfig.sizeX; - const drawY = (y - renderingConfig.tiltY) * renderingConfig.sizeY; - - ctx.clearRect(drawX, drawY, renderingConfig.sizeX, renderingConfig.sizeY); - drawTile(x, y); -} - -function uncoverSurroundings(x, y) { - const surrounding = getSurroundingTiles(x, y); - for (let tile in surrounding) { - if (surrounding.hasOwnProperty(tile)) { - uncoverTile(surrounding[tile].x, surrounding[tile].y); - } - } -} - -function uncoverTile(x, y) { - if (field[x][y].clicked || field[x][y].flagged) { - return; - } - field[x][y].clicked = true; - drawTile(x - renderingConfig.offsetX, y - renderingConfig.offsetY); - if (field[x][y].tileValue === true) { - gameOverEvent(); - } - if (field[x][y].tileValue === 0) { - setTimeout(() => { - uncoverSurroundings(x, y); - }, 100); - } -} - -function updateBombs() { - const remainingBombs = bombCount - countTotalFlags(); - bombsEl.innerText = (remainingBombs < 100 ? "0" : 0) + (remainingBombs < 10 ? "0" : "") + remainingBombs; -} - -function victoryCheck() { - return !play && (countClickedTiles() === fieldSize.x * fieldSize.y - bombCount || (countTotalFlags() === bombCount && testFlagPositions())); - -} - -function victoryEvent() { - if(victoryCheck()) { - animateVictory(); - play = false; - const fontSize = tileSize.y * 1.33; - animateBackground(0, 0, canvas.width, canvas.height, 0, .01, new Date().getTime(), 300, {r: 0, g: 0, b: 0, a: 0}); - animateText("Victory!", fieldSize.x / 2 - .5, fieldSize.y / 2 - .5, 0, fontSize, new Date().getTime(), 300, "green", "Roboto", overlay2Ctx); - } -} - Object.prototype.count = function (val) { let counter = 0; for (let el in this) { @@ -506,140 +141,27 @@ Object.prototype.countFlagged = function (val) { return counter; }; -overlay2Canvas.addEventListener("click", (e) => { - if(isDragging) - return; +String.prototype.toDOM=function(){ + let d=document + ,i + ,a=d.createElement("div") + ,b=d.createDocumentFragment(); + a.innerHTML=this; + while(i=a.firstChild)b.appendChild(i); + return b; +}; - const pos = getPosition(e); +let game; - if (isFirstClick) { - initBombs(pos.x, pos.y); - initTime(); - isFirstClick = false; - } - - tileClickEvent(pos.x, pos.y); - - victoryEvent(); - - clicked(e); +const startContainer = document.getElementsByClassName('start-container')[0]; +const startBackground = document.getElementsByClassName('start-background')[0]; +const startButton = document.getElementById('startgame'); +startButton.addEventListener('click', () => { + startContainer.classList.add('slideDown'); + startBackground.classList.add('transparent'); + game = new Game(); + game.initGame(); }); -overlay2Canvas.addEventListener("dblclick", (e) => { - if(isDragging) - return; - - const pos = getPosition(e); - - tileDoubleClick(pos.x, pos.y); - - victoryEvent(); -}); - -overlay2Canvas.addEventListener("contextmenu", (e) => { - if(isDragging) - return; - - e.preventDefault(); - - const pos = getPosition(e); - - tileFlag(pos.x, pos.y); - - updateBombs(); - - victoryEvent(); -}); - -window.addEventListener("keyup", (e) => { - e.preventDefault(); - - const changeRate = .05; - - let newZoomFactor = zoomFactor; - let newWindowX = windowX; - let newWindowY = windowY; - - if (e.code === "BracketRight") { - newZoomFactor -= changeRate; - } else if (e.code === "Slash") { - newZoomFactor += changeRate; - } else if (e.code === "ArrowLeft") { - newWindowX -= changeRate; - } else if (e.code === "ArrowRight") { - newWindowX += changeRate; - } else if (e.code === "ArrowUp") { - newWindowY -= changeRate; - } else if (e.code === "ArrowDown") { - newWindowY += changeRate; - } else { - return; - } - - newZoomFactor = Math.min(newZoomFactor, 1); - newZoomFactor = Math.max(newZoomFactor, .1); - - newWindowX = Math.min(newWindowX, 1 - newZoomFactor); - newWindowY = Math.min(newWindowY, 1 - newZoomFactor); - newWindowX = Math.max(newWindowX, 0); - newWindowY = Math.max(newWindowY, 0); - - if(newZoomFactor !== zoomFactor || newWindowX !== windowX || newWindowY !== windowY) { - zoomFactor = newZoomFactor; - windowX = newWindowX; - windowY = newWindowY; - applyScaling(); - } -}); - -let startClientX = 0; -let startClientY = 0; -let startWindowX = 0; -let startWindowY = 0; - -let hasClicked = false; -let isDragging = false; - -document.addEventListener("mousedown", (e) => { - if(e.button === 0) { - hasClicked = true; - startClientX = e.clientX; - startClientY = e.clientY; - startWindowX = windowX; - startWindowY = windowY; - } -}); - -document.addEventListener("mouseup", () => { - hasClicked = false; - if(isDragging) { - setTimeout(() => { - isDragging = false; - }, 10); - } -}); - -document.addEventListener("mousemove", (e) => { - if(hasClicked) { - isDragging = true; - - const deltaX = e.clientX - startClientX; - const deltaY = e.clientY - startClientY; - - windowX = startWindowX - deltaX / W; - windowY = startWindowY - deltaY / H; - - windowX = Math.min(windowX, 1 - zoomFactor); - windowY = Math.min(windowY, 1 - zoomFactor); - windowX = Math.max(windowX, 0); - windowY = Math.max(windowY, 0); - - applyScaling(); - } -}); - -window.addEventListener("resize", () => { - scaleCanvas(); -}); - -initGame(); +// const game = new Game(); +// game.initGame(); \ No newline at end of file