Exporter (0) Imprimer
Développer tout

Comment utiliser une zone de dessin, SVG et des interactions tactiles multipoints pour créer un jeu de puzzle en mosaïque

En commençant par les notions de base et en concluant par un jeu tactile multipoint représentant un puzzle illustré, qui s’appuie aussi bien sur une zone de dessin que sur SVG, ce didacticiel décrit, de façon progressive, comment gérer les événements de pointeur tels que ceux générés par l’usage d’une souris, d’un stylet ou d’un ou plusieurs doigts (interactions tactiles multipoint).

Remarque  Les événements de pointeur nécessitent Windows 8 ou version ultérieure.

Remarque  La mise en œuvre des événements de pointeur d’Internet Explorer a légèrement changé depuis la rédaction de cet article pour Internet Explorer 10. Pour plus d’informations sur la mise à jour et la pérennisation de votre code, voir Mises à jour des événements de pointeur.

Introduction

Dans Internet Explorer 10 et les applications du Windows Store en JavaScript, les développeurs peuvent utiliser un type d’entrée appelé pointeur. Un pointeur, dans le contexte qui nous intéresse, peut représenter n’importe quel point de contact à l’écran, effectué par une souris, un stylet, avec un doigt ou avec plusieurs doigts. Ce didacticiel décrit tout d’abord comment vous familiariser avec les pointeurs, puis détaille l’implémentation d’un jeu de puzzle illustré à pointeur multiple, qui s’appuie aussi bien sur l’élément de zone de dessin que SVG :

Puzzle tactile multipoint utilisant l’élément Canvas et SVG

Si vous savez déjà comment utiliser les événements de souris, les événements de pointeur doivent alors vous paraître familiers : MSPointerDown, MSPointerMove, MSPointerUp, MSPointerOver, MSPointerOut et ainsi de suite. La manipulation des événements de souris et de pointeur est très simple, comme le montre l’exemple 1 qui suit. De plus, pour les périphériques tactiles multipoint, l’exemple 1 fonctionne en l’état, avec autant de points de contact (doigts) simultanés que le périphérique est capable de gérer. Les événements de pointeur se déclenchent pour chaque point du contact sur l’écran. Dès lors, les applications, comme celles de l’exemple de dessin élémentaire suivant, prennent en charge les interactions tactiles multipoint sans codage particulier :

Exemple 1 : application de dessin élémentaire


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

Le navigateur exploite en général des événements tactiles pour ses propres besoins. Par exemple, pour faire défiler une page Web vers le haut, l’utilisateur peut toucher l’écran (et non un lien) et faire glisser le doigt vers le bas. Pour agrandir une page, vous pouvez aussi utiliser le geste du pincement écarté (en éloignant deux doigts en contact sur l’écran tactile). Dans l’exemple 1, nous ne voulons pas que ces comportements par défaut se produisent, sinon le dessin à l’aide d’un doigt (ou plusieurs doigts) entraînerait à la place un défilement latéral (ou éventuellement un zoom) de la page. Pour permettre à ces événements de s’insérer dans votre code JavaScript, nous utilisons la feuille CSS suivante :


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

Cela indique au navigateur d’« ignorer » toutes les actions tactiles sauf le double appui (qui permet de zoomer sur une page). En d’autres termes, tous les événements tactiles sont mis à disposition de votre code JavaScript, sauf la capture de l’événement du double appui. Les autres valeurs possibles de -ms-touch-action sont auto, none, manipulation et inherit. Elles sont décrites dans le billet « Recommandations en matière de génération de sites tactiles » du blog IEBlog.

Du point de vue d’addEventListener, il est important de noter que les événements de pointeur de Windows Internet Explorer et les événements de souris traditionnels s’excluent mutuellement : si les événements de pointeur sont disponibles, ils englobent alors les événements de souris. En d’autres termes, vous ne pouvez pas inscrire le détecteur d’événements paintCanvas avec mousemove et MSPointerMove simultanément :



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

