Dibujando pixeles en el navegador con Canvas... Y cosas extra
Tags: canvas, webdev, javascript, typescript
Published: 2020-02-23 18:15:50

## Objetivo

Crear una aplicación web para dibujar “pixel art” usando las últimas tecnologías disponibles para navegadores modernos.

### Demo
https://codepen.io/UnJavaScripter/pen/BaNpBae

El HTML básico se ve así:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pixel Paint</title>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script src="dist/app.js"></script>
</body>
</html>
```

El archivo `app.js` está dentro de la carpeta `dist` porque usaré TypeScript y definí este como el destino para los archivos _transpilados_ (convertidos de vuelta a el JS de toda la vida).

Para instalar TypeScrip podemos usar NPM:

`npm i -g typescript`


Para crear un nuevo archivo de configuración de TypeScript usamos:
`tsc --init`

Dentro del archivo `tsconfig.json` que se acaba de crear, vamos a "descomentar" la propiedad `"outDir"` y le ponemos como valor "./dist" (la que definí al llamar el script en mi HTML), si quieres, si no, cualquier otro está bien. "Descomentamos" también la propiedad `rootDir` y le ponemos como valor cualquier nombre de carpeta que se nos ocurra, por ejemplo `src` ¯\_(ツ)_/¯.

Un par de cosas más, la propiedad `target` de `tsconfig.json` debe tener como valor al menos `es2015`, con esta configuración, el compilador nos habilitará el uso de funcionalidades "modernas" (¿de hace 5 años?). Así mismo, `module` debe ser igual a `es2015`.

¡Ahora sí podemos crear la carpeta `src` y dentro de ella nuestro archivo `app.ts`!

En una terminal vamos a poner a correr:
`tsc -w`

Para que el compilador de TypeScript esté pendiente de cualquier cambio y automáticamente genere archivos con extensión `js` en la carpeta que definimos como "outDir".

### Ahora sí a programar

Creamos una _clase_ porque queremos practicar como es eso de las [clases en JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) que ya se pueden usar desde ES6 (año 2015):

```ts
class PixelPaint {
  canvasElem: HTMLCanvasElement;
  ctx: CanvasRenderingContext2D;

  constructor() {
    this.canvasElem = <HTMLCanvasElement>document.getElementById('canvas');
    this.ctx = <CanvasRenderingContext2D>this.canvasElem.getContext('2d');
  }
}
```

Los _<* tipo *>_ son para decirle a TypeScript "no me creas pendejo, no va a ser _null_. TU relajate y compila".

Ya tenemos nuestro [context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D), ahora podemos empezar a dibujar en el _canvas_.

### Grilla/Cuadricula

Empecemos por definir el tamaño del canvas y del los _pixeles_ que vamos a usar:

```ts
class PixelPaint {
  canvasElem: HTMLCanvasElement;
  ctx: CanvasRenderingContext2D;
  pixelSize: number;

  constructor() {
    this.canvasElem = <HTMLCanvasElement>document.getElementById('canvas');
    this.ctx = <CanvasRenderingContext2D>this.canvasElem.getContext('2d');
    this.canvasElem.width = window.innerWidth;
    this.canvasElem.height = window.innerHeight;
    this.pixelSize: 50; // <-- Idealmente sería dinámico
  }
}
```

Estamos usando el tamaño del [viewport](https://developer.mozilla.org/en-US/docs/Glossary/layout_viewport) como el tamaño total del canvas, y 50 como un número cualquiera para el tamaño de cada pixel.

Ahora podemos crear la función que generará _la grilla_:

```ts
private drawGrid() {
  this.ctx.fillStyle = '#666'
  this.ctx.fillRect(0, 0, this.canvasElem.width, this.canvasElem.height);

  this.ctx.strokeStyle = '#777';
  this.ctx.beginPath();
  for (let i = 0; i <= this.canvasElem.width; i += this.pixelSize) {
    this.ctx.moveTo(i, 0);
    this.ctx.lineTo(i, this.canvasElem.height);
  }
  for (let i = 0; i <= this.canvasElem.height; i += this.pixelSize) {
    this.ctx.moveTo(0, i);
    this.ctx.lineTo(this.canvasElem.width, i);
  }
  this.ctx.stroke();
}
```
> (Color de fondo _666_ porque somos rebeldes)

Con `fillRect` le decimos que vaya al punto `0,0`, que sería la esquina superior izquierda del canvas, y que desde allí dibuje un cuadrado con el tamaño del canvas; efectivamente pintando el canvas del color definido en el `fillStyle`.

A continuación, con `strokeStyle` declaramos el color de los trazos que vienen en seguida y después iniciamos un path. El path dentro de cada `for` se va moviendo dependiendo del tamaño del pixel y pone el _lápiz_ en la posición inicial con `moveTo`. En este momento no estamos dibujando, sólo movemos el _lápiz_ a donde debe iniciar el trazado que realizará el `lineTo`. EL `stroke` al final hace que se apliquen los trazos.

Si seguiste los pasos, ya deberías ver _la grilla_ en tu navegador. ¿No? bueno, será porque no has llamado a la función `drawGrid` en el `constructor`:

```ts
constructor() {
  // ...
  this.drawGrid();
}
```

¿Todavía nada? Ha de ser porque no has instanciado la clase... Prueba instanciándola en alguna parte, el final del archivo `app.ts` es una opción:


```ts
new PixelPaint();
```

### Pintar

Ya tenemos _el lienzo_ listo, ahora sí podemos pintar en él, para ello vamos a agregar eventos al canvas para capturar los eventos que se disparen cuando el usuario interactúe con él. Entonces vamos a usar `jQuery` y... NO. Vamos a usar JavaScript, como se debe:

```ts
constructor {
  // ...
  this.canvasElem.addEventListener('click', (event: MouseEvent) => {
      this.handleClick(event);
  });
}

