Cómo usar canvas, SVG y multitáctil para crear un juego de rompecabezas en mosaico.

Desde los conceptos básicos hasta un juego de rompecabezas con imágenes multitáctil que usa canvas y SVG, en este tutorial se describe, de forma gradual, cómo administrar eventos del puntero tales como aquellos generados a partir de un mouse, lápiz o uno o más dedos (multitáctil).

Nota  Los eventos de puntero requieren Windows 8 o versiones posteriores.

Nota  La implementación de eventos de puntero de Internet Explorer ha cambiado ligeramente desde que se escribió este artículo para Internet Explorer 10. Para obtener más información sobre cómo actualizar y preparar tu código para el futuro, consulta Actualizaciones de eventos de puntero.

Introducción

En Internet Explorer 10 y las aplicaciones de la Tienda Windows con JavaScript, los desarrolladores pueden usar un tipo de entrada denominada puntero. Un puntero, en este contexto, puede ser cualquier punto de contacto sobre la pantalla hecho por un mouse, un lápiz o uno o varios dedos. En este tutorial primero se ofrece una introducción a los punteros y después se explica la implementación de un juego de rompecabezas multipuntero con imágenes que usa Canvas y SVG:

Una imagen de un rompecabezas habilitado multitáctil donde se usa Canvas y SVG

Si ya sabes usar eventos del mouse, los eventos del puntero te resultarán muy familiares: MSPointerDown, MSPointerMove, MSPointerUp, MSPointerOver, MSPointerOut, etc. La administración de eventos del mouse y puntero es sencilla, como se muestra en el ejemplo 1 a continuación. Además, para dispositivos habilitados multitáctil, el ejemplo 1 funciona tal como está con la mayor cantidad de puntos de contacto simultáneos (dedos) posible que puede administrar el dispositivo. Esto es posible porque los eventos del puntero se desencadenan por cada punto de contacto de la pantalla. Por lo tanto, las aplicaciones como la del siguiente ejemplo de dibujo básica admite multitáctil sin ninguna codificación especial:

Ejemplo 1: aplicación de dibujo básica


<!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, el explorador consume eventos de toque para sus propios fines. Por ejemplo, para desplazarse hacia arriba en una página web, el usuario puede tocar la pantalla (no sobre un vínculo) y arrastrar hacia abajo. O para acercar una página, se puede usar un gesto expansivo de acercar los dedos. En el ejemplo 1, no queremos que se produzcan estos comportamientos predeterminados. De lo contrario, al hacer un dibujo con uno (o más) dedos, en cambio se podría expandir (o quizás acercar) la página. Para permitir que estos eventos fluyan hacia el código JavaScript, usamos la siguiente CSS:


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

Esto le indica al explorador que "ignore" todas las acciones de toque excepto la doble pulsación (que acerca la página). Es decir que todos los eventos de toque ya están disponibles para el código JavaScript excepto la capacidad de capturar el evento de doble pulsación. Los otros valores posibles para -ms-touch-action son auto, none, manipulation y inherit como se describe en la sección sobre las instrucciones para crear sitios para pantallas táctiles.

Desde una perspectiva addEventListener, es importante tener en cuenta que los eventos del puntero y los eventos del mouse tradicionales de Windows Internet Explorer se excluyen mutuamente. Esto significa que cuando los eventos del puntero están disponibles, estos también comprenden los eventos del mouse. Es decir, actualmente no puedes registrar la escucha de eventos de paintCanvas con mousemove y MSPointerMove:



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

En cambio, si están disponibles los eventos del puntero MS, que también informan sobre los eventos del mouse, los usamos. De los contrario, usamos eventos tradicionales del mouse:


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

Por lo tanto, el fragmento de código anterior permite que la aplicación de dibujo funcione tanto con dispositivos táctiles como con tradicionales (no táctiles).

Habiendo aclarado ya los conceptos básicos de los eventos del puntero, pasaremos a un uso más realista como es la implementación de un juego de rompecabezas con imágenes:

Una imagen donde se muestra un rompecabezas habilitado multitáctil que usa Canvas y SVG

Dividir una imagen

