Como usar canvas, SVG e multitoque para criar um jogo de quebra-cabeças em bloco

Começando dos conceitos básicos e terminado com um jogo de quebra-cabeças de imagem multitoque que usa o canvas e o SVG, este tutorial descreve de forma gradativa como manipular eventos de ponteiro, como os gerados com um mouse, uma caneta, ou um ou mais dedos (multitoque).

Observação  Eventos de ponteiros requerem Windows 8 ou posterior.

Observação  A implementação de eventos de ponteiro do Internet Explorer mudou um pouco desde que este artigo foi escrito para o Internet Explorer 10. Para obter detalhes sobre como atualizar e habilitar seu código para uso no futuro, consulte Atualizações de eventos de ponteiro.

Introdução

No Internet Explorer 10 e nos aplicativos da Windows Store em JavaScript, os desenvolvedores podem usar um tipo de entrada chamado ponteiro. Nesse contexto, um ponteiro pode ser qualquer ponto de contato na tela estabelecido com um mouse, uma caneta, um dedo apenas ou vários dedos. Este tutorial descreve primeiro uma introdução aos ponteiros e depois passa pela implementação de um jogo de quebra-cabeças de imagem com vários ponteiros que usa o canvas e o SVG:

Uma imagem de um quebra-cabeças habilitado para multitoque que usa o Canvas e o SVG

Se você já sabe como usar eventos de mouse, os eventos de ponteiro devem parecer familiares: MSPointerDown, MSPointerMove, MSPointerUp, MSPointerOver, MSPointerOut e assim por diante. A manipulação de eventos de mouse e ponteiro é direta, como mostra o Exemplo 1 a seguir. Além disso, em dispositivos habilitados para multitoque, o Exemplo 1 funciona como está com quantos pontos de contato (dedos) simultâneos o dispositivo pode lidar. Isso é possível porque são disparados eventos de ponteiro para cada ponto de contato da tela. Assim, aplicativos como o seguinte exemplo de desenho elementar a seguir dão suporte a multitoque sem qualquer codificação especial:

Exemplo 1 - aplicativo de desenho elementar


<!DOCTYPE html>
<html>

<head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  <title>Example 1</title>
  <style>
    html {
      -ms-touch-action: double-tap-zoom;  
    }    
  </style>
</head>

<body>
  <canvas id="drawSurface" width="500" height="500" style="border: 1px black dashed;"></canvas>
  <script>
    var _canvas = document.getElementById("drawSurface");
    var _context = _canvas.getContext("2d");
    
    _context.fillStyle = "rgba(255, 0, 0, 0.5)";
    
    if (navigator.msPointerEnabled) {
      _canvas.addEventListener('MSPointerMove', paintCanvas, false);
    }
    else {
      _canvas.addEventListener('mousemove', paintCanvas, false);
    }
        
    function paintCanvas(evt) {
      _context.fillRect(evt.clientX, evt.clientY, 5, 5);
    }
  </script>
</body>

</html>

Normalmente, o navegador utiliza eventos de toque para suas próprias finalidades. Por exemplo, para rolar uma página da Web para cima, o usuário pode tocar na tela (não em um link) e puxar para baixo. Ou, para ampliar uma página, é possível usar uma ação expansora de pinçagem com dois dedos. No Exemplo 1, não queremos que esses comportamentos padrão ocorram, pois ao criar um desenho com um ou mais dedos resultaria em um movimento panorâmico (ou na aplicação de zoom). Para que esses eventos fluam para seu código JavaScript, nós usamos esta CSS:


html {
  -ms-touch-action: double-tap-zoom;  
}

Ela instrui o navegador para "ignorar" todas as ações de toque, exceto o toque duplo (que amplia a página). Em outras palavras, agora todos os eventos de toque estão disponíveis para seu código JavaScript, exceto a capacidade de capturar o eventos de toque duplo. Os outros valores possíveis de -ms-touch-action são auto, none, manipulation e inherit, como descrito em Diretrizes para criar sites de navegação por toque.

Do ponto de vista do addEventListener, é importante notar que os eventos de ponteiro e os eventos tradicionais de mouse do Windows Internet Explorer são mutuamente exclusivos, o que quer dizer que, quando os eventos de ponteiro estão disponíveis, eles também englobam os ovantes de mouse. Em outras palavras, você não pode registrar ao mesmo tempo o ouvinte do evento paintCanvas com mousemove e MSPointerMove:



// DO NOT DO THE FOLLOWING:
_canvas.addEventListener('mousemove', paintCanvas, false);
_canvas.addEventListener('MSPointerMove', paintCanvas, false);