Si les événements de pointeur Microsoft sont en fait disponibles, lesquels font également état des événements de souris, il vous suffit de les utiliser. Dans le cas contraire, nous devons utiliser les événements de souris traditionnels :


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

L’extrait de code précédent permet donc à l’application de dessin de fonctionner avec les périphériques tactiles comme avec les périphériques traditionnels (non tactiles).

Ces notions de base sur les événements de pointeur maintenant acquises, nous pouvons passer à un usage plus réaliste, à savoir l’implémentation d’un jeu de puzzle illustré :

Puzzle tactile multipoint utilisant l’élément Canvas et SVG

Scission d’une image

La première étape de la création d’un tel jeu consiste à créer les morceaux ou les vignettes de l’image. La méthode drawImagede l’API canvas vous permet de découper facilement une image source sur une zone de dessin, en définissant des parties de l’image à copier ou à afficher. La syntaxe est drawImage(image, i_x, i_y, i_width, i_eight, c_x, c_y, c_width, c_height)Les deux illustrations suivantes montrent comment des parties d’un élément <img> sont sélectionnées et affichées sur une zone de dessin :

Image illustrant la possibilité de scinder ou de découper l’image en morceaux ou en vignettes

Image source

Image illustrant la possibilité de scinder ou de découper l’image en morceaux ou en vignettes

Zone de dessin

En séparant l’image source du puzzle en une série de lignes et de colonnes (un tableau), la méthode drawImage peut alors être appliquée à chaque cellule du tableau, créant ainsi les différentes vignettes d’images nécessaires :

Assemblage de toutes les vignettes pour créer un ensemble

Ce processus de génération des vignettes est démontré dans l’exemple 2. Pour afficher le code source de l’exemple 2, cliquez avec le bouton droit dans la page de l’exemple 2, puis sélectionnez Afficher la source. La discussion de l’exemple 2 se répartit en deux sections :

Balise meta X-UA-Compatible

L’exemple 2 a été développé à travers un intranet local avec Internet Explorer. La balise <meta http-equiv="X-UA-Compatible" content="IE=10"> a donc été utilisée pour veiller à ce qu’Internet Explorer soit placé dans les bons navigateur et mode de document. Pour plus d’informations, voir Définition de la compatibilité des documents. En règle générale, cette balise doit être retirée juste avant que la page ne soit publiée en production.

Découpage de l’image

Pour convertir l’image du puzzle de 400 par 400 en morceaux (ou vignettes) utiles, nous devons d’abord créer un objet d’image en mémoire représentant l’image à découper (400x400.png) et appeler une fonction anonyme une fois l’image entièrement chargée :


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

D’après la taille de l’image source (à l’aide des propriétés image.width et image.height) et du nombre de lignes et colonnes voulu pour découper l’image (par le biais des variables NUM_COLS et NUM_ROWS), nous calculons la taille de vignette nécessaire dans la fonction anonyme :


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

Nous définissons ensuite la largeur et la hauteur de la zone de dessin sur la taille d’une vignette, car nous comptons créer toutes les vignettes d’après cette zone de dessin. Les différentes images des vignettes sont créées comme suit :


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

Pour mieux comprendre cette double boucle for imbriquée, imaginons que nous cherchions à obtenir un puzzle de 5 lignes sur 5 colonnes, et que les variables row et col soient respectivement de valeur 2 et 3. En d’autres termes, le point actif se trouve dans la cellule (2, 3) du tableau de l’image source :

Puzzle de 5 lignes sur 5 colonnes avec les coordonnées de cellule de tableau (2, 3) sélectionnées

Si la taille de l’image source est de 400 x 400 pixels, nous devons écrire le code :


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

Nous obtenons donc :


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)

En d’autres termes, nous prenons un instantané de 80 par 80 de l’image source à la position (240, 160) :

Capture du puzzle indiquant les coordonnées de la cellule sélectionnée de la grille (240, 160)

Placez ensuite l’instantané dans le coin supérieur gauche d’une zone de dessin de 80 par 80 pixels :