Lo primero que hay que hacer para obtener este juego es crear las piezas de la imagen o mosaicos. El método drawImage de la API de Canvas permite dividir fácilmente una imagen de origen en un lienzo definiendo las partes de la imagen que hay que copiar o mostrar. La sintaxis es drawImage(image, i_x, i_y, i_width, i_eight, c_x, c_y, c_width, c_height)Las dos siguientes ilustraciones reflejan el modo en que las partes de un elemento <img> se seleccionan y muestran en un lienzo:

Imagen donde se muestra que puedes dividir o fragmentar la imagen en piezas o mosaicos

Imagen de origen

Imagen donde se muestra que puedes dividir o fragmentar la imagen en piezas o mosaicos

Canvas

Al cortar la imagen de origen del rompecabezas en varias filas y columnas (una tabla), se puede aplicar el método drawImage a cada celda de la tabla, y obtener así los mosaicos de imagen individuales necesarios:

Imagen donde se muestra cómo todos los mosaicos individuales pueden unirse para crear un todo

Este proceso para generar mosaicos se muestra en el ejemplo 2. Para ver el código fuente del ejemplo 2, haz clic con el botón secundario en la página del ejemplo 2 y elije Ver código fuente. El ejemplo 2 se trata en las dos siguientes secciones:

Etiqueta meta compatible con X-UA

Debido a que el ejemplo 2 se desarrolló en una intranet local con Internet Explorer, se usó la etiqueta <meta http-equiv="X-UA-Compatible" content="IE=10"> para asegurarse de que Internet Explorer esté ubicado en el modo de documento y explorador correctos. Si quieres obtener más información sobre esto, consulta Definición de la compatibilidad de documentos. En general, esta etiqueta se debería quitar justo antes de que la página vaya a producción.

División de la imagen

Para convertir la imagen del rompecabezas anterior de 400 por 400 en piezas de rompecabezas útiles (o mosaicos), primero creamos un objeto de imagen en memoria de la imagen que se va a dividir (400x400.png) e invocar una función anónima cuando la imagen se haya cargado completamente:


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

Se calcula el tamaño de mosaico necesario dentro de la función anónima, en base al tamaño de la imagen de origen (image.width y image.height) y el número de filas y columnas deseado para dividir la imagen en (NUM_COLS y NUM_ROWS):


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

Fijamos el ancho y el alto del canvas para que tenga el mismo tamaño de un mosaico porque vamos a crear todos los mosaicos a partir de este canvas. Las imágenes individuales de mosaico se crean de la siguiente manera:


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

Pare entender mejor este bucle for doblemente anidado, imagina que queremos un rompecabezas de 5 filas por 5 columnas y que las variables row y col en este momento son 2 y 3 respectivamente. Es decir, en este momento estamos en la celda de tabla (2, 3) de la imagen de origen:

Imagen donde se muestra un rompecabezas de 5 filas por 5 columnas con las coordenadas de celda de tabla (2, 3) seleccionadas

Si el tamaño de la imagen de origen es de 400 x 400 píxeles, entonces:


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

Lo cual da como resultado:


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

O bien:


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

En otras palabras, tomamos una fotografía de 80 por 80 de la imagen de origen en la posición (240, 160):

Una imagen de fotografía del rompecabezas donde se muestran la coordenadas de las celdas de tabla (240, 160) seleccionadas

Y coloca la fotografía en la esquina superior izquierda de un canvas de 80px por 80px:

Imagen donde se muestra la fotografía de las coordenadas de las celdas de tabla (240, 160) exclusivamente

Este canvas después se convierte en una imagen URL de datos y se almacena en la matriz de la imagen fragmentada, como se muestra aquí:


slicedImageTable.push( _canvas.toDataURL() );

Para asegurarnos de que la imagen del rompecabezas se dividió correctamente, en el resto del ejemplo 2 se muestran los mosaicos individuales en el mismo orden en el que se obtuvieron o cortaron.

Cómo convertir fragmentos de imagen en SVG

Ahora que ya podemos generar fragmentos de imágenes (es decir, mosaicos), investigamos cómo convertir estos mosaicos de imágenes URL de datos en objetos de imágenes SVG. Este proceso se muestra en el ejemplo 3. Para ver la salida del ejemplo 3, se debe abrir la ventana de la consola del depurador del explorador. Si quieres obtener más información, consulta Cómo usar las herramientas de desarrollo F12 para depurar las páginas web. La mayor diferencia entre los ejemplos 2 y 3 es el modo en el que se crean y configuran los elementos de imagen SVG, como se muestra aquí:


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

