javascripthtmlcanvashtml5-canvastetris

How to find where the tetris block (Tetromino) land on the gird and Show it as preview?


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.

Something like this

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);

    }
}

Solution

  • 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.