Instantané de la cellule aux coordonnées (240, 160) exclusives dans le tableau

La zone de dessin est ensuite convertie en une image à URL de données, puis stockée dans un tableau d’images découpées, comme indiqué ci-dessous :


slicedImageTable.push( _canvas.toDataURL() );

Le reste de l’exemple 2 permet de s’assurer que l’image du puzzle est correctement découpée, en affichant les différentes vignettes dans le même ordre que celui où elles ont été acquises (découpées).

Conversion des sections d’image au format SVG

Maintenant que nous pouvons générer des sections d’image (c’est-à-dire des vignettes), nous allons étudier à présent comment convertir ces vignettes à URL de données en objets d’image SVG. Ce processus est démontré dans l’exemple 3. Pour afficher la sortie que l’exemple 3 génère, la fenêtre de la console de débogage de votre navigateur doit s’ouvrir. Pour plus d’informations, voir Comment utiliser les outils de développement F12 pour déboguer vos pages Web. La différence principale entre l’exemple 2 et l’exemple 3 réside dans la création et la définition des éléments d’image SVG, comme indiqué ci-dessous :


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

SVG étant une forme de code XML, vous devez indiquer un espace de noms pendant la création d’un élément SVG (au moins en dehors du modèle objet de SVG) :


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

Les éléments d’image SVG s’appuient sur un attribut href et non sur l’attribut src (qui sert à l’élément non-SVG <img>). Sachez aussi que, pour Internet Explorer, vous pouvez utiliser svgImage.setAttribute('href', _canvas.toDataURL()) pour définir l’attribut href des éléments d’image SVG. D’autres navigateurs peuvent cependant avoir besoin de la syntaxe XLink. C’est la raison pour laquelle nous utiliserons plutôt l’instruction suivante :


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

Les propriétés width et height par défaut d’une image SVG ont la valeur 0. Nous devons dès lors définir leur valeur de façon explicite :


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

Enfin, en tenant compte du système de coordonnées associé à la fenêtre d’affichage SVG (voir Transformations de coordonnées SVG), nous pouvons créer et définir deux propriétés personnalisées, correctX et correctY, afin d’enregistrer l’emplacement prévu de chaque vignette dans le puzzle une fois terminé (une fois la partie gagnée).

Affichage d’images SVG

Maintenant que nous avons placé les images SVG stockées dans le tableau de vignettes, notre tâche suivante consiste à les afficher à l’écran. Pour simplifier, nous ajoutons un élément SVG <svg> à la page Web en plus de quelques autres améliorations que nous aborderons dans les trois sections suivantes et comme le montre l’exemple 4 (cliquez dessus avec le bouton droit pour afficher la source).

SVG fluide

En plus d’ajouter simplement un élément SVG, nous utilisons aussi quelques propriétés CSS pour rendre la fenêtre d’affichage SVG entièrement fluide. Le premier élément à prendre en compte est l’élément SVG même :


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

Ici, la taille de la fenêtre d’affichage SVG carrée représente 75 % des dimensions les plus petites de la fenêtre d’affichage du navigateur. Un système de coordonnées de 400 par 400 unités lui est appliqué. Pour que le SVG fluide fonctionne comme prévu, assurez-vous que les règles CSS suivantes sont appliquées :

  • Les éléments html et body ont besoin d’une propriété height de valeur 100% :
    
    html, body {
      margin: 0;
      padding: 0;
      height: 100%
    }
    
    

    Au fur et à mesure que la fenêtre d’affichage du navigateur est réduite, le contenu de la fenêtre d’affichage SVG l’est désormais aussi. Notez que le système de coordonnées inhérent de la fenêtre d’affichage SVG de 400 par 400 unités reste intact : seule la taille des unités de coordonnées change.

  • Tout comme l’élément <img>, l’élément SVG est un élément incorporé. Pour le centrer dans la fenêtre d’affichage du navigateur, nous devons alors attribuer à sa propriété display la valeur block, et à ses propriétés de marge left et right la valeur auto :
    
    svg {
      display: block; 
      margin: 0 auto;
    }
    
    