Ya que SVG es una forma de XML, se debe especificar un espacio de nombres al crear un elemento SVG (al menos desde fuera del modelo del objeto SVG):


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

Los elementos de imagen SVG usan un atributo href a diferencia del atributo src (el cual se usa con el elemento <img> que no es SVG). Además, observa que para Internet Explorer, se puede usar svgImage.setAttribute('href', _canvas.toDataURL()) para establecer el atributo href de los elementos de imagen SVG. Sin embargo, otros exploradores podrían necesitar sintaxis XLink, y es por eso que en cambio se usa lo siguiente:


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

El width y height predeterminados de una imagen SVG están en 0, por lo tanto debemos establecer estos valores explícitamente:


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

Finalmente, con respecto al sistema de coordenadas asociado con la ventanilla SVG (consulta Transformaciones de coordenadas de SVG ), creamos y establecemos dos propiedades predeterminadas, correctX y correctY, para registrar donde debería estar cada mosaico en un rompecabezas correcto (es decir, ganado).

Visualización de imágenes SVG

Ahora que hemos colocado las imágenes SVG almacenadas en la matriz de mosaicos, nuestra próxima tarea es mostrarlas en la pantalla. Para hacerlo de forma fácil, agregamos un elemento SVG <svg> a la página web junto con algunos otros aumentos como se tratará en las siguientes tres secciones y como se muestra en el ejemplo 4 (haz clic con el botón secundario para ver el origen).

SVG líquido

Además de agregar un elemento SVG, también usamos algunas propiedades CSS para hacer la ventanilla SVG completamente líquida (o fluida). El primer elemento a tener en cuenta es el elemento SVG en sí mismo:


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

Aquí, la ventanilla SVG cuadrada es el 75% de la dimensión de la ventanilla del explorador más pequeña, y se le aplica un sistema de coordenadas de 400 por 400 unidades. Para que el SVG líquido funcione correctamente, asegúrate de que se apliquen las siguientes reglas CSS:

  • Los elementos html y body requieren una height de 100%:
    
    html, body {
      margin: 0;
      padding: 0;
      height: 100%
    }
    
    

    Ahora, como se reduce el tamaño de la ventanilla del explorador, lo mismo pasa con los contenidos de la ventanilla SVG. Ten en cuenta que el sistema de coordenadas de la ventanilla SVG de 400 por 400 unidades inherente permanece intacto. Solo cambia el tamaño de las unidades de coordenadas.

  • Como el elemento <img>, el elemento SVG es un elemento en línea. De modo que para centrarlo dentro de la ventanilla del explorador, establecemos la propiedad display en block y sus márgenes left y right en auto:
    
    svg {
      display: block; 
      margin: 0 auto;
    }
    
    

Detección de características

Como estamos usando addEventListener, canvas y SVG, detectemos estas características antes de intentar mostrar un rompecabezas. En el ejemplo 4 se hace esto como se muestra aquí:


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 puedes ver, solo escribimos la función main si el método addEventListener es compatible (a propósito, si addEventListener no es compatible, es muy improbable que canvas o SVG tampoco lo sea).

Después de haber llegado a la función main, creamos una variable de game para contener todos los estados y variables "globales" relativos al juego. Como puedes ver, si el explorador del usuario no admite todas las características requeridas, aparece un mensaje que identifica el problema.

Una vez que estemos seguros de que todas las características requeridas son admitidas, continuaremos exactamente con el mismo enfoque que usamos en el ejemplo 3, excepto que los nombres de algunas variables han cambiado, como NUM_ROWS a game.numRows y slicedImageTable a game.tiles. Después, cuando terminemos de crear todos los elementos de las imágenes SVG (mosaicos) en el bucle for doblemente anidado, los mostramos, como se tratará a continuación.

Visualización de mosaicos

Con la posición de cada mosaico ya calculada y establecida, podemos mostrar las imágenes de mosaico SVG simplemente al anexarlas al elemento SVG:


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

Para verificar la liquidez (o fluidez) de los mosaicos SVG, cambia el tamaño de la ventana del explorador. Ten en cuenta que la ventanilla SVG es siempre el 75% de la dimensión menor (ancho y alto) de la ventanilla del explorador.

Cómo mover mosaicos

