Creando un Tetris con JavaScript IV: canvas
Tags: spanish, gamedev, javascript, tutorial
Published: 2024-07-10 11:19:20

---
title: Creando un Tetris con JavaScript IV: canvas
published: true
series: JavaScript Tetris
description: Insertrix: un tetris ligeramente distinto.
tags: #spanish #gamedev #javascript #tutorial
# cover_image: https://upload.wikimedia.org/wikipedia/commons/4/46/Tetris_logo.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-07-10 09:39 +0000
---

## 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**](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial) de HTML.

```javascript
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...
}
```

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.

```javascript
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 );
    }
}
```

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](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API).

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

```javascript
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;
    }
}
```

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.

```javascript
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;
    }
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6xita7256wj9pq9c1eu.png)

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.