Introdução
Na primavera do ano passado (2010), me interessei pelo suporte cada vez maior para HTML5 e tecnologias relacionadas. Na época, um amigo e eu estávamos nos desafiando em competições de desenvolvimento de jogos de duas semanas para aperfeiçoar nossas habilidades de programação e desenvolvimento, além de dar vida às ideias de jogos que constantemente trocávamos. Então, comecei a incorporar elementos HTML5 nas minhas inscrições para entender melhor como eles funcionavam e fazer coisas que eram quase impossíveis usando especificações anteriores de HTML.
Entre os muitos novos recursos do HTML5, o suporte crescente à tag canvas me ofereceu uma oportunidade incrível de implementar arte interativa usando JavaScript, o que me levou a tentar implementar um jogo de quebra-cabeça chamado Entanglement. Eu já tinha criado um protótipo usando o verso dos blocos do jogo Settlers of Catan. Usando isso como um modelo, há três partes essenciais para criar o bloco hexagonal na tela HTML5 para jogos na Web: desenhar o hexágono, desenhar os caminhos e girar o bloco. A seguir, detalhamos como consegui cada um desses na forma atual.
Desenhar o hexágono
Na versão original do Entanglement, usei vários métodos de desenho de tela
para desenhar o hexágono, mas a forma atual do jogo usa drawImage()
para
desenhar texturas cortadas de uma sprite sheet.

Eu combinei as imagens em um único arquivo para que fosse apenas uma solicitação para o servidor, em vez de dez. Para desenhar um hexágono escolhido na tela, primeiro precisamos reunir nossas ferramentas: tela, contexto e imagem.
Para criar uma tela, tudo o que precisamos é da tag canvas no documento HTML, como este:
<canvas id="myCanvas"></canvas>
Eu dou um ID para que possamos extrair o script:
var cvs = document.getElementById('myCanvas');
Em segundo lugar, precisamos pegar o contexto 2D da tela para começar a desenhar:
var ctx = cvs.getContext('2d');
Por fim, precisamos da imagem. Se ele for chamado de "tiles.png" na mesma pasta que nossa página da Web, podemos fazer o seguinte:
var img = new Image();
img.src = 'tiles.png';
Agora que temos os três componentes, podemos usar ctx.drawImage() para desenhar o único hexágono que queremos da folha de sprite para a tela:
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
destinationX, destinationY, destinationWidth, destinationHeight);
Neste caso, estamos usando o quarto hexágono da esquerda na linha de cima. Além disso, vamos desenhá-lo na tela no canto superior esquerdo, mantendo o mesmo tamanho do original. Supondo que os hexágonos tenham 400 pixels de largura e 346 pixels de altura, o resultado será algo como este:
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 parte da imagem para a tela, com este resultado:

Desenhar caminhos
Agora que temos o hexágono desenhado na tela, queremos desenhar algumas linhas nele. Primeiro, vamos analisar a geometria do bloco hexagonal. Queremos duas extremidades por lado, cada uma a 1/4 das extremidades ao longo de cada borda e 1/2 da borda uma da outra, assim:

Também queremos uma curva legal. Então, usando um pouco de tentativa e erro, descobri que, se eu fizer uma linha perpendicular da borda em cada endpoint, a interseção de cada par de endpoints em torno de um determinado ângulo do hexágono cria um bom ponto de controle de Bézier para os endpoints fornecidos:

Agora, mapeamos os endpoints e os pontos de controle para um plano cartesiano correspondente à nossa imagem da tela e estamos prontos para voltar ao código. Para simplificar, vamos começar com uma linha. Vamos começar desenhando um caminho do ponto final superior esquerdo para o ponto final inferior direito. Com a imagem de hexágono anterior sendo 400 x 346, isso vai fazer com que o endpoint superior tenha 150 pixels de largura e 0 pixels de altura, abreviação (150, 0). O ponto de controle será (150, 86). O endpoint da borda inferior é (250, 346) com um ponto de controle de (250, 260):

Com as coordenadas em mãos, agora podemos começar a desenhar. Vamos começar do zero com ctx.beginPath() e depois passar para o primeiro endpoint usando:
ctx.moveTo(pointX1,pointY1);
Podemos desenhar a linha usando ctx.bezierCurveTo() da seguinte maneira:
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
Como queremos que a linha tenha uma borda bonita, vamos traçar esse caminho duas vezes usando uma largura e uma cor diferentes a cada vez. A cor será definida usando a propriedade ctx.strokeStyle e a largura será definida usando ctx.lineWidth. No geral, desenhar a primeira linha fica assim:
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();
Agora temos um bloco hexagonal com a primeira linha se movendo:

Inserindo coordenadas para os outros 10 endpoints e os pontos de controle de curva de Bézier correspondentes, podemos repetir as etapas acima e criar um bloco parecido com este:

Como girar a tela
Depois de criar o bloco, queremos que ele possa ser girado para que diferentes caminhos possam
ser seguidos no jogo. Para fazer isso usando a tela, usamos ctx.translate()
e ctx.rotate()
. Queremos que o bloco gire em torno do centro. Portanto, a primeira
etapa é mover o ponto de referência da tela para o centro do bloco hexagonal.
Para isso, usamos:
ctx.translate(originX, originY);
Em que originX será metade da largura do bloco hexagonal e originY será metade da altura, resultando em:
var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
Agora podemos girar o bloco com nosso novo ponto central. Como um hexágono tem seis lados, vamos querer girá-lo por algum múltiplo de Math.PI dividido por 3. Vamos manter a simplicidade e fazer uma única volta no sentido horário usando:
ctx.rotate(Math.PI / 3);
No entanto, como nosso hexágono e as linhas estão usando as coordenadas antigas (0,0) como a origem, depois de terminar a rotação, vamos querer traduzir de volta antes de desenhar. Então, agora temos:
var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);
Colocar a tradução e a rotação acima do código de renderização faz com que ela renderize o bloco girado:

Resumo
Acima, destaquei alguns dos recursos que o HTML5 oferece usando a tag canvas, incluindo renderização de imagens, desenho de curvas de Bézier e rotação da tela. Usar a tag de tela HTML5 e as ferramentas de desenho em JavaScript para a Entanglement foi uma experiência agradável. Estou ansioso para os muitos novos aplicativos e jogos que outras pessoas vão criar com essa tecnologia aberta e emergente.
Referência de código
Todos os exemplos de código fornecidos acima são combinados abaixo como referência:
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();