Caso de éxito: Enredarse en el lienzo de HTML5

Derek Detweiler
Derek Detweiler

Introducción

Durante la primavera de 2010, me interesó el aumento rápido de la compatibilidad con HTML5 y las tecnologías relacionadas. En ese momento, un amigo y yo nos desafiábamos en competencias de dos semanas de desarrollo de juegos para perfeccionar nuestras habilidades de programación y desarrollo, así como para dar vida a las ideas de juegos que nos planteamos constantemente. Así que empecé a incorporar elementos HTML5 en las entradas de la competencia para comprender mejor cómo funcionaban y poder hacer cosas que eran casi imposibles con las especificaciones HTML anteriores.

De las numerosas funciones nuevas de HTML5, la compatibilidad cada vez mayor con la etiqueta de lienzo me ofreció una oportunidad emocionante para implementar arte interactivo con JavaScript, lo que me llevó a implementar un juego de habilidad mental que ahora se llama Entanglement. Ya había creado un prototipo con la parte posterior de las tarjetas de Settlers of Catan, por lo que, usando esto como plano, hay tres partes esenciales para diseñar el mosaico hexagonal en el lienzo HTML5 para jugar en la Web: dibujar el hexágono, dibujar los trazados y rotar la tarjeta. A continuación, se explica en detalle cómo lo logré en su forma actual.

Dibujar el hexágono

En la versión original de Entanglement, usé varios métodos de dibujo de lienzo para dibujar el hexágono, pero la forma actual del juego usa drawImage() para dibujar texturas recortadas de una hoja de objeto.

Hoja de objeto de tarjetas
Hoja de objeto de tarjetas

Combiné las imágenes en un solo archivo, así que se trata de una sola solicitud al servidor en lugar de diez, en este caso. Para dibujar un hexágono elegido en el lienzo, primero debemos reunir nuestras herramientas: lienzo, contexto y la imagen.

Para crear un lienzo, solo necesitamos la etiqueta de lienzo de nuestro documento HTML, así:

<canvas id="myCanvas"></canvas>

Le asigno un ID para que podamos incorporarlo en nuestra secuencia de comandos:

var cvs = document.getElementById('myCanvas');

En segundo lugar, necesitamos captar el contexto 2D para el lienzo para poder comenzar a dibujar:

var ctx = cvs.getContext('2d');

Por último, necesitamos la imagen. Si se llama "tiles.png" en la misma carpeta que nuestra página web, podemos obtenerlo de la siguiente manera:

var img = new Image();
img.src = 'tiles.png';

Ahora que tenemos los tres componentes, podemos usar ="{.drawImage() para dibujar el único hexágono que queremos de la hoja de objeto en el lienzo:

ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

En este caso, usaremos el cuarto hexágono de la izquierda en la fila superior. Además, lo dibujaremos en el lienzo de la esquina superior izquierda, manteniendo el mismo tamaño que el original. Si suponemos que los hexágonos tienen 400 píxeles de ancho y 346 píxeles de alto, en general, se verá de la siguiente manera:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';
var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

Copiamos correctamente parte de la imagen en el lienzo con el siguiente resultado:

Mosaico hexagonal
Azulejo hexagonal

Rutas de dibujo

Ahora que dibujamos nuestro hexágono en el lienzo, queremos dibujar algunas líneas sobre él. Primero, veremos parte de la geometría con respecto a la tarjeta hexagonal. Queremos que dos líneas terminen por lado, cada uno con la terminación 1/4 desde los extremos a lo largo de cada borde y un 1/2 del borde separado uno del otro, de la siguiente manera:

Extremos de línea en mosaico hexagonal
Extremos de líneas en mosaico hexagonal

También queremos una curva agradable, por lo que, con un poco de prueba y error, descubrí que, si hago una línea perpendicular desde el borde de cada extremo, la intersección de cada par de extremos alrededor de un ángulo determinado del hexágono crea un buen punto de control de Bézier para los extremos dados:

Puntos de control en mosaico hexagonal
Puntos de control en mosaico hexagonal

