DEV Community

Creando un Tetris con JavaScript IV: canvas

Introducción

En esta nueva entrega de la serie, veremos cómo mostrar el tablero y la pieza que está bajando en ese momento en pantalla. Para ello, tendremos que dibujarlo en el navegador, y la opción que tenemos para hacerlo es el elemento Canvas de HTML.

class Canvas {
    static SEPARATION = 2;
    #_painting = false;
    #_element = null;
    #_board = null;
    #_piece = null;

    constructor(element, board)
    {        
        element.width = 5 + ( board.cols * Board.PIXEL_SIZE );
        element.height = 5 + ( board.rows * Board.PIXEL_SIZE );

        this._board = board;
        this._element = element;
    }

    // más cosas...
}
Enter fullscreen mode Exit fullscreen mode

Esta clase Canvas representa el elemento de mismo nombre de HTML, que se le pasa como parámetro en el constructor. Dado que va a dibujar el tablero, también se le pasa como parámetro, para poder acceder a los puntos a dibujar.

Lo primero que hace es dimensionar el elemento Canvas para poder acomodar el tablero, según las dimensiones que el mismo tablero reporta a través de sus propiedades cols y rows. El tablero nos dice también cuántos pixels forma un punto de cada pieza o cada celda del tablero, a través de PIXEL_SIZE.

Redibujando el juego

Dejémonos de rodeos. Tenemos que pintar el tablero y la pieza que en ese momento desciende, ¿no? Pues vamos al lío.

class Canvas {
    // más cosas...

    paint()
    {
        if ( this._painting ) {
            return;
        }

        const ctx = this.element.getContext( "2d" );
        const SEP = Canvas.SEPARATION;

        this._painting = true;
        this.clear();

        this.paintBoard( ctx, SEP );
        this.paintPiece( ctx, SEP );

        this._painting = false;
    }

    clear()
    {
        const ctx = this.element.getContext( "2d" );

        ctx.clearRect( 0, 0, this.element.width, this.element.height );
    }
}
Enter fullscreen mode Exit fullscreen mode

Primero tomamos el contexto para 2D, que nos permitirá dibujar en el canvas. Por curiosidad, existe también un contexto para 3D, que se basa en WebGL.

Tenemos unas guardas (_painting), que evitan que varios threads estén ejecutando el método a la vez (en distintos puntos), en un momento dado. Esto podría suceder si el método fuera ejecutado durante más tiempo que el tiempo entre redibujado. Aunque bueno, en ese caso tendríamos otros muchos problemas...

El siguiente paso es borrar lo que haya en la pantalla en el anterior redibujado (frame). Esto lo hacemos con el método clear(), que utiliza clearRect() para eliminar la imagen en el canvas.

Y después pintamos el tablero, y entonces la pieza que desciende en ese momento. Pues ya estaría. Ale, entrega finalizada.

Que noooo. Vamos a ir viendo cómo se pinta el tablero y la pieza. Lo primero es pintar el tablero. SEP es la separación que dejaremos entre las piezas y el recuadro del tablero. Este recuadro es lo primero que pintamos en el párrafo de código titulado Draw frame. Se trata de un simple rectángulo que se puede dibujar con strokeRect(), que acepta cuatro parámetros con la posición del vértice superior izquierdo, y a continuación el ancho y alto del mismo.

Pintando el tablero

class Canvas {
    // más cosas...

    paintBoard(ctx, SEP)
    {       
        // Draw frame
        ctx.strokeWidth = 1;
        ctx.strokeStyle = this.board.color;
        ctx.strokeRect( 1, 1,
                  this.element.width - 1,
                  this.element.height  -1 );

        // Draw board
        for(let numRow = 0; numRow < this.board.rows; ++numRow)
        {
            const row = this.board.getRow( numRow );

            for(let numCol = 0; numCol < row.length; ++numCol) {
                if ( Boolean( row[ numCol ] ) ) {
                    ctx.strokeWidth = 1;
                    ctx.strokeStyle = this.board.color;
                    ctx.fillStyle = this.board.color;
                    ctx.fillRect(
                        SEP + ( Board.PIXEL_SIZE * numCol ),
                        SEP + ( Board.PIXEL_SIZE * numRow ),
                        Board.PIXEL_SIZE,
                        Board.PIXEL_SIZE );
                }
            }            
        }

        return;
    }
}
Enter fullscreen mode Exit fullscreen mode

Después viene un bucle anidado (filas y columnas), de manera que veremos cuáles de las celdas del tablero tienen contenido (un entero 1, frente a un entero 0), y entonces dibujar un pequeño cuadrado de lado PIXEL_SIZE.

Así, el primer bucle recorre las filas hasta Board.rows. Obtenemos entonces la fila completa con el método getRow(), para recorrerla con el bucle interior, hasta Board.cols.

Así, dada un celda en la fila/columna f/c, Board.getCell(f, c), y teniendo en cuenta que en JavaScript tiene un constructor para Boolean que acepta un entero que ante cualquier valor excepto el 0, significa true, pintamos un cuadrado de lado PIXEL_SIZE. Entonces, para saber dónde pintar la fila f, tenemos que multiplicar por PIXEL_SIZE y sumarle la separación entre el recuadro del tablero y la primera celda. Como son cuadrados, encontraremos la columna c de la misma forma: SEP + (c * PIXEL_SIZE).

Pintando la pieza

Hacemos algo parecido con las piezas. Al tener una forma (shape), que no es más que una matriz, tendremos de nuevo dos bucles, el exterior para filas y el interior para columnas.

class Canvas {
    // más cosas...

    paintPiece(ctx, SEP)
    {
        const SHAPE = this.piece.shape;

        for(let numRow = 0; numRow < SHAPE.length; ++numRow) {
            const ROW = SHAPE[ numRow ];
            for(let numCol = 0; numCol < ROW.length; ++numCol) {
                if ( Boolean( ROW[ numCol ] ) ) {
                    ctx.strokeWidth = 1;
                    ctx.strokeStyle = this.piece.color;
                    ctx.fillStyle = this.piece.color;
                    ctx.fillRect(
                        SEP
                        + ( this.piece.col * Board.PIXEL_SIZE )
                        + ( numCol * Board.PIXEL_SIZE ),
                        SEP + 
                        + ( this.piece.row * Board.PIXEL_SIZE )
                        + ( numRow * Board.PIXEL_SIZE ),
                        Board.PIXEL_SIZE,
                        Board.PIXEL_SIZE );
                }
            }
        }

        return;
    }
}
Enter fullscreen mode Exit fullscreen mode

De nuevo, si nos encontramos un 1, pintaremos un cuadrado de lado PIXEL_SIZE. La posición para pintar cada cuadrado que conforma la pieza nos la da la propia posición de la pieza fila/columna (Piece.row/Piece.col). Hay que multiplicar esto por PIXEL_SIZE y sumarle la separación con el recuadro.

El juego Insertrix en su estado actual

En este momento, lo que podremos ver es bastante... soso. El tablero está vacío, y no tenemos un bucle de juego, por lo que las piezas ni siquiera bajan. Trataremos ese tema en la próxima entrega, de forma que podamos empezar a ver ya algo parecido a la imagen de arriba.

Top comments (0)