Em vez disso, se eventos de ponteiro da MS, que também relatam eventos de mouse, estiverem disponíveis, use-os. Caso contrário, usamos os eventos de mouse tradicionais:


if (window.navigator.msPointerEnabled) {
  _canvas.addEventListener('MSPointerMove', paintCanvas, false);
}
else {
  _canvas.addEventListener('mousemove', paintCanvas, false);
}

Assim, o fragmento de código anterior permite que o aplicativo de desenho funcione com dispositivos habilitados para toque ou tradicionais (sem toque).

Tendo entendido o básico dos eventos de ponteiro, vamos passar para a utilização mais realista: a implementação de um jogo de quebra-cabeças de imagem:

Uma imagem de um quebra-cabeças habilitado para multitoque que usa o Canvas e o SVG

Fatiando uma imagem

A primeira etapa para criar esse jogo é criar as peças ou os blocos da imagem. O método drawImage da API canvas permite facilmente fatiar uma imagem de origem em uma tela definindo as partes da imagem que serão copiadas ou exibidas. A sintaxe é o elemento drawImage(image, i_x, i_y, i_width, i_eight, c_x, c_y, c_width, c_height)As duas ilustrações seguintes mostram como partes de um elemento <img> são selecionadas e exibidas em uma tela:

Uma imagem mostrando que você pode dividir ou fatiar a imagem em peças ou blocos

Imagem de origem

Uma imagem mostrando que você pode dividir ou fatiar a imagem em peças ou blocos

Canvas

Ao quebrar a imagem de origem do quebra-cabeças em uma série de linhas e colunas (uma tabela), o método drawImage pode ser aplicado a cada célula da tabela, criando os blocos de imagem individuais requeridos:

Uma imagem mostrando como todos os blocos individuais podem se reunir para criar um todo

Esse processo de geração de blocos é demonstrado no Exemplo 2. Para ver o código-fonte do Exemplo 2, clique com o botão direito do mouse na página do Exemplo 2 e escolha Exibir código-fonte. A discussão do Exemplo 2 é dividida nestas duas seções:

Marca meta compatível com X-UA

Como o Exemplo 2 foi desenvolvido em uma intranet local usando o Internet Explorer, a marca <meta http-equiv="X-UA-Compatible" content="IE=10"> foi usada para garantir que o Internet Explorer fosse colocado no navegador e no modo de documento corretos. Para saber mais sobre isso, veja Definindo a compatibilidade de documentos. Em geral, essa marca deve ser removida pouco antes da página entrar na produção.

Fatiamento da imagem

Para converter a imagem do quebra-cabeças de 400 por 400 acima em peças (ou blocos) de quebra-cabeças úteis, primeiro criamos um objeto de imagem na memória da imagem a ser fatiada (400x400.png) e invocamos uma função anônima quando a imagem for totalmente carregada:


var image = new Image();
image.src = "images/400x400.png";
image.onload = function () { ... }

De acordo com o tamanho da imagem de origem (image.width e image.height) e o número desejado de linhas e colunas para quebrar a imagem (NUM_COLS e NUM_ROWS), calculamos o tamanho do bloco necessário com a função anônima:


var dx = _canvas.width = image.width / NUM_COLS;
var dy = _canvas.height = image.height / NUM_ROWS;

Definimos a largura e a altura do canvas para o mesmo tamanho de um bloco, pois vamos criar todos esses blocos a partir desse canvas. As imagens dos blocos individuais são criadas assim:


for (var row = 0; row < NUM_ROWS; row++) {
  for (var col = 0; col < NUM_COLS; col++) {
    ctx.drawImage(image, dx * col, dy * row, dx, dy, 0, 0, dx, dy);
    ctx.strokeRect(0, 0, dx, dy); // Place a border around each tile.
    slicedImageTable.push( _canvas.toDataURL() );
  }
}

Para entender melhor esse loop for duplamente aninhado, assuma que queremos um quebra-cabeças com cinco linhas e cinco colunas, e que as variáveis row e col são 2 e 3, respectivamente. Ou seja, estamos agora na célula (2, 3) da tabela da imagem de origem:

Uma imagem mostrando um quebra-cabeças com cinco linhas e cinco colunas, com as coordenadas (2, 3) de célula da tabela selecionadas

Se o tamanho da imagem de origem é de 400 x 400 pixels:


dx = canvas.width = 400 / 5 = 80
dy = canvas.height = 400 / 5 = 80

Isso resulta em:


ctx.drawImage(image, 80*3, 80*2, 80, 80, 0, 0, 80, 80)

Ou:


ctx.drawImage(image, 240, 160, 80, 80, 0, 0, 80, 80)

Em outras palavras, tiramos um instantâneo de 80 por 80 da imagem de origem na posição (240, 160):

