Major rework of basic game code - pre-work for multiplayer

Includes automatic generation of required HTML elements and better responsiveness
This commit is contained in:
Marcel
2019-07-18 11:49:06 +02:00
committed by KingOfDog
parent 544b988a9b
commit 06a2e582fd
14 changed files with 627 additions and 357 deletions

View File

@@ -23,14 +23,9 @@ class Arena {
this.field.unshift(row);
++y;
this.p.score += rowCount * 10;
this.p.score += rowCount * 20;
rowCount *= 2;
}
if (this.p.score - this.g.prevUpdateScore > 50) {
this.g.dropInterval -= 20;
this.g.dropInterval = this.g.dropInterval > 100 ? this.g.dropInterval : 100;
this.g.prevUpdateScore = this.p.score;
}
this.game.drawArena();
}
}

View File

@@ -3,19 +3,43 @@ class GameInfo {
this.fieldSize = {x: 12, y: 20};
this.arena = new Arena(this, game);
this.player = new Player(this, game);
this.player = new LocalPlayer(this, game);
this.canvas = document.getElementById('tetris');
this.context = this.canvas.getContext('2d');
const container = document.createElement('div');
container.className = 'game-instance';
manager.container.appendChild(container);
this.canvasBg = document.getElementById('tetris-background');
this.contextBg = this.canvasBg.getContext('2d');
this.score = document.createElement('div');
this.score.classList.add('game-stats', 'score');
container.appendChild(this.score);
this.canvasHold = document.getElementById('tetris-hold');
this.canvasContainer = document.createElement('div');
this.canvasContainer.className = 'canvas-container';
container.appendChild(this.canvasContainer);
this.canvasHold = document.createElement('canvas');
this.canvasHold.className = 'tetris-hold';
this.contextHold = this.canvasHold.getContext('2d');
this.canvasContainer.appendChild(this.canvasHold);
this.canvasUpcoming = document.getElementById('tetris-upcoming');
this.canvasBg = document.createElement('canvas');
this.canvasBg.className = 'tetris-background';
this.contextBg = this.canvasBg.getContext('2d');
this.canvasContainer.appendChild(this.canvasBg);
this.canvas = document.createElement('canvas');
this.canvas.className = 'tetris-arena';
this.context = this.canvas.getContext('2d');
this.canvasContainer.appendChild(this.canvas);
this.canvasUpcoming = document.createElement('canvas');
this.canvasUpcoming.className = 'tetris-upcoming';
this.contextUpcoming = this.canvasUpcoming.getContext('2d');
this.canvasContainer.appendChild(this.canvasUpcoming);
this.time = document.createElement('div');
this.time.classList.add('game-stats', 'time');
container.appendChild(this.time);
this.isPaused = true;
@@ -25,28 +49,32 @@ class GameInfo {
this.keys = {
down: {
keys: [40, 83],
action: () => this.player.drop()
action: () => {
this.player.drop();
this.player.score++;
this.player.game.updateScore(false);
},
},
left: {
keys: [37, 65],
action: () => this.player.move(-1)
action: () => this.player.move(-1),
},
right: {
keys: [39, 68],
action: () => this.player.move(1)
action: () => this.player.move(1),
},
rotateLeft: {
keys: [81],
action: () => this.player.rotate(-1)
action: () => this.player.rotate(-1),
},
rotateRight: {
keys: [69],
action: () => this.player.rotate(1)
action: () => this.player.rotate(1),
},
holdTile: {
keys: [38, 87],
action: () => this.player.hold()
}
action: () => this.player.hold(),
},
};
this.prevUpdateScore = 0;
@@ -65,6 +93,16 @@ class GameInfo {
modern -> rounded corners
snake -> all tiles are connected
*/
this.theme = 'default';
this.theme = new DefaultTheme();
}
}
updateScore(animate) {
this.score.innerText = this.lastScore;
if (animate) {
this.score.classList.add('update');
setTimeout(() => {
this.score.classList.remove('update');
}, 1000);
}
}
}

View File