Ahora que los mosaicos del rompecabezas se pueden ver, el próximo desafío es permitirle al usuario moverlos con un dispositivo señalador. Para analizar este problema, primero veremos cómo mover tres círculos SVG, como se muestra en el ejemplo 5 (haz clic con el botón secundario para ver el origen).

Primero creamos una matriz global que contenga los círculos que están actualmente activos. Es decir, los círculos sobre los que se ha hecho clic como se indica en un evento mousedown o MSPointerDown:


var _activeCircles = [];

Para poder explicar el siguiente fragmento de código, si un controlador de eventos mousemove se adjuntara directamente a los círculos SVG (lo que parece razonable), el usuario (desafortunadamente) podría mover el mouse tan rápidamente como para "perder" un círculo. Es decir, el movimiento del círculo SVG no puede igualar la velocidad de los movimientos tan rápidos que realiza el usuario con el mouse. Si asumimos que el controlador de eventos mousemove es responsable de mover un círculo a lo largo de la pantalla, tan pronto como el mouse y el círculo se encuentren físicamente separados, el controlador de eventos mousemove deja de ejecutarse y, por consiguiente, se detiene el movimiento del círculo. Por este motivo, adjuntamos el controlador de eventos mousemove al objeto window en lugar de los círculos SVG (sin importar cuán rápido el usuario mueva el mouse, nunca podrá perder el objeto window omnipresente:


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

Tal como se muestra en el fragmento de código anterior, registramos un controlador de eventos del puntero MSPointerMove si está disponible (que también controla los eventos tradicionales del mouse) y si no, registramos el controlador de eventos mousemove. Recuerda que no puedes registrarte para ambos tipos de evento (MSPointerMove y mousemove) en el mismo objeto (tal como se describe en el ejemplo 1).

A continuación, registramos los controladores de evento del puntero hacia arriba y el puntero hacia abajo en cada elemento del 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

Presta mucha atención a la función handlePointerEvents, tal como analizaremos a continuación:


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 describir handlePointerEvents, recorreremos dos escenarios: mover un solo círculo, después mover dos círculos simultáneamente.

Movimiento de un solo círculo

Hay tres eventos para controlar: down, move y up.

Evento down

Cuando el usuario toca un solo círculo (con el mouse, lápiz o el dedo), se desencadena el evento down, que invoca handlePointerEvents. Si se admiten los eventos de puntero, evt.pointerId no será nulo y activeCircleIndex será igual a evt.pointerId; de lo contrario, activeCircleIndex será 0 (gracias a evt.pointerId || 0). Si evt.pointerId es null, solo un círculo podrá activarse a la vez, es decir _activeCircles[0], que es la única posibilidad cuando el único dispositivo señalador permitido es el mouse.

A continuación, la instrucción switch comprueba evt.type y deriva el flujo de control hacia la cláusula mousedown/MSPointerDown. Para asegurarte de que el círculo activo esté siempre arriba de todos los otros, simplemente lo quitamos y lo anexamos al DOM (el último elemento que se anexa es siempre el último/superior elemento representado).

A continuación, si se define evt.pointerId, llamamos msSetPointerCapture en evt.target (es decir, el círculo activo) para que el círculo pueda continuar para recibir todos los eventos registrados. Esto permite que el círculo se quite y se vuelva a colocar físicamente en la ventanilla del explorador.

Por último, registramos el círculo tocado en la lista de círculos activos:


_activeCircles[activeCircleIndex] = evt.target;

Evento move

Cuando se mueve el dispositivo señalador (en el objeto window), se ejecuta el controlador de eventos mousemove/MSPointerMove (handlePointerEvents), que deriva el flujo de control a la cláusula mousemove/MSPointerMove de la instrucción switch. En caso de que un círculo no haya sido tocado con anterioridad, la matriz _activeCircles estará vacía y activeCircle = _activeCircles[activeCircleIndex] será null. En este caso, saltamos directo a la instrucción break y salimos de la instrucción switch.

Si, por otro lado, activeCircle no es null (es decir, hay un círculo activo que se debe mover), convertimos las coordenadas del dispositivo señalizador (evt.clientX, evt.clientY), que son relativas a la ventanilla del explorador, al sistema de coordenadas SVG de 400 por 400. Esto se lleva a cabo a través de la matriz de transformación de coordenadas (CTM). Para obtener más información, consulta Transformaciones de coordenadas de SVG.

Por último, movemos el centro del círculo (cx, cy) a los valores transformados de las coordenadas:


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

Evento Up

Si un evento up se activa en un círculo, el flujo de control se deriva a la cláusula mouseup/MSPointerUp de la instrucción switch. El círculo que se ha tocado o en el que se ha hecho clic, indicado por activeCircleIndex, se quita de la lista de círculos activos y, si corresponde, la solicitud de eventos de captura se libera a través de msRelasePointerCapture.

Movimiento de dos círculos

Como en el caso de un solo círculo, hay tres eventos para controlar: down, move y up.

Eventos down

En este caso, fluyen dos objetos de eventos down de círculo (casi simultáneos) a la cláusula mousedown/MSPointerDown de switch. La matriz _activeCircles ahora contiene dos objetos círculo (cuyos índices que tienen acceso a ellos son los valores evt.pointerId asociados).

Eventos move

Mientras los dos objetos de evento move de la ventana (casi simultáneos) fluyen a la cláusula mousemove/MSPointerMove de switch, cada círculo se mueve de a uno por vez, como en el caso de un solo círculo.

Eventos Up

Mientras cada objeto de evento up de círculo (casi simultáneo) fluye a la cláusula mouseup/MSPointerUp, cada uno se limpia de a uno por vez, como en el caso de un solo círculo.

Rompecabezas con imágenes

Ahora ya contamos con todas las piezas esenciales necesarias para crear un juego de rompecabezas con imágenes para pantallas táctiles (aunque sin ser necesariamente cautivador) totalmente funcional. Dividiremos este análisis extenso en varios componentes fáciles de comprender. Comenzaremos con el ejemplo 6, la estructura de partida para el juego (el juego completo se muestra en el ejemplo 7 y se analiza más adelante).

Estructura de partida

A diferencia del juego presentado en Animación SVG avanzada, la estructura de partida para el rompecabezas es relativamente simple, como se muestra en el ejemplo 6 (haz clic con el botón secundario para ver el origen). El marcado principal del ejemplo 6 es el siguiente:


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

El elemento <rect> se usa para que el color de toda la ventanilla de SVG sea negro (sin importar su tamaño actual). Esto proporciona un "campo de juego" para las piezas en mosaico del rompecabezas. Este efecto puede verse en la siguiente captura de pantalla del ejemplo 6:

Captura de pantalla que muestra el "campo de juego" para las piezas en mosaico del rompecabezas

El resto de la estructura de partida para el ejemplo 6 consiste en el siguiente JavaScript:


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

Tal como se describe en la sección de detección de características anterior, si addEventListener está disponible, desencadenamos la función main. A continuación, inicializamos una matriz, imageList, que contiene las rutas a las imágenes que se usarán en el juego (es decir, las imágenes que se dividirán en mosaicos, se seleccionarán aleatoriamente y resolverán). Si el usuario llega al séptimo nivel, se reciclará la primera imagen (puzzle0.png) en forma infinita.

A continuación, invocamos la función constructora Game. El primer parámetro, 2, da instrucciones al constructor para que genere un objeto game que tiene dos columnas y dos filas. El último parámetro es, por supuesto, la lista de imágenes de rompecabezas para recorrer (si es necesario).

Dentro del constructor, colocamos todas sus variables "globales" en una variable gameData práctica. Si no estás familiarizado con la palabra clave this, la instrucción this.hasCanvas = !!game.elements.canvas.getContext crea y establece una propiedad denominada hasCanvas en el objeto que el constructor crea (la variable game). La doble negación (!!) simplemente fuerza que la expresión game.elements.canvas.getContext tenga un valor booleano (true si se admite canvas).

De manera similar, this.init = function() { … } define un método, llamado init, para todos los objetos creados por el constructor (solo hay un objeto de ese tipo, game). Al invocar game.init(), entre otras cosas, se inicia el juego.

Juego de rompecabezas con imágenes multitáctil

Ahora estamos en condiciones de combinar toda la información anterior en un juego de rompecabezas con imágenes multitáctil totalmente funcional, como se muestra en el ejemplo 7. El código fuente del ejemplo 7 ha sido bien comentado y deberías estar familiarizado con él, pero los dos componentes a continuación estarán más claros con una explicación adicional:

Organización aleatoria de los mosaicos

En la función createAndAppendTiles, generamos los objetos de imagen de mosaico de SVG (como en el ejemplo 4). Pero antes de anexarlos al elemento SVG, los organizamos aleatoriamente y nos aseguramos de que el diseño aleatorio resultante no coincida exactamente con la imagen original (completa) del rompecabezas (que solo puede ser un problema en el primer nivel, de cuatro mosaicos):


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 que sea fácil organizar aleatoriamente los mosaicos, colocamos sus pares de coordenadas asociados en una matriz (coordinatePairs) y usamos el método de ordenación de la matriz de JavaScript de la siguiente manera:



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

Tal como se describe en la sección sobre el método de ordenación (JavaScript) y dado que Math.random() devuelve un valor entre 0 y 1, esta función de ordenación anónima ordena aleatoriamente los elementos de la matriz.

Cláusula pointer up extendida

Las cláusulas down y move con la instrucción switch son casi idénticas a los ejemplos anteriores. Sin embargo, la cláusula up es significativamente extendida:



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;

El primer componente para analizar de la cláusula up es el ajuste de mosaicos. Para muchos juegos de rompecabezas (incluido este), es necesario que una pieza (mosaico) de rompecabezas en vuelo se ajuste en su lugar cuando se mueve apropiadamente cerca de su ubicación correcta. El código usado para hacerlo, copiado del ejemplo anterior, se muestra a continuación:


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

En este fragmento de código, obtenemos la posición del mosaico activo (cuando se activa el evento up) e iteramos a través de todos los mosaicos para determinar las posiciones correctas para todos ellos. Si la posición del mosaico activo está "lo suficientemente cerca" a una de las posiciones correctas, lo ajustamos en su posición e inmediatamente se rompe el bucle for (ya que no es necesario buscar otra posición correcta). La cláusula if metafóricamente dibuja un pequeño cuadro de detección de colisiones alrededor del centro de la posición correcta y, si la posición del mosaico cae dentro de él, la cláusula pasa a ser true y ajusta el mosaico en su lugar a través de las siguientes dos llamadas de método setAttribute:


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

Ten cuidado que cuando aumenta gameData.snapDelta, también aumenta el tamaño del cuadro de detección de colisiones, lo que hace que el ajuste de mosaicos sea menos sensible.

A continuación, si el juego está en curso actualmente, comprobamos si la ubicación del mosaico activo fue la definitiva y la correcta al iterar a través de todos los mosaicos y comprobando mediante fuerza   bruta :


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

Si no todos los mosaicos están en las posiciones correctas, inmediatamente salimos de handlePointerEvents y esperamos a que el siguiente evento de puntero active handlePointerEvents. De lo contrario, el usuario habrá resuelto el rompecabezas y se ejecutará el siguiente código:


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 se ha ganado este nivel del juego, establecemos gameData.inProgress en false y mostramos la nueva puntuación. Debido a que el tamaño actual (la cantidad de filas y columnas) del juego también se usa para indicar el nivel actual del juego, (es decir, cuántos rompecabezas ha resuelto el usuario hasta aquí) y como la dificultad del juego es proporcional al cuadrado de la cantidad de filas (o la cantidad de columnas, ya que es lo mismo), la puntuación aumenta por el cuadrado del tamaño actual del juego:


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

Después incrementamos el proxy del nivel del juego, gameData.size, y mostramos un mensaje de felicitación aleatorio de una matriz de frases posible del tipo "has ganado":


++(gameData.size);

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

Finalmente, quitamos todo elemento de imagen SVG (mosaico) preexistente adjuntado al elemento SVG apara prepararnos para la próxima ronda de elementos de imagen SVG (diferentes) que se crearán y anexarán al elemento SVG a través de createAndAppendTiles:


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

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

 

El objetivo aquí fue mostrarte, a través de este tutorial, cómo controlar eventos multitáctil en un escenario razonablemente realista (la implementación de un juego de rompecabezas para pantallas táctiles). La guía de esta sección debería proporcionarte el conocimiento necesario para controlar eventos de toque en varias circunstancias (posiblemente incluidas aquellas que involucran canvas y SVG).

Temas relacionados

Galería de fotos Contoso Images
Eventos de gesto
Instrucciones para crear sitios para pantallas táctiles
Cómo simular el estado mantener en dispositivos táctiles
Gráficos de HTML5
Ejemplos y tutoriales de Internet Explorer
Eventos de puntero
Actualizaciones de eventos de puntero

 

 

Mostrar:
© 2014 Microsoft