handleClick(event: MouseEvent) {
  this.handlePaint(event.x, event.y);
}

handlePaint(x: number, y: number) {
  const pixelXstart = (x - (x % this.pixelSize)) / this.pixelSize;
  const pixelYstart = (y - (y % this.pixelSize)) / this.pixelSize;
  this.drawPixel(pixelXstart, pixelYstart);
}
```

Nada extraño hasta ahora, sólo que no estamos ejecutando la acción de _pintar_ desde el callback del evento de clic, estamos delegando esta funcionalidad a `drawPixel`:

```ts
private drawPixel(x: number, y: number, color = "#CACA00") {
  const pixelXstart = x - (x % this.pixelSize);
  const pixelYstart = y - (y % this.pixelSize);

  this.ctx.fillStyle = color;
  this.ctx.fillRect(x * this.pixelSize, y * this.pixelSize, this.pixelSize, this.pixelSize);
}
```

La función es privada porque somos no queremos que quién implemente la clase `PixelPaint` tenga acceso a este método directamente. Nuestra clase, nuestras reglas.

Definimos un valor por defecto para el color del pixel. Por ahora sólo nos preocuparemos por pintar algo, más adelante veremos que inventamos para usar diferentes colores.

`pixelXstart` y `pixelYstart` buscan el punto de origen de la posición del evento de click y determinan a qué pixel corresponden usando el [_módulo_](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#Remainder). Esta es la operación matemática más compleja en esta aplicación. Con base en esto sabemos cuál es el punto de origen del pixel (esquina superior izquierda) y desde allí dibujamos un cuadrado con `fillRect` del tamaño de `pixelSize`.

Ahora sí cuando hacemos click en un cuadrado de la grilla veremos que se pinta de color `CACA`00.

Ya puedes arreglar ese margen horrible que tiene el `body` por defecto.

### Quiero copiar y pegar

Entendible, me pasa igual. Aquí está:

```ts
class PixelPaint {
  canvasElem: HTMLCanvasElement;
  ctx: CanvasRenderingContext2D;
  pixelSize: number;

  constructor() {
    this.canvasElem = <HTMLCanvasElement>document.getElementById('canvas');
    this.ctx = <CanvasRenderingContext2D>this.canvasElem.getContext('2d');
    this.canvasElem.width = window.innerWidth;
    this.canvasElem.height = window.innerHeight;
    this.pixelSize = 50;
    this.drawGrid();

    this.canvasElem.addEventListener('click', (event: MouseEvent) => {
      this.handleClick(event);
    });
  }

  handleClick(event: MouseEvent) {
    this.drawPixel(event.x, event.y);
  }

  private drawPixel(x: number, y: number, color = "#CACA00") {
    const pixelXstart = x - (x % this.pixelSize);
    const pixelYstart = y - (y % this.pixelSize);

    this.ctx.fillStyle = color;
    this.ctx.fillRect(pixelXstart, pixelYstart, this.pixelSize, this.pixelSize);
  }

  private drawGrid() {
    this.ctx.fillStyle = '#666'
    this.ctx.fillRect(0, 0, this.canvasElem.width, this.canvasElem.height);

    this.ctx.strokeStyle = '#777';
    this.ctx.beginPath();
    for (let i = 0; i <= this.canvasElem.width; i += this.pixelSize) {
      this.ctx.moveTo(i, 0);
      this.ctx.lineTo(i, this.canvasElem.height);
    }
    for (let i = 0; i <= this.canvasElem.height; i += this.pixelSize) {
      this.ctx.moveTo(0, i);
      this.ctx.lineTo(this.canvasElem.width, i);
    }
    this.ctx.stroke();
  }
}

new PixelPaint();
```

### ¿Y el repo?

Aquí está https://github.com/UnJavaScripter/pixel-paint

### ¿Qué sigue?

Hay muchas funcionalidades que vamos a agregar, entre ellas:
- [Hacer que funcione con _touch_](https://dev.to/unjavascripter/dibujando-con-canvas-trazados-5d8h)
- [Dibujar arrastrando _el pointer_ (dedo o cursor)](https://dev.to/unjavascripter/dibujando-con-canvas-trazados-5d8h)
- Borrar
- Historial
  - Deshacer
  - Rehacer
- Seleccionar color
- Dibujar canvas desde un Web Worker
- Guardar imagen
- Agregar música (¿por qué no?)
- Social-painting: Dibujar con amigos