Uma imagem de instantâneo do quebra-cabeças que mostra as coordenadas (240, 160) de célula da tabela selecionadas

E colocamos o instantâneo no canto superior esquerdo de um canvas de 80 px por 80 px:

Uma imagem mostrando exclusivamente o instantâneo das coordenadas (240, 160) de célula da tabela

Esse canvas é então convertido para uma imagem de URL de dados e armazenado na matriz da imagem fatiada, como mostrado aqui:


slicedImageTable.push( _canvas.toDataURL() );

O resto do Exemplo 2 garante que a imagem do quebra-cabeças foi fatiada corretamente, mostrando os blocos individuais na mesma ordem em que foram adquiridos (fatiados).

Convertendo fatias de imagem para SVG

Agora que podemos gerar fatias da imagem (ou seja, blocos), vamos investigar como converter esses blocos da imagem de URL de dados em objetos de imagem do SVG. Esse processo é demonstrado no Exemplo 3. Para ver a saída do Exemplo 3, você precisa abrir a janela do console do depurador do navegador. Para saber mais, veja Como usar as Ferramentas de Desenvolvedor F12 para depurar suas páginas da Web. A principal diferença entre os Exemplos 2 e 3 está em como os elementos de imagem SVG são criados e definidos, como mostrado aqui:


for (var row = 0; row < NUM_ROWS; row++) {
  for (var col = 0; col < NUM_COLS; col++) {
    ctx.drawImage(image, dx*col, dy*row, dx, dy, 0, 0, dx, dy);
    ctx.strokeRect(0, 0, dx, dy); // Place a border around each tile.

    var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image');
    svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", _canvas.toDataURL());
    svgImage.setAttribute('width', dx);
    svgImage.setAttribute('height', dy);
    svgImage.setAttribute('x', dx * col);
    svgImage.setAttribute('y', dy * row);
    svgImage.correctX = dx * col;
    svgImage.correctY = dy * row;
    slicedImageTable.push(svgImage);
  }
}

Como o SVG é um formulário de XML, é necessário especificar um namespace ao criar um elemento SVG (pelo menos de fora do modelo de objeto do SVG):


var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image')

Os elementos de imagem SVG usam um atributo href em oposição ao atributo src (que é usado com o elemento <img> fora do SVG). Além disso, saiba que, para o Internet Explorer, é possível usar svgImage.setAttribute('href', _canvas.toDataURL()) para definir o atributo href dos elementos de imagem SVG. Porém, outros navegadores podem exigir a sintaxe XLink; por isso, usamos o seguinte:


svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", _canvas.toDataURL());

width e height padrão de uma imagem SVG são 0, então precisamos definir esses valores explicitamente:


svgImage.setAttribute('width', dx);
svgImage.setAttribute('height', dy);

Por último, em relação ao sistema de coordenadas associado com o visor SVG (veja Transformações de coordenadas SVG), nós criamos e definimos duas propriedades personalizadas, correctX e correctY, para gravar onde cada bloco deve estar em um quebra-cabeças correto (ou seja, ao ganhar).

Exibindo imagens SVG

Agora que posicionamos as imagens SVG armazenadas na matriz de blocos, nossa próxima tarefa é mostrá-los na tela. Para fazer isso facilmente, adicionamos um elemento SVG <svg> à página da Web junto com alguns outros acréscimos, como discutido nas três próximas seções e como mostrado no Exemplo 4 (clique com o botão direito do mouse para ver a origem).

SVG líquido

Além de simplesmente adicionar um elemento SVG, também usamos algumas propriedades CSS para tornar o visor SVG totalmente líquido (ou fluido). O primeiro item que deve ser considerado é o próprio elemento SVG:


<svg width="75%" height="75%" viewbox="0 0 400 400"></svg>

Aqui, o visor SVG quadrado tem 75% da menor dimensão do visor do navegador, sendo aplicado um sistema de coordenadas de 400 por 400 unidades a ele. Para que o SVG líquido funcione como desejado, verifique se as seguintes regras CSS estão aplicadas:

  • Os elementos html e body requerem height igual a 100%:
    
    html, body {
      margin: 0;
      padding: 0;
      height: 100%
    }
    
    

    Agora, da mesma forma que o visor do navegador, o conteúdo do visor SVG também estão reduzidos. Note que o sistema de coordenadas do visor SVG de 400 por 400 unidades inerente continua intacto; somente o tamanho das unidades de coordenada muda.

  • Da mesma forma que o elemento <img>, o elemento SVG é um elemento embutido. Então, para centralizá-lo no visor do navegador, definimos sua propriedade display como block e suas margens left e right como auto:
    
    svg {
      display: block; 
      margin: 0 auto;
    }
    
    