Détection de fonctionnalités

Puisque nous utilisons addEventListener, une zone de dessin et SVG, détectons la présence de ces fonctionnalités avant d’essayer d’afficher un puzzle. L’exemple 4 illustre le code nécessaire ci-dessous :


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

Comme vous pouvez le voir, l’exécution n’entre dans la fonction main que si la méthode addEventListener est prise en charge (d’ailleurs, si addEventListener ne l’est pas, il est extrêmement improbable que l’élément Canvas ou SVG le soit).

Dans la fonction main, nous créons une variable game destinée à contenir toutes les variables « globales » et l’état de notre jeu. Comme vous pouvez le voir, si le navigateur de l’utilisateur ne prend pas en charge toutes les fonctionnalités nécessaires, un message s’affiche pour identifier le problème.

Quand nous sommes sûrs que les fonctionnalités requises sont prises en charge, nous pouvons continuer avec exactement la même approche que celle de l’exemple 3, à l’exception de certaines variables dont le nom change, par exemple NUM_ROWS en game.numRows et slicedImageTable en game.tiles. Une fois tous les éléments (vignettes) de notre image SVG créés dans la double boucle for imbriquée, nous pouvons les afficher, comme il est abordé ci-après.

Affichage des vignettes

La position de chaque vignette ainsi calculée et définie, nous pouvons afficher les images des vignettes SVG en les ajoutant simplement à l’élément SVG :


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

Pour vérifier la fluidité des vignettes SVG, modifiez la taille de la fenêtre du navigateur. Vous remarquerez probablement que la fenêtre d’affichage SVG garde toujours ses dimensions à 75 % des dimensions les plus petites (en largeur ou en hauteur) de la fenêtre d’affichage du navigateur.

Déplacement des vignettes

Maintenant que les vignettes de notre puzzle sont affichées, notre objectif est de permettre à l’utilisateur de les déplacer à l’aide d’un périphérique de pointage. Nous allons regarder ce problème de plus près en examinant tout d’abord comment déplacer trois cercles SVG, tel qu’illustré dans l’exemple 5 (cliquez avec le bouton droit pour afficher la source).

Nous devons en premier lieu créer un tableau global destiné à contenir les cercles actifs, c’est-à-dire lesquels ont été « cliqués » selon que l’événement mousedown ou l’événement MSPointerDown est déclenché :


var _activeCircles = [];

Pour expliquer un peu mieux l’extrait de code suivant, si un gestionnaire d’événement mousemove est attaché directement aux cercles SVG (ce qui semble le plus logique), l’utilisateur risque de bouger la souris trop rapidement et peut alors « perdre » un cercle, c’est-à-dire que le mouvement du cercle SVG n’arrive alors pas à suivre les mouvements très rapides de la souris. En supposant que le gestionnaire d’événement mousemove est responsable du déplacement d’un cercle à l’écran, dès lors que la souris et le cercle sont physiquement séparés, le gestionnaire d’événement mousemove interrompt son exécution pour arrêter le mouvement du cercle. C’est pour cela que nous attachons le gestionnaire d’événement mousemove à l’objet window plutôt qu’aux cercles SVG : indépendamment de la rapidité à laquelle l’utilisateur déplace la souris, il ne peut jamais perdre l’objet window omniprésent :


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

Tel qu’illustré dans l’extrait de code, nous inscrivons un gestionnaire d’événement de pointeur MSPointerMove s’il est disponible, lequel gère aussi les événements de souris traditionnels. Dans le cas contraire, nous inscrivons un gestionnaire d’événement mousemove. N’oubliez pas que vous ne pouvez pas inscrire les deux types d’événements (MSPointerMove et mousemove) sur le même objet (comme décrit dans l’exemple 1).

Ensuite, nous devons inscrire les gestionnaires d’événements de pointeur maintenu appuyé et de pointeur levé sur chaque élément de cercle 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

Regardez la fonction handlePointerEvents d’un peu plus près : nous allons en reparler juste après.


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

