DEV Community

Cover image for Dibujando pixeles en el navegador con Canvas... Y cosas extra
Diego Coy
Diego Coy

Posted on • Updated on

Dibujando pixeles en el navegador con Canvas... Y cosas extra

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í:

<!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>
Enter fullscreen mode Exit fullscreen mode

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 que ya se pueden usar desde ES6 (año 2015):

class PixelPaint {
  canvasElem: HTMLCanvasElement;
  ctx: CanvasRenderingContext2D;

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

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

Ya tenemos nuestro context, 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:

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Estamos usando el tamaño del 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:

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();
}
Enter fullscreen mode Exit fullscreen mode

(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:

constructor() {
  // ...
  this.drawGrid();
}
Enter fullscreen mode Exit fullscreen mode

¿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:

new PixelPaint();
Enter fullscreen mode Exit fullscreen mode

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:

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
}
Enter fullscreen mode Exit fullscreen mode

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

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

Quiero copiar y pegar

Entendible, me pasa igual. Aquí está:

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();
Enter fullscreen mode Exit fullscreen mode

¿Y el repo?

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

¿Qué sigue?

Hay muchas funcionalidades que vamos a agregar, entre ellas:

Top comments (0)