I am learning HTML canvas and choose to make Tetris game as the first project I have completed the game by watching a few tutorials. But want to make it better by adding a live preview where the Tetromino land on the page can anyone help how can I do that? By preview I mean How can I show the player where the piece land on gameArea.
The Html File:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tetris Game</title>
<link rel="stylesheet" href="./Game Assets/Style/style.css">
<link rel="shortcut icon" href="./Game Assets/img/tetris.png" type="image/x-icon">
</head>
<body body>
<div id="wrapper">
<div id="GameArea">
<canvas id="gameCanvas" width="300" height="600">
</canvas>
<div id="SLN">
<div id="SCORE">
<h3>Score:</h3>
<div id="sc">0</div>
</div>
<div id="Level">
</div>
<div id="NEXT">
<h3>Next:</h3>
<canvas id="preCanvas" width="120" height="120"></canvas>
</div>
</div>
</div>
</div>
<script src="./Game Assets/Script/index.js"></script>
</body>
</html>
Here is Java Script File
const canvas = document.getElementById("gameCanvas"); //Get The Canvas Element
const ctx = canvas.getContext("2d");
const preview = document.getElementById("preCanvas");
const pre = preview.getContext("2d");
//Tetrominoes Code
const I = [
[
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
],
[
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
[
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
],
[
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
]
];
const J = [
[
[1, 0, 0],
[1, 1, 1],
[0, 0, 0]
],
[
[0, 1, 1],
[0, 1, 0],
[0, 1, 0]
],
[
[0, 0, 0],
[1, 1, 1],
[0, 0, 1]
],
[
[0, 1, 0],
[0, 1, 0],
[1, 1, 0]
]
];
const L = [
[
[0, 0, 1],
[1, 1, 1],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 0],
[0, 1, 1]
],
[
[0, 0, 0],
[1, 1, 1],
[1, 0, 0]
],
[
[1, 1, 0],
[0, 1, 0],
[0, 1, 0]
]
];
const O = [
[
[0, 0, 0, 0],
[0, 1, 1, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
]
];
const S = [
[
[0, 1, 1],
[1, 1, 0],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 1],
[0, 0, 1]
],
[
[0, 0, 0],
[0, 1, 1],
[1, 1, 0]
],
[
[1, 0, 0],
[1, 1, 0],
[0, 1, 0]
]
];
const T = [
[
[0, 1, 0],
[1, 1, 1],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 1],
[0, 1, 0]
],
[
[0, 0, 0],
[1, 1, 1],
[0, 1, 0]
],
[
[0, 1, 0],
[1, 1, 0],
[0, 1, 0]
]
];
const Z = [
[
[1, 1, 0],
[0, 1, 1],
[0, 0, 0]
],
[
[0, 0, 1],
[0, 1, 1],
[0, 1, 0]
],
[
[0, 0, 0],
[1, 1, 0],
[0, 1, 1]
],
[
[0, 1, 0],
[1, 1, 0],
[1, 0, 0]
]
];
// we first need too define ROW and columns constants
const ROW = 20;
const COL = 10;
// Const for square size
const SQ = 30;
function drawSquare(x, y, color, stroke, T) {
if (stroke === "piece") {
ctx.fillStyle = color;
ctx.fillRect(x * SQ, y * SQ, SQ, SQ);
ctx.strokeStyle = "black";;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.strokeRect(x * SQ + 1, y * SQ + 1, SQ - 2, SQ - 2);
ctx.strokeRect(x * SQ + 2, y * SQ + 2, SQ - 4, SQ - 4);
// ctx.strokeRect(x * SQ + 2.5, y * SQ + 2.5, SQ - 5, SQ - 5);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.strokeRect(x * SQ + 6.5, y * SQ + 6.5, SQ - 13, SQ - 13);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x * SQ + 2, y * SQ + 2)
ctx.lineTo(x * SQ + 6.5, y * SQ + 6.5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x * SQ + SQ - 2, y * SQ + 2)
ctx.lineTo(x * SQ + SQ - 6.5, y * SQ + 6.5);
ctx.stroke();
// Downn
ctx.beginPath();
ctx.moveTo(x * SQ + 2, y * SQ - 2 + SQ)
ctx.lineTo(x * SQ + 6.5, y * SQ - 6.5 + SQ);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x * SQ + SQ - 2, y * SQ - 2 + SQ)
ctx.lineTo(x * SQ - 6.5 + SQ, y * SQ - 6.5 + SQ);
ctx.stroke();
} else {
ctx.fillStyle = color;
ctx.fillRect(x * SQ, y * SQ, SQ, SQ);
}
};
// a VACANT (empty) square has this color.
// const VACANT = "rgba(19, 18, 18, 0.719)";
const VACANT = "transparent";
// now we define the board array.
let board = [];
// let's create the rows.
for (let r = 0; r < ROW; r++) {
board[r] = [];
// let's create the columns
for (let c = 0; c < COL; c++) {
board[r][c] = VACANT;
// when we first draw the board all the square are empty, so every square has the value "VACANT".
}
}
function drawBoard() {
for (r = 0; r < ROW; r++) {
for (c = 0; c < COL; c++) {
if (board[r][c] != VACANT) {
drawSquare(c, r, board[r][c], "piece")
} else {
drawSquare(c, r, board[r][c], "board");
}
}
}
}
drawBoard()
//Piece and there color
let Score = 0;
const PIECES = [
[Z, "blue"],
[S, "rgb(238, 132, 46)"],
[T, "rgb(248, 232, 232)"],
[O, "yellow"],
[L, "rgb(245, 94, 144)"],
[I, "purple"],
[J, "rgb(121, 236, 240)"]
]
// Generator random Piece
function NewPiece(canvas) {
let r = randomN = Math.floor(Math.random() * PIECES.length)
return new Piece(PIECES[r][0], PIECES[r][1])
}
function nextPiece() {
r = Math.floor(Math.random() * PIECES.length)
previewPiece = new PreviewPiece(PIECES[r][0], PIECES[r][1]);
xd = r
previewPiece.clear();
previewPiece.draw();
return xd
}
x = nextPiece()
let p = new Piece(PIECES[x][0], PIECES[x][1]);
function Piece(Tetromino, color) {
this.tetromino = Tetromino;
this.color = color;
this.tetrominoN = 0;
this.activeTetromino = this.tetromino[this.tetrominoN];
this.x = 3;
this.y = -2;
// Piece.prototype.draw
this.draw = function() {
for (r = 0; r < this.activeTetromino.length; r++) {
for (c = 0; c < this.activeTetromino.length; c++) {
if (this.activeTetromino[r][c]) {
drawSquare(this.x + c, this.y + r, this.color, "piece");
}
}
}
}
// Piece.prototype.Clear
this.update = function() {
for (r = 0; r < this.activeTetromino.length; r++) {
for (c = 0; c < this.activeTetromino.length; c++) {
if (this.activeTetromino[r][c]) {
update();
}
}
}
}
// Piece.prototype.moveDown
this.moveDown = function() {
if (!this.collide(0, 1, this.activeTetromino)) {
this.update();
this.y++;
this.draw();
// ctx.shadowOffsetY -= SQ
} else {
//generate new Piece
this.lockPiece();
p = new Piece(PIECES[x][0], PIECES[x][1]);
}
if (this.y === -1) {
// ctx.shadowOffsetY -= 1 * SQ
x = nextPiece();
}
}
// Piece.prototype.moveRight
this.moveRight = function() {
if (!this.collide(1, 0, this.activeTetromino)) {
this.update();
this.x++;
this.draw();
}
}
// Piece.prototype.moveLeft
this.moveLeft = function() {
if (!this.collide(-1, 0, this.activeTetromino)) {
this.update();
this.x--;
this.draw();
}
}
// Piece.prototype.rotate
this.rotate = function() {
let nextPat = this.tetromino[(this.tetrominoN + 1) % this.tetromino.length];
let kick = 0;
if (this.collide(0, 0, nextPat)) {
if (this.x > COL / 2) {
kick = -1;
} else {
kick = 1;
}
}
if (!this.collide(kick, 0, nextPat)) {
this.update();
this.x += kick;
this.tetrominoN = (this.tetrominoN + 1) % this.tetromino.length;
this.activeTetromino = this.tetromino[this.tetrominoN];
this.draw();
}
}
// Piece.prototype.collide
this.collide = function(x, y, piece) {
for (r = 0; r < piece.length; r++) {
for (c = 0; c < piece.length; c++) {
//Empty square skip
if (!piece[r][c]) {
continue;
}
let newX = this.x + c + x;
let newY = this.y + r + y;
if (newX < 0 || newX > COL || newY >= ROW) {
return true;
}
if (newY < 0) {
continue;
}
if (board[newY][newX] != VACANT) { return true; }
}
}
}
this.lockPiece = function() {
for (r = 0; r < this.activeTetromino.length; r++) {
for (c = 0; c < this.activeTetromino.length; c++) {
//skip empty block
if (!this.activeTetromino[r][c]) {
continue;
}
//piece to lock if reaches the top
if (this.y + r < 0) {
//Game over
// alert("Game Over");
//Stop Game
gameOver = true;
if (gameOver) {
// document.getElementById("gameCanvas").style.display = "none"
}
break;
}
//we lock piece when it reaches bottom
board[this.y + r][this.x + c] = this.color
}
}
// remove full rows
for (r = 0; r < ROW; r++) {
let isRowFull = true;
for (c = 0; c < COL; c++) {
isRowFull = isRowFull && (board[r][c] != VACANT);
}
if (isRowFull) {
for (y = r; y > 1; y--) {
for (c = 0; c < COL; c++) {
// ctx.clearRect(y * SQ, c * SQ, SQ, SQ);
board[y][c] = board[y - 1][c];
}
}
//the top row board[0][...] has no row above it
for (c = 0; c < COL; c++) {
board[0][c] = VACANT;
// ctx.clearRect(r * SQ, c * SQ, SQ, SQ);
}
//Increase Score
Score += 100;
console.log(Score)
}
// if (this.x > 0) {
// // x = nextPiece();
// }
}
update();
}
}
function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawBoard();
}
let dropStart = Date.now();
let gameOver = false;
let speed = 1000;
let PrevScore = 0;
function drop() {
let now = Date.now();
let delta = now - dropStart;
if (delta > speed) {
// p.x = 3;
// p.y = -1
p.moveDown()
dropStart = Date.now();
}
if (!gameOver) {
requestAnimationFrame(drop)
}
document.getElementById("sc").innerHTML = Score;
if (Score - PrevScore > 1000) {
speed -= 10;
PrevScore = Score;
}
}
drop()
document.addEventListener("keydown", CONTROL);
function CONTROL(event) {
if (event.keyCode == 37) {
if (!gameOver) {
event.preventDefault();
p.moveLeft();
// dropStart = Date.now();
}
} else if (event.keyCode == 38) {
if (!gameOver) {
event.preventDefault();
p.rotate();
// dropStart = Date.now();
}
} else if (event.keyCode == 39) {
if (!gameOver) {
event.preventDefault();
p.moveRight()
// dropStart = Date.now();
}
} else if (event.keyCode == 40) {
if (!gameOver) {
event.preventDefault();
Score++;
p.moveDown();
}
}
}
document.getElementById("gameCanvas").addEventListener("click", function(e) {
e.preventDefault();
p.rotate();
dropStart = Date.now();
})
let TouchX, TouchY, MoveX = 0,
MoveY = 0,
XDiff, YDiff;
document.getElementById("GameArea").addEventListener("touchstart", function(e) {
TouchX = e.touches[0].clientX
TouchY = e.touches[0].clientY
// console.log("Tx: ", TouchX, "Ty: ", TouchY)
document.getElementById("GameArea").addEventListener('touchmove', function(e) {
e.preventDefault() // prevent scrolling when inside DIV
XDiff = TouchX - MoveX;
YDiff = TouchY - MoveY;
if (Math.abs(e.touches[0].clientX - MoveX) > 15) {
if (Math.abs(XDiff) > Math.abs(YDiff)) {
if (XDiff < 0) {
//Right Swipe;
p.moveRight();
} else {
// Left Swipe
p.moveLeft();
}
}
} else if (Math.abs(e.touches[0].clientY - MoveY) > 0) {
if (Math.abs(XDiff) < Math.abs(YDiff)) {
if (XDiff < 0) {
//Down Swipe;
Score++;
p.moveDown();
}
}
}
MoveX = e.touches[0].clientX;
MoveY = e.touches[0].clientY;
// console.log("x: ", e.touches[0].clientX, "y: ", e.touches[0].clientY);
}, { passive: false })
}, false);
function drawSquarePreview(x, y, color) {
pre.fillStyle = color;
pre.fillRect(x * SQ, y * SQ, SQ, SQ);
pre.strokeStyle = "black";;
pre.lineWidth = 1;
pre.beginPath();
pre.strokeRect(x * SQ + 1, y * SQ + 1, SQ - 2, SQ - 2);
pre.strokeRect(x * SQ + 2, y * SQ + 2, SQ - 4, SQ - 4);
// pre.strokeRect(x * SQ + 2.5, y * SQ + 2.5, SQ - 5, SQ - 5);
pre.lineWidth = 1;
pre.beginPath();
pre.strokeRect(x * SQ + 6.5, y * SQ + 6.5, SQ - 13, SQ - 13);
pre.lineWidth = 1;
pre.beginPath();
pre.moveTo(x * SQ + 2, y * SQ + 2)
pre.lineTo(x * SQ + 6.5, y * SQ + 6.5);
pre.stroke();
pre.beginPath();
pre.moveTo(x * SQ + SQ - 2, y * SQ + 2)
pre.lineTo(x * SQ + SQ - 6.5, y * SQ + 6.5);
pre.stroke();
// Down
pre.beginPath();
pre.moveTo(x * SQ + 2, y * SQ - 2 + SQ)
pre.lineTo(x * SQ + 6.5, y * SQ - 6.5 + SQ);
pre.stroke();
pre.beginPath();
pre.moveTo(x * SQ + SQ - 2, y * SQ - 2 + SQ)
pre.lineTo(x * SQ - 6.5 + SQ, y * SQ - 6.5 + SQ);
}
function PreviewPiece(Tetromino, color) {
this.tetromino = Tetromino;
this.color = color;
this.tetrominoN = 0;
this.activeTetromino = this.tetromino[this.tetrominoN];
this.x = 0;
this.y = 0;
this.draw = function() {
for (r = 0; r < this.activeTetromino.length; r++) {
for (c = 0; c < this.activeTetromino.length; c++) {
if (this.activeTetromino[r][c]) {
drawSquarePreview(this.x + c, this.y + r, this.color);
}
}
}
}
this.clear = function() {
pre.clearRect(0, 0, preview.width, preview.height);
}
}
It is funny to answer my own question but, I found my answer in my dreams so let me put it here so if anyone wants to know the answer get helped Because,
That's what the community do.
so let's not waste any more time.
check how the game looks after the edit
SO first I created a new prototype function called shadow then check for where the tetromino collides using a for-loop return the for loop instance(where the Tetromino collides) minus 1. because we want to draw the shadow 1 step before the collision like this
this.shadow = function() {
for (i = 1; i < ROW; i++) {
if (this.collide(0, i, this.activeTetromino)) {
return i - 1 //i is the point of collision therefore we draw piece one step before collision
} else { continue; }
}
}
then, I updated my draw function something like this
this.draw = function() {
let sha = this.shadow();
for (r = 0; r < this.activeTetromino.length; r++) {
for (c = 0; c < this.activeTetromino.length; c++) {
if (this.activeTetromino[r][c]) {
drawSquare(this.x + c, this.y + r, this.color, "piece");
// this will also work without the if condition
if (this.y + r !== this.y + r + sha) {
drawSquare(this.x + c, this.y + sha + r, this.color, "piece", "shadow")
}
}
}
}
}
in this function, I stored the value returned from the shadow function in a variable called sha
and then draw the tetromino piece changing y to this.y + sha + r
and passing an extra parameter to draw shadow & you guessed it right there is an update in drawSquare function to draw square a slite different so the player can figure out which piece is shadow and which is the actual piece. here you can do anything to make it different like giving it only grey color or only drawing the stroke boundary or changing the globalApha value so I choose to change the global alpha value like this
function drawSquare(x, y, color, stroke, T) {
ctx.globalAlpha = 1;
if (stroke === "piece") {
if (T === "shadow") { ctx.globalAlpha = 0.4; }
ctx.fillStyle = color;
ctx.fillRect(x * SQ, y * SQ, SQ, SQ);
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
if (T === "shadow") { ctx.lineWidth = 3; };
ctx.beginPath();
ctx.strokeRect(x * SQ + 1, y * SQ + 1, SQ - 2, SQ - 2);
ctx.strokeRect(x * SQ + 2, y * SQ + 2, SQ - 4, SQ - 4);
// ctx.strokeRect(x * SQ + 2.5, y * SQ + 2.5, SQ - 5, SQ - 5);
ctx.lineWidth = 1;
if (T === "shadow") { ctx.lineWidth = 2; };
ctx.beginPath();
ctx.strokeRect(x * SQ + 6.5, y * SQ + 6.5, SQ - 13, SQ - 13);
ctx.lineWidth = 2;
// if (T === "shadow") { ctx.lineWidth = 5; };
ctx.beginPath();
ctx.moveTo(x * SQ + 2, y * SQ + 2)
ctx.lineTo(x * SQ + 6.5, y * SQ + 6.5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x * SQ + SQ - 2, y * SQ + 2)
ctx.lineTo(x * SQ + SQ - 6.5, y * SQ + 6.5);
ctx.stroke();
// Downn
ctx.beginPath();
ctx.moveTo(x * SQ + 2, y * SQ - 2 + SQ)
ctx.lineTo(x * SQ + 6.5, y * SQ - 6.5 + SQ);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x * SQ + SQ - 2, y * SQ - 2 + SQ)
ctx.lineTo(x * SQ - 6.5 + SQ, y * SQ - 6.5 + SQ);
ctx.stroke();
}
}
hope this helps you if you were looking for this. If you have a better way to do this for optimization feel free to answer. I am not a good explainer and this is my question as well as an answer on this form so plz forgive any grammatical.