Detecção de recurso

Como estamos usando addEventListener, o canvas e o SVG, vamos descobrir esses recursos antes de tentar mostrar um quebra-cabeças. O Exemplo 4 faz isso, como mostrado aqui:


if (window.addEventListener) {
  window.addEventListener('load', main, false);
} else {
  document.getElementsByTagName('body')[0].innerHTML = "<h1>The addEventListener method is not supported - please upgrade your browser and try again.</h1>";
} // if-else

function main() {
  var game = {};
  game.canvas = document.createElement('canvas');
  game.hasCanvas = !!game.canvas.getContext;
  game.svg = document.getElementsByTagName('svg')[0];
  game.hasSVG = game.svg.namespaceURI == "http://www.w3.org/2000/svg";
  
  .
  .
  .

  if (!game.hasCanvas) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas is not supported - please upgrade your browser and try again.</h1>";
    return;
  }

  if (!game.hasSVG) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>SVG is not supported - please upgrade your browser and try again.</h1>";
    return;
  } 
        
  .
  .
  .
  
} // main

Como você pode ver, nós só inserimos a função main se o método addEventListener é compatível (falando nisso, se addEventListener não é compatível, é bastante improvável que o canvas ou o SVG sejam).

Depois que estamos na função main, criamos uma variável game para conter todas as variáveis "globais" e o estado relacionados ao nosso jogo. Como você pode ver, se o navegador do usuário não dá suporte a todos os recursos necessários, aparece uma mensagem identificando o problema.

Quando temos certeza de que todos os recursos necessários são compatíveis, continuamos exatamente com a mesma abordagem usada no Exemplo 3, exceto pela mudança nos nomes de algumas variáveis, como de NUM_ROWS para game.numRows e de slicedImageTable para game.tiles. Então, depois que terminamos de criar todos os nossos elementos de imagem SVG (blocos) no loop for duplamente aninhado, nós os exibimos, como discutimos a seguir.

Exibindo os blocos

Com a posição de cada bloco já calculada e definida, podemos mostrar as imagens de blocos SVG simplesmente acrescentando-as ao elemento SVG:


function displaySvgImages() {
  for (var i = 0; i < game.tiles.length; i++) {
    game.svg.appendChild(game.tiles[i]);
  }
 }

Para verificar a liquidez (ou fluidez) dos blocos SVG, mude o tamanho da janela do navegador. Observe como o visor SVG tem sempre 75% da menor dimensão (largura ou altura) do visor do navegador.

Movendo os blocos

Agora que os blocos do quebra-cabeças estão sendo mostrados, nosso próximo desafio é permitir que o usuário os movimento usando um dispositivo apontador. Vamos explorar esse problemas analisando primeiro como mover três círculos SVG, como mostrado no Exemplo 5 (clique com o botão direito do mouse para ver a origem).

Primeiro, criamos uma matriz global para conter os círculos ativos no momento. Ou seja, os círculos que foram "clicados", conforme indicado por um evento mousedown ou MSPointerDown:


var _activeCircles = [];

Para ajudar a explicar o fragmento de código a seguir, se um manipulador de eventos mousemove está anexado diretamente aos círculos SVG (como parece razoável), (infelizmente) se torna possível que o usuário mova o mouse rápido o suficiente para "perder" um círculo. Ou seja, o movimento do círculo SVG não consegue acompanhar os movimentos muito rápidos do mouse do usuário. Se o manipulador de eventos mousemove é responsável pela movimentação de um círculo pela tela, assim que o mouse e o círculo são separados fisicamente, o manipulador de eventos mousemove para de ser executado e, consequentemente, o movimento do círculo para. É por isso que anexamos o manipulador de eventos mousemove ao objeto window e não aos círculos SVG – não importa a rapidez com que o usuário move o mouse, ele nunca perde o objeto window onipresente:


if (navigator.msPointerEnabled) {
  window.addEventListener('MSPointerMove', handlePointerEvents, false);
} else {
  window.addEventListener('mousemove', handlePointerEvents, false);
}

Como mostrado no fragmento de código anterior, se disponível, registramos um manipulador de eventos de ponteiro MSPointerMove (que também manipula os eventos de mouse tradicionais); caso contrário, registramos um manipulador de eventos mousemove. Lembre-se de que você não pode registrar os dois tipos de eventos (MSPointerMove e mousemove) no mesmo objeto (como descrito no Exemplo 1).

Em seguida, registramos os manipuladores de eventos de ponteiro para baixo e ponteiro para cima em cada elemento de círculo SVG:


var svgCircles = document.getElementsByTagName('circle');
for (var i = 0; i < svgCircles.length; i++) {
  if (navigator.msPointerEnabled) {
    svgCircles[i].addEventListener('MSPointerDown', handlePointerEvents, false);
    svgCircles[i].addEventListener('MSPointerUp', handlePointerEvents, false);
  } else {
    svgCircles[i].addEventListener('mousedown', handlePointerEvents, false);
    svgCircles[i].addEventListener('mouseup', handlePointerEvents, false);
  }
} // for

Preste muita atenção à função handlePointerEvents, pois vamos discuti-la em seguida:


function handlePointerEvents(evt) {
  var activeCircle; 
  var activeCircleIndex = evt.pointerId || 0;

  switch (evt.type) {
    case "mousedown":
    case "MSPointerDown":
      _svgElement.removeChild(evt.target);
      _svgElement.appendChild(evt.target);
      if (evt.pointerId) {
        evt.target.msSetPointerCapture(evt.pointerId);
      }
      _activeCircles[activeCircleIndex] = evt.target;
      break;
    case "mousemove":
    case "MSPointerMove":
      activeCircle = _activeCircles[activeCircleIndex];
      if (activeCircle) {
        var svgPoint = _svgElement.createSVGPoint();
        svgPoint.x = evt.clientX;
        svgPoint.y = evt.clientY;

        var ctm = activeCircle.getScreenCTM();
        svgPoint = svgPoint.matrixTransform(ctm.inverse());
        activeCircle.setAttribute('cx', svgPoint.x);
        activeCircle.setAttribute('cy', svgPoint.y);
      } // if
      break;
    case "mouseup":
    case "MSPointerUp":
      if (evt.pointerId) {
        _activeCircles[activeCircleIndex].msReleasePointerCapture(evt.pointerId);
      }              
      delete _activeCircles[activeCircleIndex];
      break;
    default:
      alert("Error in handlePointerEvents on: " + evt.type);
  } // switch
} // handlePointerEvents

Para ajudar a descrever handlePointerEvents, vamos passar por dois cenários: a movimentação de um único círculo e a movimentação simultânea de dois círculos.

Movimento de um único círculo

Há três eventos que devem ser manipulados: para baixo, mover e para cima.

Evento para baixo

Quando o usuário toca em um único círculo (com o mouse, a caneta ou o dedo), o evento para baixo é disparado, invocando handlePointerEvents. Se os eventos de ponteiro são compatíveis, evt.pointerId não é nulo e activeCircleIndex é igual a evt.pointerId; caso contrário, activeCircleIndex é 0 (graças a evt.pointerId || 0). Se evt.pointerId é null, apenas um círculo pode estar ativo por vez, especificamente _activeCircles[0], que é a única possibilidade quando um mouse é o único dispositivo apontador permitido.

Em seguida, a instrução switch verifica evt.type e desvia o fluxo de controle para a cláusula mousedown/MSPointerDown. Para garantir que o círculo ativo esteja sempre sobre todos os outros, basta remover e acrescentá-lo ao DOM (o último elemento acrescentado é sempre o elemento renderizado por último/superior).

Então, se evt.pointerId está definido, chamamos msSetPointerCapture em evt.target (ou seja, o círculo ativo) para que o círculo possa continuar recebendo todos os eventos registrados. Assim, o círculo pode ser retirado e devolvido fisicamente ao visor do navegador.

Por fim, registramos o círculo tocado na lista de círculos ativos:


_activeCircles[activeCircleIndex] = evt.target;

Evento mover

Quando o dispositivo apontador é movimentado (no objeto window), o manipulador de eventos mousemove/MSPointerMove (handlePointerEvents) é executado, o que desvia o fluxo de controle para a cláusula mousemove/MSPointerMove da instrução switch. Se nenhum círculo foi tocado antes, a matriz de _activeCircles está vazia e activeCircle = _activeCircles[activeCircleIndex] é null. Nesse caso, passamos direto para a instrução break e saímos da instrução switch.

Se, por outro lado, activeCircle não é null (ou seja, se há um círculo ativo que deve ser movido), convertemos as coordenadas do dispositivo apontador (evt.clientX, evt.clientY), que são relativas ao visor do navegador, para o sistema de coordenadas SVG de 400 por 400. Isso é feito através da CTM (matriz de transformação de coordenadas). Para saber mais, veja Transformações de coordenadas SVG.

Por fim, movemos o centro do círculo (cx, cy) para os valores de coordenadas transformados:


activeCircle.setAttribute('cx', svgPoint.x);
activeCircle.setAttribute('cy', svgPoint.y);

Evento para cima

Se um evento para cima é disparado em um círculo, o fluxo de controle é desviado para a cláusula mouseup/MSPointerUp da instrução switch. O círculo tocado/clicado, indicado por activeCircleIndex, é removido da lista de círculos ativos e, se aplicável, sua solicitação de eventos de captura é liberada via msRelasePointerCapture.

