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>
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');
}
}
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
}
}
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();
}
(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();
}
¿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();
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);
}
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);
}
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 CACA
00.
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();
¿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
- Dibujar arrastrando el pointer (dedo o cursor)
- 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
Top comments (0)