Pour décrire handlePointerEvents un peu mieux, nous allons étudier deux scénarios : le déplacement d’un simple cercle et le déplacement simultané de deux cercles.

Mouvement d’un seul cercle

Trois événements sont à gérer : down (maintenu appuyé), move (déplacement) et up (levé).

Événement down

Quand l’utilisateur touche un seul cercle (à l’aide de la souris, au stylet ou au doigt), l’événement down se déclenche, ce qui entraîne l’appel à handlePointerEvents. Si les événements de pointeur sont pris en charge, evt.pointerId prend une valeur non nulle et activeCircleIndex est égal à evt.pointerId. Dans le cas contraire, activeCircleIndex prend la valeur 0 (cela se traduit par l’entité evt.pointerId || 0). Si evt.pointerId correspond à null, un seul cercle peut être actif à la fois, plus précisément _activeCircles[0], ce qui est l’unique possibilité sachant qu’une souris est le seul périphérique de pointage autorisé.

Ensuite, l’instruction switch vérifie evt.type et passe le contrôle à la clause mousedown/MSPointerDown. Pour s’assurer que le cercle actif se trouve toujours devant tous les autres, nous le supprimons puis l’ajoutons simplement au DOM (le dernier élément ajouté est toujours le dernier élément/celui le plus à l’avant-plan à être affiché).

Si evt.pointerId est défini, nous pouvons ensuite appeler msSetPointerCapture sur evt.target (c’est-à-dire, le cercle actif) afin que le cercle puisse continuer à recevoir tous les événements inscrits. Cela permet au cercle d’être physiquement extrait puis replacé dans la fenêtre d’affichage du navigateur.

Enfin, nous pouvons enregistrer le cercle touché dans la liste des cercles actifs :


_activeCircles[activeCircleIndex] = evt.target;

Événement move

Si le périphérique de pointage est déplacé (sur l’objet window), le gestionnaire d’événement mousemove/MSPointerMove (handlePointerEvents) s’exécute, lequel passe ensuite le contrôle à la clause mousemove/MSPointerMove de l’instruction switch. Au cas où un cercle n’a pas encore été touché, le tableau _activeCircles est alors vide et activeCircle = _activeCircles[activeCircleIndex] a la valeur null. Dans ce cas, nous pouvons passer directement à l’instruction break et l’exécution peut quitter l’instruction switch.

Si, au contraire, activeCircle n’a pas la valeur null (c’est-à-dire qu’un cercle actif doit être déplacé), nous convertissons les coordonnées du périphérique de pointage (evt.clientX, evt.clientY), qui sont relatives à la fenêtre d’affichage du navigateur, au système de coordonnées SVG de 400 par 400. Pour ce faire, nous utilisons la matrice de transformation des coordonnées (CTM, Coordinate Transformation Matrix). Pour plus d’informations, voir Transformations de coordonnées SVG.

Enfin, nous déplaçons le centre du cercle (cx, cy) aux valeurs de coordonnées transformées :


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

Événement up

Si un événement up se déclenche sur un cercle, le contrôle passe alors à la clause mouseup/MSPointerUp de l’instruction switch. Le cercle touché/cliqué, indiqué par activeCircleIndex, est supprimé de la liste des cercles actifs et, au besoin, sa demande d’événements de capture est envoyée par le biais de msRelasePointerCapture.

Mouvement double de cercle

Comme pour le cercle simple, il existe trois événements à gérer : down (maintenu appuyé), move (déplacement) et up (levé).

Événements down

Dans ce cas, deux objets événements down (pratiquement simultanés) de cercles passent par la clause mousedown/MSPointerDown de l’instruction switch. Le tableau _activeCircles contient désormais deux objets de cercles (dont les index qui les accèdent correspondent aux valeurs evt.pointerId associées).

Événements move

Sachant que les deux objets événements move (pratiquement simultanément) de la fenêtre passent par la clause mousemove/MSPointerMove de l’instruction switch, chaque cercle est déplacé tour à tour, tout comme pour le cas du cercle unique.