Movimento de dois círculos

Como no caso do círculo único, há três eventos para serem manipulados: para baixo, mover e para cima.

Eventos para baixo

Nesse caso, dois objetos de evento para baixo de círculo (quase simultâneos) fluem para a cláusula mousedown/MSPointerDown do switch. Agora, a matriz _activeCircles contém dois objetos de círculo (cujos índices que os acessam são os valores de evt.pointerId associados).

Eventos mover

Como os dois objetos de evento mover de janela (quase simultâneos) fluem para a cláusula mousemove/MSPointerMove do switch, cada círculo é movido por sua vez, como no caso do círculo único.

Eventos para cima

Como cada objeto de evento para cima de círculo (quase simultâneos) flui para a cláusula mouseup/MSPointerUp, cada um é limpo por sua vez, como no caso do círculo único.

Quebra-cabeças da imagem

Agora temos todas as peças essenciais necessárias para criar um jogo de quebra-cabeças de imagem por toque totalmente funcional (mas não necessariamente atraente). Vamos dividir esta discussão bastante longa em vários componentes fáceis de digerir. Vamos começar com o Exemplo 6, a estrutura do esqueleto do jogo (o jogo completo é apresentado no Exemplo 7 e discutido mais tarde).

Estrutura do esqueleto

Ao contrário do jogo apresentado em Animação SVG avançada, a estrutura do esqueleto do quebra-cabeças é relativamente direta, como mostrado no Exemplo 6 (clique com o botão direito do mouse para ver a origem). A marcação principal do Exemplo 6 é esta:


<table>
  <tr>
    <td colspan="3"><h1>A Multi-Touch Enabled Image Puzzle Using Canvas &amp; SVG</h1></td>
  </tr>
  <tr>
    <td id="playButtonWrapper"><button id="playButton">Play</button></td>
    <td id="messageBox">Always carefully study the image before clicking the play button!</td>
    <td id="scoreBox"><strong>Score:</strong> 0</td>
  </tr>
</table>
<svg width="75%" height="75%" viewBox="0 0 400 400">
  <rect width="100%" height="100%" style="fill: black; stroke: black;" />
</svg>

O elemento <rect> é usado para colorir todo o visor SVG de preto (independente de seu tamanho atual). Isso fornece um "campo de jogo" para as peças de bloco do quebra-cabeças. Esse efeito pode ser visto na seguinte captura de tela do Exemplo 6:

Uma captura de tela mostrando o "campo de jogo" do esqueleto das peças de bloco do quebra-cabeças

O resto da estrutura do esqueleto do Exemplo 6 consiste no JavaScript a seguir:


if (window.addEventListener) {
  window.addEventListener('load', main, false);
} else {
  document.getElementsByTagName('body')[0].innerHTML = "<h1>The addEventListener method is not supported - please upgrade your browser and try again.</h1>";
} // if-else

function main() {
  var imageList = ["images/puzzle0.png", "images/puzzle1.png", "images/puzzle2.png", "images/puzzle3.png", "images/puzzle4.png", "images/puzzle5.png"]; // Contains the paths to the puzzle images (and are cycled through as necessary).
  var game = new Game(2, imageList);

  function Game(size_in, imageList_in) {
    var gameData = {};
    gameData.elements = {};
    gameData.elements.canvas = document.createElement('canvas');
    gameData.elements.svg = document.getElementsByTagName('svg')[0];
    
    this.hasCanvas = !!gameData.elements.canvas.getContext;
    this.hasSVG = gameData.elements.svg.namespaceURI == "http://www.w3.org/2000/svg";

    this.init = function() { alert("The init() method fired.") };
  } // Game

  if (!game.hasCanvas) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas is not supported - please upgrade your browser and try again.</h1>";
    return;
  } 
  
  if (!game.hasSVG) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>SVG is not supported - please upgrade your browser and try again.</h1>";
    return; 
  } 

  // Assert: The user's browser supports all required features.

  game.init(); // Start the game.
} // main

Como descrito na seção anterior sobre detecção de recursos, se addEventListener está disponível, disparamos a função main. Então, inicializamos uma matriz, imageList, que contém os caminhos das imagens a serem usadas no jogo (ou seja, as imagens que vão ser quebradas em blocos, aleatorizadas e resolvidas). Se o usuário chega ao sétimo nível, a primeira imagem (puzzle0.png) é reciclada - infinitamente.

Em seguida, invocamos a função de construtor Game. O primeiro parâmetro, 2, instrui o construtor a gerar um objeto game que tem duas colunas e duas linhas. O último parâmetro é, claro, a lista de imagens do quebra-cabeças que vão circular (se necessário).