@@ -38,54 +38,14 @@ class Game {
}
drawTile(x, y, offset, color, matrix, ctx = this.g.context) {
ctx.fillStyle = color;
x += offset.x;
y += offset.y;
switch (this.g.theme) {
case "default":
ctx.fillRect(x + tileGap / 2, y + tileGap / 2, 1 - tileGap, 1 - tileGap);
break;
case "clean":
ctx.fillRect(x, y, 1, 1);
break;
case "modern":
drawRoundRect(ctx, x + tileGap / 2, y + tileGap / 2, 1 - tileGap, 1 - tileGap, .15);
break;
case "snakes":
let r1 = .15, // top right
r2 = .15, // bottom right
r3 = .15, // bottom left
r4 = .15; // top left
// Is there a tile to the left?
if (matrix[y][x - 1] > 0) {
r3 = 0;
r4 = 0;
}
// Is there a tile to the right?
if (matrix[y][x + 1] > 0) {
r1 = 0;
r2 = 0;
}
// Is there a tile to the top?
if (matrix[y - 1] !== undefined && matrix[y - 1][x] > 0) {
r1 = 0;
r4 = 0;
}
// Is there a tile to the bottom?
if (matrix[y + 1] !== undefined && matrix[y + 1][x] > 0) {
r2 = 0;
r3 = 0;
}
drawRoundRect(ctx, x, y, 1, 1, [r1, r2, r3, r4]);
break;
case "retro":
drawReliefRect(ctx, x, y, 1, 1, .15, color);
break;
default:
this.g.theme = "default";
this.drawTile(x, y, offset, color, matrix, ctx);
break;
}
ctx.save();
ctx.translate(x, y);
this.g.theme.drawTile(color, matrix, ctx, x, y);
ctx.restore();
}
drawUpcoming() {
@@ -141,9 +101,42 @@ class Game {
});
}
rescale() {
let conWidth = manager.container.clientWidth / manager.instances.size;
let conHeight = manager.container.clientHeight - 78;
if (conHeight < conWidth) {
conWidth = conHeight;
}
conWidth = Math.floor(conWidth);
const canvasScale = Math.floor(conWidth * .6 / this.g.fieldSize.x);
const canvasHoldScale = Math.floor(conWidth * .2 / 6);
const realWidth = canvasScale * this.g.fieldSize.x + 2 * canvasHoldScale * 6;
const realHeight = canvasScale * this.g.fieldSize.y;
this.g.canvasContainer.style.width = realWidth + 'px';
this.g.canvasContainer.style.height = realHeight + 'px';
this.g.canvasUpcoming.style.height = this.g.canvasUpcoming.clientWidth * 3 + 'px';
this.g.canvasHold.style.height = this.g.canvasHold.clientWidth + 'px';
this.g.canvasBg.adjustResolution(this.g.contextBg, canvasScale);
this.g.canvas.adjustResolution(this.g.context, canvasScale);
this.g.canvasUpcoming.adjustResolution(this.g.contextUpcoming, canvasHoldScale);
this.g.canvasHold.adjustResolution(this.g.contextHold, canvasHoldScale);
if (!firstRun && this.g.isPaused) {
this.draw();
}
this.redrawScreen();
}
saveHighscore() {
if (getCookie("highscore").value < this.p.score) {
document.cookie = "highscore=" + this.p.score + "; max-age=" + 60 * 60 * 24 * 365 * 1000 + "; path=/;";
if (getCookie('highscore') && getCookie('highscore').value < this.p.score) {
document.cookie = 'highscore=' + this.p.score + '; max-age=' + 60 * 60 * 24 * 365 * 1000 + '; path=/;';
}
}
@@ -175,18 +168,24 @@ class Game {
}
}
updateScore() {
updateScore(animate = true) {
if (this.g.lastScore !== this.p.score) {
scoreUpdateAni();
this.g.updateScore(animate);
this.g.lastScore = this.p.score;
this.saveHighscore();
if (this.p.score - this.p.lastLevelScore > 500) {
this.p.lastLevelScore = this.p.score;
this.p.level++;
this.g.dropInterval *= .9;
}
}
document.getElementById('score').innerText = this.p.score.toString();
this.g.updateScore(false);
}
updateTime() {
timePassed += Date.now() - this.g.lastTimeUpdate;
timeElement.innerHTML = formatMillis(timePassed);
this.g.time.innerText = formatMillis(timePassed);
this.g.lastTimeUpdate = Date.now();
}
}
}

6
js/helper-functions.js Normal file
View File

