Estudo de caso - Getting Entangled with HTML5 Canvas

Derek Detweiler
Derek Detweiler

Introdução

Na primavera de 2010, me interessei pelo rápido suporte para HTML5 e tecnologias relacionadas. Na época, um amigo e eu estávamos desafiando uns aos outros em competições de desenvolvimento de jogos de duas semanas para aprimorar nossas habilidades de programação e desenvolvimento, além de dar vida às ideias de jogos que tínhamos constantemente jogando um para o outro. Então, eu comecei a incorporar elementos HTML5 naturalmente nas minhas inscrições da concorrência para compreender melhor como eles funcionavam e conseguir fazer coisas que eram quase impossíveis com as especificações de HTML anteriores.

Dentre os muitos recursos novos em HTML5, o suporte crescente para a tag de tela me deu uma excelente oportunidade de implementar arte interativa usando JavaScript. Isso me levou a tentar implementar um quebra-cabeças agora chamado de Entanglement. Eu já havia criado um protótipo usando a parte de trás dos blocos da Settlers of Catan, então usando isso como uma espécie de planta, há três partes essenciais para modelar o bloco hexagonal no canvas do HTML5 para jogo na Web: desenhar o hexágono, desenhar os caminhos e girar o bloco. A seguir, há detalhes descrevendo como realizei cada uma dessas ideias em seu formulário atual.

Como desenhar o hexágono

Na versão original de Entanglement, eu 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 recortadas de uma folha de sprite.

Folha de sprite de blocos
Folha de sprite de blocos

Eu combinei as imagens em um único arquivo para que seja apenas uma solicitação ao servidor em vez de, neste caso, dez. Para desenhar um hexágono escolhido na tela, primeiro precisamos reunir nossas ferramentas: tela, contexto e a imagem.

Para criar uma tela, basta ter a tag da tela no documento html, da seguinte forma:

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

Forneço um ID para que possamos inseri-lo em nosso script:

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

Depois, precisamos do contexto 2D da tela para começar a desenhar:

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

Por fim, precisamos da imagem. Se o nome dele for "tiles.png" na mesma pasta da nossa página da Web, podemos obtê-lo:

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 superior. Além disso, vamos desenhá-la na tela do canto superior esquerdo, mantendo-a do mesmo tamanho que a original. Supondo que os hexágonos tenham 400 pixels de largura e 346 pixels de altura, no total, eles terão esta aparência:

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 e o resultado abaixo foi o seguinte:

Bloco hexagonal
Bloco hexagonal

Traçando caminhos

Agora que o hexágono está desenhado na tela, queremos desenhar algumas linhas nele. Primeiro, vamos analisar a geometria do bloco hexagonal. Queremos duas extremidades de linha de cada lado, com cada uma terminando a 1/4 das extremidades ao longo de cada borda e 1/2 da borda uma à outra, desta forma:

Extremidades de linha no bloco hexagonal
Terminais de linha no bloco hexagonal

Também queremos uma boa curva. 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 criará um bom ponto de controle de bézi para esses endpoints:

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 de tela e estamos prontos para voltar ao código. Para simplificar, começaremos com uma linha. Vamos começar desenhando um caminho do endpoint superior esquerdo até o endpoint inferior direito. Como a imagem hexagonal anterior é de 400 x 346, o endpoint superior terá 150 pixels de largura e 0 pixels de baixo. Essa é uma abreviação de 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, estamos preparados para começar a desenhar. Começaremos do zero com ctx.beginPath() e depois passaremos para o primeiro endpoint usando:

ctx.moveTo(pointX1,pointY1);

Podemos então desenhar a linha própria usando ctx.bezierCurveTo() da seguinte maneira:

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

Como queremos que a linha tenha uma borda agradável, vamos traçar esse caminho duas vezes usando uma largura e cor diferentes a cada vez. A cor será definida usando a propriedade ctx.strokeStyle, e a largura será definida usando ctx.lineWidth. Por fim, desenhar a primeira linha ficará 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 serpenteando:

Linha solitária em um bloco hexagonal
Linha solitária em bloco hexagonal

Ao inserir coordenadas para os outros 10 endpoints, bem como os pontos de controle da curva de bézier correspondentes, podemos repetir as etapas acima e criar um bloco como este:

Bloco hexagonal concluído.
Bloco hexagonal concluído

Girar a tela

Quando tivermos nosso bloco, queremos transformá-lo para que ele possa ser percorrido em diferentes caminhos no jogo. Para fazer isso usando o canvas, usamos ctx.translate() e ctx.rotate(). Queremos que o bloco gire em torno do centro, então 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, o que nos dá:

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 girar por um 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 usam as coordenadas antigas (0,0) como origem, quando terminarmos de girar, vamos traduzir de volta antes do desenho. Agora, temos:

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

Colocar a translação e a rotação acima antes do código de renderização faz com que ele renderize o bloco girado:

Bloco hexagonal girado
Bloco hexagonal girado

Resumo

Acima, destaquei alguns dos recursos que o HTML5 tem a oferecer usando a tag de tela, 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 JavaScript para Entanglement provou ser uma experiência agradável, e estou ansioso para os novos aplicativos e jogos que outras pessoas criam com essa tecnologia aberta e emergente.

Referência de código

Todos os exemplos de código fornecidos acima são combinados abaixo como uma 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();