No construtor, colocamos todas as variáveis "globais" em uma conveniente variável gameData. Se você não conhece a palavra-chave this, a instrução this.hasCanvas = !!game.elements.canvas.getContext cria e define uma propriedade chamada hasCanvas no objeto que o construtor construiu (a variável game). A dupla negação (!!) simplesmente força a expressão game.elements.canvas.getContext para um valor Booliano (true se o canvas for compatível).

De forma semelhante, this.init = function() { … } define um método, chamado init, para todos os objetos criados pelo construtor (há apenas um objeto desses, game). A invocação de game.init(), entre outras coisas, inicia o jogo.

Um jogo de quebra-cabeças de imagem multitoque

Agora estamos em posição de combinar tudo o que fizemos em um jogo de quebra-cabeças de imagem multitoque totalmente funcional, como mostrado no Exemplo 7. O código-fonte associado ao Exemplo 7 deve parecer familiar e foi bem comentado, mas os dois componentes a seguir podem ficar mais claros com uma explicação adicional:

Randomização de blocos

Na função createAndAppendTiles, geramos os objetos de imagem de bloco SVG (como no Exemplo 4). Porém, antes de acrescentá-los ao elemento SVG, eles são aleatorizados e verificamos se o padrão aleatorizado não corresponde exatamente à imagem do quebra-cabeças original (completa) (isso é um problema real apenas no primeiro nível, com quatro blocos):


var randomizationOK = false; 
while (!randomizationOK) {
  coordinatePairs.sort(function() { return 0.5 - Math.random(); });
  for (var i = 0; i < gameData.tiles.length; i++) {
    if (gameData.tiles[i].correctX != coordinatePairs[i].x || 
        gameData.tiles[i].correctY != coordinatePairs[i].y) {
      randomizationOK = true;
      break;
    } // if
  } // for
} // while

Para aleatorizar os blocos facilmente, colocamos seus pares de coordenadas associados em uma matriz (coordinatePairs) e usamos o método de classificação de matriz JavaScript da seguinte maneira:



coordinatePairs.sort(function() { return 0.5 - Math.random(); });

Como descrito em sort Method (JavaScript) e desde que Math.random() retorne um valor entre 0 e 1, essa função de classificação anônima classifica os elementos da matriz de forma aleatória.

Cláusula up de ponteiro estendida

As cláusula down e move da instrução switch são quase idênticas aos exemplos anteriores. Porém, a cláusula up é significativamente ampliada:



case "mouseup":
case "MSPointerUp":
activeTile = gameData.activeTiles[activeTileIndex];
var currentX = activeTile.getAttribute('x');
var currentY = activeTile.getAttribute('y');

for (var i = 0; i < gameData.tiles.length; i++) {
  var correctX = gameData.tiles[i].correctX;
  var correctY = gameData.tiles[i].correctY;

  if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) && 
      currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
    activeTile.setAttribute('x', correctX);
    activeTile.setAttribute('y', correctY);
    break;
  } // if
} // for

if (evt.pointerId) {
  gameData.activeTiles[activeTileIndex].msReleasePointerCapture(evt.pointerId);
} 

delete gameData.activeTiles[activeTileIndex];

if (gameData.inProgress) {
  for (var i = 0; i < gameData.tiles.length; i++) {
    currentX = Math.round(gameData.tiles[i].getAttribute('x'));
    currentY = Math.round(gameData.tiles[i].getAttribute('y'));

    correctX = Math.round(gameData.tiles[i].correctX);
    correctY = Math.round(gameData.tiles[i].correctY);

    if (currentX != correctX || currentY != correctY) {
      return;
    } // if
  } // for

  // Assert: The user has solved the puzzle.

  gameData.inProgress = false;

  gameData.score += gameData.size * gameData.size;
  gameData.elements.scoreBox.innerHTML = "<strong>Score:</strong> " + gameData.score;

  ++(gameData.size);

  var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
  document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];

  for (var i = 0; i < gameData.tiles.length; i++) {
    gameData.elements.svg.removeChild(gameData.tiles[i])
  }
  
  createAndAppendTiles();

  gameData.elements.playButton.innerHTML = "Play";
} // if
break;

O primeiro componente da cláusula up que devemos discutir é o ajuste de blocos. Em vários jogos de quebra-cabeças (inclusive este), é necessário que a peça (o bloco) do quebra-cabeças em andamento se ajuste no lugar quando movido apropriadamente para perto de seu local correto. O código usado para fazer isso, conforme copiado do exemplo anterior, é mostrado aqui:


activeTile = gameData.activeTiles[activeTileIndex];
var currentX = activeTile.getAttribute('x');
var currentY = activeTile.getAttribute('y');