@@ -0,0 +1,6 @@
HTMLCanvasElement.prototype.adjustResolution = function (ctx, scale) {
this.width = this.clientWidth;
this.height = this.clientHeight;
ctx.scale(scale, scale);
};

View File

@@ -53,6 +53,17 @@ let firstRun = true;
class Language {
constructor(lang) {
const xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
const obj = JSON.parse(this.responseText);
console.log(JSON.stringify(obj));
}
};
xmlhttp.open("GET", "/lang.json", true);
xmlhttp.send();
this.lang = lang;
if(eval('typeof ' + this.lang) === 'undefined')
this.lang = "en";

21
js/local-player.js Normal file
View File

@@ -0,0 +1,21 @@
class LocalPlayer extends Player {
constructor(gameInfo, game) {
super(gameInfo, game);
this.isHolding = false;
this.holdingTile = null;
}
hold() {
if (this.isHolding)
return;
if (this.holdingTile === null) {
this.holdingTile = this.matrix;
this.reset(true);
} else {
this.holdingTile = [this.matrix, this.matrix = this.holdingTile][0];
this.reset(true, false);
}
this.game.drawHolding();
}
}

View File

@@ -6,69 +6,19 @@
let escState = 1;
window.onresize = () => {
scaleWindow();
game.redrawScreen();
manager.callAll('rescale', []);
};
function scaleWindow() {
const canvasContainer = document.getElementById("canvas-container");
let height = .8 * window.innerHeight - 40;
let width = height / (5 / 3);
let conWidth = width + (2 * (height / game.g.fieldSize.y * 5));
const ratio = width / conWidth;
if (conWidth > window.innerWidth * .8) {
conWidth = window.innerWidth * .8;
width = conWidth * ratio;
height = width * (5 / 3);
}
width = Math.floor(width);
height = Math.floor(height);
canvasContainer.style.height = height + "px";
canvasContainer.style.width = conWidth + "px";
const canvasScale = width / game.g.fieldSize.x;
game.g.canvasBg.height = height;
game.g.canvasBg.width = width;
game.g.contextBg.scale(canvasScale, canvasScale);
game.g.canvas.height = height;
game.g.canvas.width = width;
game.g.context.scale(canvasScale, canvasScale);
game.g.canvasHold.height = game.g.canvasHold.width = height / game.g.fieldSize.y * 5;
game.g.canvasHold.style.transform = "translate(-100%, -.2em) translateX(-" + width / 2 + "px)";
const contextHoldScale = Math.floor(game.g.canvasHold.width / 6);
game.g.contextHold.scale(contextHoldScale, contextHoldScale);
game.g.canvasUpcoming.width = height / game.g.fieldSize.y * 5;
game.g.canvasUpcoming.height = game.g.canvasUpcoming.width * 3;
game.g.canvasUpcoming.style.transform = "translate(100%, -.2em) translateX(" + width / 2 + "px)";
const contextUpcomingScale = Math.floor(game.g.canvasUpcoming.width / 6);
game.g.contextUpcoming.scale(contextUpcomingScale, contextUpcomingScale);
if (!firstRun && game.g.isPaused) {
game.draw();
}
}
scaleWindow();
document.addEventListener("keydown", (event) => {
if(event.keyCode === 32) {
if(firstRun) {
document.addEventListener('keyup', (event) => {
console.log(event.key);
if (event.key === ' ') {
if (firstRun) {
initGame();
} else {
if (!game.g.isPaused) {
showMenu();
} else {
hideMenu();
}
escState = escState === 0 ? 1 : 0;
toggleMenu();
}
} else if(event.keyCode === 27) {
} else if (event.keyCode === 27) {
escState++;
if (firstRun && escState % 3 === 0) {
escState++;
@@ -78,15 +28,15 @@ document.addEventListener("keydown", (event) => {
}
});
document.getElementById("game-play").addEventListener("click", () => {
if(firstRun) {
document.getElementById('game-play').addEventListener('click', () => {
if (firstRun) {
initGame();
} else {
hideMenu();
}
});
document.getElementById("game-reset").addEventListener("click", () => {
document.getElementById('game-reset').addEventListener('click', () => {
firstRun = true;
game.clearScreen();
hideMenu();
@@ -94,17 +44,21 @@ document.getElementById("game-reset").addEventListener("click", () => {
showMenu();
});
document.getElementsByName("theme").forEach((el) => {
el.addEventListener("change", (e) => {
game.g.theme = e.target.getAttribute("data-theme");
game.redrawScreen();
document.getElementsByName('theme').forEach((el) => {
el.addEventListener('change', (e) => {
const themeName = e.target.getAttribute('data-theme');
const theme = themes[themeName];
manager.callAll(instance => {
instance.g.theme = theme;
instance.redrawScreen();
});
});
});
let isActive = false;
const menuButton = document.getElementById("menu-opener");
const menuButton = document.getElementById('menu-opener');
menuButton.addEventListener("click", () => {
menuButton.addEventListener('click', () => {
toggleSettings();
});
@@ -112,120 +66,109 @@ function toggleSettings() {
if (isActive) {
escState = 1;
menuButton.classList.remove('active');
document.getElementsByTagName('body')[0].classList.remove('menu-open');
document.body.classList.remove('menu-open');
} else {
escState = 2;
menuButton.classList.add('active');
document.getElementsByTagName('body')[0].classList.add('menu-open');
document.body.classList.add('menu-open');
}
isActive = !isActive;
}
function toggleMenu() {
if (escState === 0) {
document.getElementsByTagName("body")[0].classList.remove("menu-open");
document.body.classList.remove('menu-open');
menuButton.classList.remove('active');
hideMenu();
} else if (escState === 1) {
document.getElementsByTagName("body")[0].classList.remove("menu-open");
document.body.classList.remove('menu-open');
menuButton.classList.remove('active');
showMenu();
} else {
document.getElementsByTagName("body")[0].classList.add("menu-open");
document.body.classList.add('menu-open');
menuButton.classList.add('active');
}
}
function fadeBlurIn() {
const blurEl = document.getElementById("f1").children[0];
const blurEl = document.getElementById('f1').children[0];
const finalVal = 15;
let currentVal = 0;
const id = setInterval(frame, 16);
const interval = 1000 / 60;
const id = setInterval(frame, interval);
function frame() {
if(currentVal >= finalVal) {
if (currentVal >= finalVal) {
clearInterval(id);
} else {
currentVal += 1.6;
blurEl.setAttribute("stdDeviation", currentVal);
currentVal += 0.5;
blurEl.setAttribute('stdDeviation', currentVal);
}
}
setTimeout(() => {
if (currentVal < finalVal) {
blurEl.setAttribute("stdDeviation", finalVal);
blurEl.setAttribute('stdDeviation', finalVal);
clearInterval(id);
console.log("Performance Issues: system couldn't hold up");
console.log('Performance Issues: system couldn\'t hold up');
}
}, 1000);
}
function fadeBlurOut() {
const blurEl = document.getElementById("f1").children[0];
const blurEl = document.getElementById('f1').children[0];
const finalVal = 0;
let currentVal = 15;
const id = setInterval(frame, 16);
const interval = 1000 / 60;
const id = setInterval(frame, interval);
function frame() {
if(currentVal <= finalVal) {
if (currentVal <= finalVal) {
clearInterval(id);
} else {
currentVal -= 1.6;
blurEl.setAttribute("stdDeviation", currentVal);
currentVal -= 0.5;
blurEl.setAttribute('stdDeviation', currentVal);
}
}
setTimeout(() => {
blurEl.setAttribute("stdDeviation", finalVal);
blurEl.setAttribute('stdDeviation', finalVal);
clearInterval(id);
if (currentVal < finalVal) {
console.log("Performance Issues: system couldn't hold up");
console.log('Performance Issues: system couldn\'t hold up');
}
}, 1000);
}
const scoreEl = document.getElementById("score");
const nativeTransform = getComputedStyle(scoreEl).transform;
function scoreUpdateAni() {
scoreEl.classList.add("update");
setTimeout(() => {
scoreEl.classList.remove("update");
}, 1500);
}
function showMenu() {
game.g.isPaused = true;
manager.pause();
escState = 1;
document.getElementById("game-title").style.display = "block";
document.getElementById("game-play").style.display = "block";
document.getElementById("game-reset").style.display = "block";
document.getElementById('game-title').style.display = 'block';
document.getElementById('game-play').style.display = 'block';
document.getElementById('game-reset').style.display = 'block';
document.getElementById("game-title").style.opacity = "1";
document.getElementById("game-play").style.opacity = "1";
document.getElementById('game-title').style.opacity = '1';
document.getElementById('game-play').style.opacity = '1';
fadeBlurIn();
if(!firstRun) {
document.getElementById("game-reset").style.opacity = "1";
if (!firstRun) {
document.getElementById('game-reset').style.opacity = '1';
}
}
function hideMenu() {
game.g.isPaused = false;
manager.resume();
escState = 0;
document.getElementById("game-title").style.opacity = "0";
document.getElementById("game-play").style.opacity = "0";
document.getElementById("game-reset").style.opacity = "0";
document.getElementById('game-title').style.opacity = '0';
document.getElementById('game-play').style.opacity = '0';
document.getElementById('game-reset').style.opacity = '0';
setTimeout(() => {
document.getElementById("game-title").style.display = "none";
document.getElementById("game-play").style.display = "none";
document.getElementById("game-reset").style.display = "none";
document.getElementById('game-title').style.display = 'none';
document.getElementById('game-play').style.display = 'none';
document.getElementById('game-reset').style.display = 'none';
}, 500);
game.g.lastTimeUpdate = Date.now();
fadeBlurOut();
if(!firstRun) {
game.update(game.g.lastTime);
}
}
function initGame() {
@@ -234,4 +177,4 @@ function initGame() {
firstRun = false;
switchLang(currentLang);
}
}

View File

@@ -6,9 +6,8 @@ class Player {
this.pos = {x: 0, y: 0};
this.matrix = null;
this.score = 0;
this.isHolding = false;
this.holdingTile = null;
this.level = 1;
this.lastLevelScore = 0;
this.a.p = this;
}
@@ -29,19 +28,6 @@ class Player {
this.g.dropCounter = 0;
}
hold() {
if (this.isHolding)
return;
if (this.holdingTile === null) {
this.holdingTile = this.matrix;
this.reset(true);
} else {
this.holdingTile = [this.matrix, this.matrix = this.holdingTile][0];
this.reset(true, false);
}
this.game.drawHolding();
}
move(dir) {
this.pos.x += dir;
if (collide(this.a.field, this)) {
@@ -83,4 +69,4 @@ class Player {
}
}
}
}
}

56
js/tetris-manager.js Normal file
View File

@@ -0,0 +1,56 @@
class TetrisManager {
constructor() {
this.container = document.querySelector('.game-container');
this.instances = new Set;
}
createPlayer() {
const game = new Game();
this.instances.add(game);
}
init() {
this.callAll('rescale', []);
}
removePlayer(tetris) {
this.instances.delete(tetris);
}
start() {
this.instances.forEach(instance => {
instance.start();
});
this.init();
}
resume() {
this.instances.forEach(instance => {
instance.g.isPaused = false;
instance.g.lastTimeUpdate = Date.now();
if (!firstRun) {
instance.update(instance.g.lastTime);
}
});
}
pause() {
this.instances.forEach(instance => {
instance.g.isPaused = true;
});
}
callAll(method, args) {
if (typeof method === 'string') {
this.instances.forEach(instance => {
instance[method](...args);
});
} else if (typeof method === 'function') {
this.instances.forEach(instance => {
method(instance);
});
}
}
}

View File

@@ -25,7 +25,7 @@ function centerOffset(matrix) {
let offsetY = 0;
matrix.forEach((row, y) => {
let onlyZeroesY = true;
row.forEach((value, x) => {
row.forEach((value) => {
if (value > 0) {
onlyZeroesY = false;
}
@@ -39,7 +39,7 @@ function centerOffset(matrix) {
});
for (let x = 0; x < matrix[0].length; x++) {
let onlyZeroesX = true;
matrix.forEach((row, y) => {
matrix.forEach((row) => {
if (row[x] > 0)
onlyZeroesX = false;
});
@@ -165,44 +165,6 @@ function drawRoundRect(ctx, x, y, w, h, r) {
}
function drawReliefRect(ctx, x, y, w, h, l, clr) {
ctx.fillStyle = clr;
ctx.fillRect(x + l, y + l, w - (2 * l), h - (2 * l));
ctx.fillStyle = colorLuminance(clr, .6);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w - l, y + l);
ctx.lineTo(x + l, y + l);
ctx.fill();
ctx.closePath();
ctx.fillStyle = colorLuminance(clr, .3);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, y + h);
ctx.lineTo(x + l, y + h - l);
ctx.lineTo(x + l, y + l);
ctx.fill();
ctx.closePath();
ctx.fillStyle = colorLuminance(clr, -.6);
ctx.beginPath();
ctx.moveTo(x, y + h);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x + w - l, y + h - l);
ctx.lineTo(x + l, y + h - l);
ctx.fill();
ctx.closePath();
ctx.fillStyle = colorLuminance(clr, -.3);
ctx.beginPath();
ctx.moveTo(x + w, y);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x + w - l, y + h - l);
ctx.lineTo(x + w - l, y + l);
ctx.fill();
ctx.closePath();
}
function formatMillis(millis) {
@@ -279,10 +241,14 @@ function rotate(matrix, dir) {
}
}
const game = new Game();
const manager = new TetrisManager();
manager.createPlayer();
// const game = new Game();
function startGame() {
game.start();
// game.start();
manager.start();
}
/**
@@ -306,4 +272,4 @@ function colorLuminance(hex, lum) {
}
return rgb;
}
}

110
js/theme.js Normal file
View File

@@ -0,0 +1,110 @@
class Theme {
drawTile(color, matrix, ctx) {
}
}
class DefaultTheme extends Theme {
drawTile(color, matrix, ctx) {
ctx.fillStyle = color;
ctx.fillRect(tileGap / 2, tileGap / 2, 1 - tileGap, 1 - tileGap);
}
}
class CleanTheme extends Theme {
drawTile(color, matrix, ctx) {
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
}
}
class ModernTheme extends Theme {
drawTile(color, matrix, ctx) {
ctx.fillStyle = color;
drawRoundRect(ctx, tileGap / 2, tileGap / 2, 1 - tileGap, 1 - tileGap, .15);
}
}
class SnakesTheme extends Theme {
drawTile(color, matrix, ctx, x, y) {
let r1 = .15, // top right
r2 = .15, // bottom right
r3 = .15, // bottom left
r4 = .15; // top left
// Is there a tile to the left?
if (matrix[y][x - 1] > 0) {
r3 = 0;
r4 = 0;
}
// Is there a tile to the right?
if (matrix[y][x + 1] > 0) {
r1 = 0;
r2 = 0;
}
// Is there a tile to the top?
if (matrix[y - 1] !== undefined && matrix[y - 1][x] > 0) {
r1 = 0;
r4 = 0;
}
// Is there a tile to the bottom?
if (matrix[y + 1] !== undefined && matrix[y + 1][x] > 0) {
r2 = 0;
r3 = 0;
}
drawRoundRect(ctx, x, y, 1, 1, [r1, r2, r3, r4]);
}
}
class RetroTheme extends Theme {
static drawReliefRect(ctx, x, y, width, height, elevation, color) {
ctx.fillStyle = color;
ctx.fillRect(x + elevation, y + elevation, width - (2 * elevation), height - (2 * elevation));
ctx.fillStyle = colorLuminance(color, .6);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.lineTo(x + width - elevation, y + elevation);
ctx.lineTo(x + elevation, y + elevation);
ctx.fill();
ctx.closePath();
ctx.fillStyle = colorLuminance(color, .3);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, y + height);
ctx.lineTo(x + elevation, y + height - elevation);
ctx.lineTo(x + elevation, y + elevation);
ctx.fill();
ctx.closePath();
ctx.fillStyle = colorLuminance(color, -.6);
ctx.beginPath();
ctx.moveTo(x, y + height);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x + width - elevation, y + height - elevation);
ctx.lineTo(x + elevation, y + height - elevation);
ctx.fill();
ctx.closePath();
ctx.fillStyle = colorLuminance(color, -.3);
ctx.beginPath();
ctx.moveTo(x + width, y);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x + width - elevation, y + height - elevation);
ctx.lineTo(x + width - elevation, y + elevation);
ctx.fill();
ctx.closePath();
}
drawTile(color, matrix, ctx) {
RetroTheme.drawReliefRect(ctx, 0, 0, 1, 1, .15, color);
}
}
const themes = {
default: new DefaultTheme(),
clean: new CleanTheme(),
modern: new ModernTheme(),
snakes: new SnakesTheme(),
retro: new RetroTheme()
};