Ahora, asignamos los extremos y los puntos de control a un plano cartesiano correspondiente a nuestra imagen de lienzo y estamos listos para regresar al código. Para simplificar, comenzaremos con una línea. Comenzaremos por dibujar una ruta desde el extremo superior izquierdo hasta el extremo inferior derecho. La imagen hexagonal anterior era de 400 × 346, lo que hará que el extremo superior tenga 150 píxeles de ancho y 0 píxeles hacia abajo (150, 0). Su punto de control será (150, 86). El extremo del borde inferior es (250, 346) con un punto de control de (250, 260):

Coordenadas de la primera curva de Bézier
Coordenadas para la primera curva de Bézier

Con nuestras coordenadas en la mano, estamos listos para comenzar a dibujar. Comenzaremos desde cero con ="{.beginPath() y, luego, pasaremos al primer extremo con lo siguiente:

ctx.moveTo(pointX1,pointY1);

Luego, podemos trazar la línea usando ="{.bezierCurveTo() de la siguiente manera:

ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);

Como queremos que la línea tenga un borde agradable, trazaremos esta ruta dos veces con un ancho y un color diferentes cada vez. El color se establecerá con la propiedad compras.outlineStyle y el ancho, con ="{.lineWidth. En conjunto, dibujar la primera línea se verá de la siguiente manera:

var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.beginPath();
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

Ahora tenemos un mosaico hexagonal con la primera línea serpenteante a lo largo de:

Línea solitaria en mosaico hexagonal
Línea soltera en mosaico hexagonal

Si ingresas las coordenadas de los otros 10 extremos y de los puntos de control de la curva Bézier correspondientes, podemos repetir los pasos anteriores y crear una tarjeta similar a la siguiente:

Se completó el mosaico hexagonal.
Mosaico hexagonal completado

Cómo rotar el lienzo

Una vez que tengamos la tarjeta, queremos poder girarla para que se puedan tomar diferentes caminos en el juego. Para lograr esto mediante el lienzo, usamos ctx.translate() y ctx.rotate(). Queremos que la tarjeta gire sobre su centro, por lo que nuestro primer paso es mover el punto de referencia del lienzo al centro de la tarjeta hexagonal. Para hacerlo, usamos lo siguiente:

ctx.translate(originX, originY);

donde originX será la mitad del ancho del mosaico hexagonal y originY será la mitad de la altura, lo que nos proporciona lo siguiente:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);

Ahora podemos rotar la tarjeta con nuestro nuevo punto central. Dado que un hexágono tiene seis lados, vamos a querer rotarlo por un múltiplo de Math.PI dividido por 3. Lo mantendremos simple y daremos un solo giro en el sentido de las manecillas del reloj usando lo siguiente:

ctx.rotate(Math.PI / 3);

Sin embargo, dado que el hexágono y las líneas usan las coordenadas anteriores (0,0) como origen, una vez que terminemos de rotar, queremos volver a traducir antes de dibujar. Entonces, en conjunto, tenemos lo siguiente:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

Si colocas la traslación y rotación anteriores antes de que nuestro código de renderización, ahora se renderice la tarjeta rotada:

Mosaico hexagonal girado
Mosaico hexagonal rotado

Resumen

Arriba, he destacado algunas de las capacidades que HTML5 tiene para ofrecer al usar la etiqueta de lienzo, incluidas la renderización de imágenes, el dibujo de curvas Bézier y la rotación del lienzo. Usar la etiqueta de lienzo de HTML5 y sus herramientas de dibujo de JavaScript para Entanglement resultó ser una experiencia agradable, y espero con ansias las numerosas aplicaciones y juegos nuevos que otros usuarios crean con esta tecnología abierta y emergente.

Referencia de código

Todos los ejemplos de códigos proporcionados más arriba se combinan a continuación como referencia:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

ctx.beginPath();
var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 250;
pointY1 = 0;
controlX1 = 250;
controlY1 = 86;
controlX2 = 150;
controlY2 = 86;
pointX2 = 75;
pointY2 = 43;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 150;
pointY1 = 346;
controlX1 = 150;
controlY1 = 260;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 43;
controlX1 = 250;
controlY1 = 86;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 130;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 25;
pointY1 = 130;
controlX1 = 100;
controlY1 = 173;
controlX2 = 100;
controlY2 = 173;
pointX2 = 25;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 303;
controlX1 = 250;
controlY1 = 260;
controlX2 = 150;
controlY2 = 260;
pointX2 = 75;
pointY2 = 303;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();