for (var i = 0; i < gameData.tiles.length; i++) {
  var correctX = gameData.tiles[i].correctX;
  var correctY = gameData.tiles[i].correctY;

  if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) && 
      currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
    activeTile.setAttribute('x', correctX);
    activeTile.setAttribute('y', correctY);
    break; // We've positioned the active tile correctly, so exit the FOR loop now.
  } // if
} // for

Nesse fragmento de código, obtemos a posição do bloco ativo (quando o evento para cima é disparado) e fazemos a iteração por todos os blocos para determinar as posições corretas de todos os blocos. Se a posição do bloco ativo estiver "bastante próxima" de uma dessas posições corretas, ele é ajustado no lugar e o loop for é imediatamente interrompido (não é preciso olhar em nenhuma das outras posições corretas). A cláusula if desenha metaforicamente uma pequena caixa de detecção de colisão ao redor do centro de uma posição correta e, se a posição do bloco ativo estiver dentro dela, a cláusula se torna true e ajusta o bloco no lugar usando as duas chamadas do método setAttribute a seguir:


if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) && 
    currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
  activeTile.setAttribute('x', correctX);
  activeTile.setAttribute('y', correctY);
  break;
} // if

Saiba que, conforme gameData.snapDelta aumenta, também aumenta o tamanho da caixa de detecção de colisão, o que torna o ajuste do bloco menos sensível.

Em seguida, se o jogo está em andamento, verificamos se o posicionamento do bloco ativo foi o final e correto, iterando todos os blocos e verificando  à força :


if (gameData.inProgress) {
  for (var i = 0; i < gameData.tiles.length; i++) {
    currentX = Math.round(gameData.tiles[i].getAttribute('x'));
    currentY = Math.round(gameData.tiles[i].getAttribute('y'));

    correctX = Math.round(gameData.tiles[i].correctX);
    correctY = Math.round(gameData.tiles[i].correctY);

    if (currentX != correctX || currentY != correctY) {
      return;
    } // if
  } // for

Se nem todos os blocos estiverem em suas posições corretas, saímos de handlePointerEvents imediatamente e esperamos que o próximo evento de ponteiro dispare handlePointerEvents. Caso contrário, o usuário solucionou o quebra-cabeças e o seguinte código é executado:


gameData.inProgress = false;

gameData.score += gameData.size * gameData.size;
gameData.elements.scoreBox.innerHTML = "<strong>Score:</strong> " + gameData.score;

++(gameData.size);

var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];

for (var i = 0; i < gameData.tiles.length; i++) {
  gameData.elements.svg.removeChild(gameData.tiles[i])
}

createAndAppendTiles();

gameData.elements.playButton.innerHTML = "Play";

Como este nível do jogo foi vencido, definimos gameData.inProgress como false e mostramos o novo placar. Como o tamanho atual (o número de linhas e colunas) do jogo também é usado para indicar o nível do jogo (ou seja, quantos quebra-cabeças o usuário resolveu até agora) e como a dificuldade do jogo é proporcional ao quadrado de sua contagem de linhas (ou de colunas, pois eles são iguais), o placar aumenta pelo quadrado do tamanho atual do jogo:


gameData.score += gameData.size * gameData.size;

Em seguida, incrementamos nosso proxy de nível de jogo, gameData.size, e mostramos um dizer espirituoso aleatório de uma matriz de possíveis frases "você venceu":


++(gameData.size);

var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];

Finalmente, removemos todos os elementos de imagem SVG (blocos) pré-existentes anexados ao elemento SVG para nos preparamos para a próxima rodada de elementos de imagem SVG (diferentes) que será criada e acrescentada ao elemento SVG via createAndAppendTiles:


for (var i = 0; i < gameData.tiles.length; i++) {
  gameData.elements.svg.removeChild(gameData.tiles[i])
}

createAndAppendTiles();
gameData.elements.playButton.innerHTML = "Play";

 

Nosso objetivo aqui era mostrar a você, por meio deste tutorial, como manipular eventos multitoque em um cenário relativamente realista: a implementação de um jogo de quebra-cabeças por toque. Esta orientação deve fornecer conhecimento suficiente para você manipular eventos de toque em diversas situações (possivelmente incluindo aquelas que envolvem o canvas e o SVG).

Tópicos relacionados

Galeria de fotos de imagens da Contoso
Eventos de Gesto
Diretrizes para criar sites de navegação por toque
Como simular foco em dispositivos habilitados para toque
Gráficos HTML5
Amostras e tutoriais do Internet Explorer
Eventos de ponteiro
Atualizações de eventos de ponteiro

 

 

Mostrar:
© 2014 Microsoft