Estudo de caso: como usar o HTML5 Canvas

Derek Detweiler
Derek Detweiler

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.

Folha de sprite de blocos
Folha de sprite de blocos

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:

Bloco hexagonal
Telha hexagonal

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:

Endpoints de linha em Bloco hexagonal
Endpoints de linha em bloco hexagonal

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:

Pontos de controle no bloco hexagonal
Pontos de controle no bloco hexagonal

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

Coordenadas da primeira curva de Bézier
Coordenadas da primeira curva de Bézier

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:

Linha única em bloco hexagonal
Linha solitária no bloco hexagonal

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:

Bloco hexagonal concluído.
Telha hexagonal concluída

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:

Bloco hexagonal girado
Telha hexagonal girada

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();