Événements up

Sachant que tous les objets événements up (pratiquement simultanément) des cercles passent par la clause mouseup/MSPointerUp, ils sont alors nettoyés tour à tour, comme pour la technique du cercle unique.

Puzzle illustré

Nous avons désormais toutes les pièces essentielles à la création d’un jeu de puzzle illustré tactile entièrement fonctionnel (mais pas nécessairement captivant). Pour appréhender mieux cette discussion particulièrement longue, nous allons la décomposer en plusieurs composants plus facilement assimilables. Nous allons commencer par l’exemple 6, le squelette du jeu (le jeu complet est présenté dans l’exemple 7 et est traité plus en détails par la suite).

Squelette

Contrairement au jeu présenté dans Animation SVG avancée, le squelette du puzzle est relativement simple, comme le montre l’exemple 6 (cliquez avec le bouton droit pour afficher la source). Le code principal des balises de l’exemple 6 se présente comme suit :


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

L’élément <rect> est utilisé pour colorier l’intégralité de la fenêtre d’affichage SVG en noir (quelle que soit sa taille). Cela offre un « champ de jeu » pour les morceaux des vignettes du puzzle. Cet effet se retrouve dans la capture d’écran suivante de l’exemple 6 :

Squelette du « terrain de jeu » pour les pièces du puzzle

Le reste du squelette de l’exemple 6 est constitué du code JavaScript suivant :


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

Comme indiqué dans la section précédente sur la détection de fonctionnalités, si addEventListener est disponible, nous devons exécuter la fonction main. Nous initialisons ensuite un tableau appelé imageList qui renferme les chemins d’accès aux images à utiliser dans le jeu (c’est-à-dire celles à scinder en vignettes disposées de façon aléatoire puis résolues). Si l’utilisateur réussit à atteindre le septième niveau, la première image (puzzle0.png) est alors recyclée, et ainsi indéfiniment.

Nous appelons ensuite la fonction constructeur de Game. Le premier paramètre, 2, indique au constructeur de générer un objet game qui possède deux colonnes et deux lignes. Le dernier paramètre correspond bien sûr à la liste des images du puzzle que le code sélectionne à tour de rôle (si nécessaire).

Dans le constructeur, nous plaçons toutes ses variables « globales » dans une variable gameData plus pratique. Si le mot clé this vous est peu familier, sachez que l’instruction this.hasCanvas = !!game.elements.canvas.getContext crée et définit une propriété, appelée hasCanvas, sur l’objet que le constructeur construit (la variable game). La double négation (!!) force simplement l’expression game.elements.canvas.getContext à recevoir une valeur booléenne (true si l’élément Canvas est pris en charge).

De même, this.init = function() { … } définit une méthode appelée init pour tous les objets (dans notre cas, l’objet game uniquement) que le constructeur crée. L’appel de game.init() lance le jeu, entre autres opérations.

Jeu tactile multipoint représentant un puzzle illustré

Nous voici maintenant au point de combiner toutes les informations précédentes en un jeu tactile multipoint représentant un puzzle illustré entièrement opérationnel, comme nous le révèle l’exemple 7. Le code source associé à l’exemple 7 doit ressembler à celui que nous reprenons ici et qui est amplement commenté. Les deux composants suivants peuvent néanmoins gagner en clarté avec quelques explications supplémentaires :

Randomisation des vignettes

Dans la fonction createAndAppendTiles, nous générons les objets des images des vignettes SVG (comme dans l’exemple 4). Mais avant de les ajouter à l’élément SVG, nous les générons de façon aléatoire, puis nous nous assurons que le modèle ainsi obtenu ne coïncide pas exactement avec l’image originale (complète) du puzzle, ce qui n’est vraiment problématique qu’au premier niveau avec seulement 4 vignettes :


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

Pour générer facilement les vignettes de façon aléatoire, nous devons placer leurs coordonnées associées par paires dans un tableau (coordinatePairs) et faire appel à la méthode JavaScript de tri de tableau comme suit :



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

Comme indiqué dans la section Méthode sort (JavaScript), et sachant que Math.random() retourne une valeur comprise entre 0 et 1, cette fonction anonyme trie de façon aléatoire les éléments du tableau.

Clause étendue du pointeur levé

Les clauses move et down incluses dans l’instruction switch ressemblent pratiquement aux exemples antérieurs. La clause up cependant est significativement étendue :



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;

Le premier composant de la clause du pointeur levé à aborder est l’ancrage des vignettes. Pour de nombreux jeux de puzzle (y compris celui-ci), il est nécessaire qu’un morceau (vignette) du puzzle en cours d’exécution s’ancre à son emplacement s’il est déplacé relativement près de sa position correcte. Le code permettant cela est repris ici, tel que copié de l’exemple antérieur :


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

Dans cet extrait de code, nous obtenons la position de la vignette active (quand l’événement up se déclenche) et parcourons toutes les vignettes pour déterminer les bonnes positions pour une vignette quelconque. Si la position de la vignette active est « suffisamment proche » de l’une de ces positions correctes, nous l’ancrons à sa place et annulons immédiatement l’exécution de la boucle for (en ce sens qu’il n’est plus nécessaire de rechercher l’une des autres positions correctes). La clause if délimite un petit rectangle de détection des collisions autour du centre de la position correcte et, si la position de la vignette active coïncide, la clause prend la valeur true et ancre la vignette à sa place à travers les deux appels de méthode setAttribute suivants :


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

Sachez que gameData.snapDelta est augmenté, ainsi que la taille de la zone de détection des collisions, rendant ainsi l’ancrage des vignettes moins sensible.

Ensuite, pendant l’exécution du jeu, nous vérifions si le placement de la vignette active correspond à l’emplacement final correct en analysant toutes les vignettes pour savoir si elles sont bien placées :


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 les vignettes ne se trouvent pas à leur position correcte, l’exécution sort immédiatement de handlePointerEvents et attend que l’événement de pointeur suivant déclenche handlePointerEvents. Dans le cas contraire, l’utilisateur a résolu le puzzle et le code suivant peut alors s’exécuter :


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

Sachant que ce niveau du jeu a été gagné, nous attribuons à gameData.inProgress la valeur false et affichons le nouveau score. Sachant que la taille active (le nombre de lignes et de colonnes) du jeu sert aussi à indiquer le niveau du jeu (c’est-à-dire le nombre de puzzles que l’utilisateur a résolu jusqu’à présent) et dans la mesure où la difficulté du jeu est proportionnelle au carré de son nombre de lignes (ou son nombre de colonnes, puisqu’ils sont identiques), le score se voit augmenté du carré de la taille active du jeu :


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

Nous incrémentons ensuite le proxy gameData.size du niveau de notre jeu et affichons un message aléatoire de félicitation repris d’un tableau de phrases possibles du style « vous avez gagné » :


++(gameData.size);

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

Enfin, nous supprimons tout autre élément d’image SVG préexistant (vignette) attaché à l’élément SVG pour préparer le niveau suivant avec des éléments d’images SVG (différents) à créer et à ajouter à l’élément SVG par le biais de la fonction createAndAppendTiles :


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

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

 

Notre objectif a été, à travers ce didacticiel, de vous montrer comment gérer les événements d’interactions tactiles multipoint dans un scénario relativement réaliste, à savoir l’implémentation d’un jeu de puzzle tactile. Ces instructions doivent pouvoir vous offrir des connaissances suffisantes pour gérer les événements tactiles dans un certain nombre de cas (y compris probablement ceux impliquant une zone de dessin et SVG).

Rubriques connexes

Galeries de photos Contoso Images
Événements de mouvement
Recommandations en matière de génération de sites tactiles
Comment simuler le pointage sur des appareils tactiles
Graphiques HTML5
Exemples et didacticiels Internet Explorer
Événements de pointeur
Mises à jour des événements de pointeur

 

 

Afficher:
© 2014 Microsoft