Come usare Canvas, SVG e multitocco per creare un gioco di tipo puzzle in sezioni

Partendo dalle nozioni di base fino ad arrivare a realizzare un gioco di tipo puzzle a immagini multitocco che usa sia Canvas che SVG, in questa esercitazione viene spiegato passo dopo passo come gestire eventi del puntatore, ad esempio quelli generati da un mouse, una penna o da un dito o più dita (multitocco).

Nota  Per gli eventi puntatore è necessario Windows 8 o versione successiva.
Nota  L'implementazione degli eventi puntatore di Internet Explorer è sensibilmente cambiata da quando è stato scritto questo articolo per Internet Explorer 10. Per informazioni su come aggiornare il codice e assicurarne la compatibilità con le versioni future, vedi Aggiornamenti degli eventi puntatore.

Introduzione

In Internet Explorer 10 e nelle app di Windows Store scritte con JavaScript gli sviluppatori possono usare un tipo di input detto puntatore. In tale contesto un puntatore può essere costituito da qualsiasi punto di contatto sullo schermo, creato con il mouse, la penna o con uno o più dita. Questa esercitazione spiega innanzitutto come iniziare a usare i puntatori e quindi illustra dettagliatamente l'implementazione di un gioco di tipo puzzle a immagini con più puntatori, che usa sia Canvas che SVG:

Immagine di un puzzle abilitato per il multitocco che usa Canvas e SVG

Se sai già come usare gli eventi del mouse, non dovresti avere difficoltà con eventi del puntatore quali MSPointerDown, MSPointerMove, MSPointerUp, MSPointerOver, MSPointerOut e così via. Gestire gli eventi del mouse e del puntatore è davvero semplice, come illustrato nell'esempio 1 più avanti. Per i dispositivi abilitati per il multitocco poi puoi usare l'esempio 1 senza modifiche con tutti i punti di contatto simultanei (dita) che il dispositivo è in grado di gestire, perché gli eventi del puntatore vengono attivati per ogni punto di contatto sullo schermo. Allo stesso modo le app, come quella dell'esempio di disegno elementare seguente, supportano il multitocco senza aggiungere codice speciale:

Esempio 1 - App per disegno elementare


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

Il browser usa in genere gli eventi di tocco per le proprie finalità. Ad esempio per scorrere una pagina Web verso l'alto, l'utente può toccare lo schermo (non su un link) e tirare verso il basso. Per applicare lo zoom avanti in una pagina si possono allargare due dita. Nell'esempio 1 non vogliamo applicare questi due comportamenti predefiniti, altrimenti creando un disegno con un dito o con più dita alla pagina verrebbe applicata la panoramica o lo zoom. Per consentire la trasmissione di questi eventi al codice JavaScript, useremo il codice CSS seguente:


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

Questo codice indica al browser di "ignorare" tutte le azioni di tocco ad eccezione del doppio tocco per applicare lo zoom avanti a una pagina. In altre parole tutti gli eventi di tocco sono ora disponibili nel codice JavaScript, ma la funzionalità di acquisizione dell'evento di doppio tocco è disattivata. Gli altri valori possibili per -ms-touch-action sono auto, none, manipulation e inherit, come descritto nelle linee guida per la creazione di siti sensibili al tocco.

Dal punto di vista di addEventListener è importante notare che gli eventi del puntatore di Windows Internet Explorer e i normali eventi del mouse si escludono a vicenda e che quindi, se disponibili, gli eventi del puntatore raggruppano anche quelli del mouse. In altre parole non puoi registrare simultaneamente il listener di eventi paintCanvas sia con mousemove che con MSPointerMove:



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

Useremo invece, se disponibili, eventi del puntatore MS, che segnalano anche eventi del mouse. Diversamente useremo i normali eventi del mouse:


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

Il frammento di codice precedente consente di usare l'app di disegno sia con dispositivi abilitati per il tocco che con dispositivi tradizionali.

Una volta comprese le nozioni di base sugli eventi del puntatore, passiamo ora a un uso più realistico, vale a dire l'implementazione di un gioco di tipo puzzle a immagini:

Immagine di un puzzle abilitato per il multitocco che usa Canvas e SVG

Sezionamento di un'immagine

La prima operazione da eseguire per creare un gioco di questo tipo consiste nel creare i pezzi o i riquadri dell'immagine. Il metodo drawImage dell'API Canvas ti consente di sezionare facilmente un'immagine di origine in un Canvas, impostando parti dell'immagine da copiare o visualizzare. La sintassi è drawImage(image, i_x, i_y, i_width, i_eight, c_x, c_y, c_width, c_height)Le due illustrazioni seguenti mostrano come parti di un elemento <img> vengono selezionate e visualizzate in un Canvas:

Immagine relativa alla divisione o al sezionamento dell'immagine in pezzi o riquadri

Immagine di origine

Immagine relativa alla divisione o al sezionamento dell'immagine in pezzi o riquadri

Canvas

Suddividendo l'immagine del puzzle di origine in una serie di righe e colonne (una tabella, puoi applicare il metodo drawImage a ogni cella della tabella, ottenendo i singoli riquadri dell'immagine:

Immagine in cui tutti i singoli riquadri vengono combinati per creare un'immagine completa

Questo processo di generazione dei riquadri è descritto nell'esempio 2. Per visualizzare il codice di origine dell'esempio 2, fai clic con il pulsante destro del mouse sulla pagina dell'esempio 2 e scegli HTML. La descrizione dell'esempio 2 è suddivisa in due sezioni:

Tag meta X-UA-Compatible

Dal momento che l'esempio 2 è stato sviluppato su una Intranet locale con Internet Explorer, è stato usato il tag <meta http-equiv="X-UA-Compatible" content="IE=10"> per garantire l'uso della modalità documento e browser corretta per Internet Explorer. Per altre informazioni su questo aspetto, vedi Definizione della compatibilità di documenti. Questo tag deve in genere essere rimosso prima che la pagina venga usata in un ambiente di produzione.

Sezionamento dell'immagine

Per convertire l'immagine del puzzle in formato 400 per 400 in pezzi o riquadri utili, creeremo prima un oggetto immagine in memoria dell'immagine da sezionare (400x400.png) e quindi richiameremo la funzione anonima una volta completato il caricamento dell'immagine:


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

In base alle dimensioni dell'immagine di origine (image.width e image.height) e al numero desiderato di righe e colonne in cui suddividere l'immagine (NUM_COLS e NUM_ROWS), calcoliamo le dimensioni richieste nella funzione anonima:


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

Impostiamo larghezza e altezza dell'elemento Canvas su quelle del riquadro perché creeremo tutti i riquadri da questo Canvas. Ecco come vengono create le singole immagini per i riquadri:


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

Per comprendere meglio questo ciclo for nidificato due volte, supponiamo di voler ottenere un puzzle da 5 righe per 5 colonne e che al momento le variabili row e col siano rispettivamente impostate su 2 e 3, ovvero che ci troviamo nella cella (2, 3) dell'immagine di origine:

Immagine con puzzle da 5 righe per 5 colonne con coordinate della cella della tabella (2, 3) selezionate

Se le dimensioni dell'immagine di origine sono 400 per 400 pixel, quindi:


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

Il risultato sarà:


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

Oppure:


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

In altre parole considereremo un'istantanea 80 per 80 dell'immagine di origine nella posizione (240, 160):

Istantanea del puzzle con le coordinate della cella della tabella (240, 160) selezionate

Posizioniamo ora l'istantanea nell'angolo superiore sinistro di un elemento Canvas da 80 per 80px:

Immagine della sola istantanea corrispondente alle coordinate della cella della tabella (240, 160)

Questo elemento Canvas viene quindi convertito in un'immagine URL dati e archiviato nella matrice di oggetti sezionati, come illustrato qui:


slicedImageTable.push( _canvas.toDataURL() );

Nella parte rimanente dell'esempio 2 viene verificato che l'immagine del puzzle è stata correttamente sezionata visualizzando i singoli riquadri nell'ordine in cui sono stati acquisiti (sezionati).

Conversione delle sezioni dell'immagine in SVG

A questo punto, dopo aver generato le sezioni dell'immagine, ovvero i riquadri, vedremo come convertire questi riquadri di immagini URL dati in oggetti immagine SVG. Questo processo di generazione è descritto nell'esempio 3. Per visualizzare l'output dell'esempio 3, devi aprire la finestra della console di debug del browser. Per altre informazioni, vedi Come usare gli Strumenti di sviluppo F12 per eseguire il debug delle pagine Web. La principale differenza tra l'esempio 2 e l'esempio 3 è data dalla modalità di creazione e impostazione degli elementi immagine SVG, come illustrato qui:


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

Dal momento che SVG è un tipo di XML, è necessario specificare uno spazio dei nomi durante la creazione di un elemento SVG (almeno dall'esterno del modello a oggetti SVG):


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

Con gli elementi immagine SVG viene usato un attributo href e non quello src usato con l'elemento <img> non SVG. Tieni inoltre presente che per Internet Explorer puoi usare svgImage.setAttribute('href', _canvas.toDataURL()) per impostare l'attributo href degli elementi immagine SVG. Poiché con altri browser potrebbe essere richiesta la sintassi XLink, viene usato l'attributo seguente:


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

Il valore degli attributi width e height predefiniti di un'immagine SVG è 0, per questo dobbiamo impostare questi valori in modo esplicito:


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

Rispetto infine al sistema di coordinate associato al riquadro di visualizzazione SVG (vedi Trasformazioni delle coordinate SVG), creeremo e imposteremo due proprietà personalizzate correctX e correctY, per registrare la posizione di ogni riquadro in un puzzle corretto, ovvero vincente.

Visualizzazione di immagini SVG

Ora che abbiamo posizionato le immagini SVG archiviate nella matrice dei riquadri, l'attività successiva consisterà nel visualizzarle sullo schermo. Per semplificare questa operazione, aggiungiamo un elemento SVG <svg> alla pagina Web, oltre ad alcuni potenziamenti, come descritto nelle tre sezioni seguenti e illustrato nell'esempio 4 (fai clic con il pulsante destro del mouse visualizzare il codice sorgente).

SVG liquido

Oltre ad aggiungere un elemento SVG, useremo anche alcune proprietà CSS per rendere l'intero riquadro di visualizzazione SVG completamente liquido (o fluido). Il primo elemento da considerare è l'elemento SVG stesso:


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

In questo caso il riquadro di visualizzazione SVG quadrato rappresenta il 75% della dimensione minima del riquadro di visualizzazione del browser, quindi viene applicato un sistema di coordinate da 400 per 400. Per ottenere l'elemento SVG liquido desiderato, assicurati che vengano applicate le regole CSS seguenti:

  • Con gli elementi html e body è richiesto un attributo height impostato su 100%:
    
    html, body {
      margin: 0;
      padding: 0;
      height: 100%
    }
    
    

    A questo vengono ridotti sia le dimensioni del riquadro di visualizzazione del browser che il contenuto del riquadro di visualizzazione SVG. Tieni presente che il sistema di coordinate da 400 per 400 unità intrinseco del riquadro di visualizzazione SVG rimane invariato: cambiano solo le dimensioni delle unità delle coordinate.

  • Come l'elemento <img>, anche SVG è un elemento incorporato. Per centrarlo nel riquadro di visualizzazione del browser, impostiamo quindi la proprietà display su block e i margini left e right su auto:
    
    svg {
      display: block; 
      margin: 0 auto;
    }
    
    

Rilevamento funzionalità

Dal momento che stiamo usando addEventListener, Canvas e SVG, rileveremo queste funzionalità prima di provare a visualizzare un puzzle, come nell'esempio 4 illustrato qui:


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

Come puoi vedere, accediamo alla funzione main solo se è supportato il metodo addEventListener. A proposito, se addEventListener non è supportato, è altamente improbabile che lo sia l'elemento Canvas o SVG.

Dopo l'accesso alla funzione main creiamo una variabile game che conterrà tutte le variabili "globali" e gli stati correlati al nostro gioco. Come puoi vedere, se il browser dell'utente non supporta tutte le funzionalità richieste, viene visualizzato un messaggio per consentire l'identificazione del problema.

Quando siamo certi che tutte le funzionalità richieste sono supportate, proseguiamo con lo stesso approccio usato nell'esempio. In questo caso però alcuni nomi di variabili sono cambiati, ad esempio NUM_ROWS in game.numRows e slicedImageTable in game.tiles. Dopo aver completato la creazione di tutti gli elementi immagine SVG (riquadri) nel ciclo for nidificato due volte, li visualizziamo come illustrato più avanti.

Visualizzazione dei riquadri

Dopo aver calcolato e impostato la posizione di ogni riquadro, possiamo visualizzare le immagini dei riquadri SVG aggiungendole semplicemente all'elemento SVG:


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

Per verificare la liquidità (o fluidità) dei riquadri SVG, cambia le dimensioni della finestra del browser. Noterai che il riquadro di visualizzazione SVG è sempre il 75% della dimensione più piccola (larghezza o altezza) del riquadro di visualizzazione del browser.

Spostamento di riquadri

Dopo aver visualizzato i riquadri del puzzle, la prossima sfida consiste nel consentire all'utente di spostarli con un dispositivo puntatore. Per iniziare, vedremo in che modo spostare tre cerchi SVG, come illustrato nell'esempio 5 (fai clic con il pulsante destro del mouse per visualizzare il codice sorgente).

Creiamo innanzitutto una matrice globale che contiene i cerchi attualmente attivi, vale a dire quelli su cui è stato fatto clic, come indicato da un evento mousedown o MSPointerDown:


var _activeCircles = [];

Tanto per spiegare il frammento di codice seguente, se si associasse direttamente un gestore dell'evento mousemove ai cerchi SVG (come sembra ragionevole), l'utente potrebbe malauguratamente spostare il mouse in modo abbastanza rapido da "perdere" un cerchio. Ciò equivale a dire che il movimento del cerchio SVG non riesce a tenere il passo con i movimenti rapidi del mouse eseguiti dall'utente. Presupponendo che il gestore dell'evento mousemove sia responsabile dello spostamento di un cerchio nella schermata, non appena il mouse e il cerchio vengono separati fisicamente, l'esecuzione del gestore dell'evento mousemove si interrompe e di conseguenza il movimento del cerchio si arresta. È per questo motivo che associamo il gestore dell'evento mousemove all'oggetto window e non ai cerchi SVG: in questo modo anche se l'utente esegue movimenti molto rapidi con il mouse, l'oggetto window non andrà mai perso.


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

Come illustrato nel frammento di codice precedente, registriamo un gestore dell'evento del puntatore MSPointerMove se disponibile, che gestisce anche i normali eventi del mouse. In caso contrario, registriamo un gestore dell'evento mousemove. Come ricorderai dalla descrizione dell'esempio, non puoi eseguire la registrazione di entrambi i tipi di evento (MSPointerMove e mousemove) nello stesso oggetto.

Più avanti registreremo i gestori dell'evento per puntatore su e puntatore giù in ogni elemento cerchio 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 particolare attenzione alla funzione handlePointerEvents, di cui parleremo in seguito:


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

Per descrivere più facilmente handlePointerEvents, esamineremo due scenari, uno per lo spostamento di un cerchio singolo e l'altro per lo spostamento di due cerchi contemporaneamente.

Movimento di un cerchio singolo

Gli eventi da gestire sono tre: down, move e up.

Evento down

Quando l'utente tocca un cerchio singolo (con mouse, penna o dito), viene attivato l'evento down, che richiama handlePointerEvents. Se gli eventi puntatore sono supportati, evt.pointerId sarà non null e activeCircleIndex sarà uguale a evt.pointerId. In caso contrario, activeCircleIndex sarà 0 (grazie a evt.pointerId || 0). Se evt.pointerId è null, può esistere un solo cerchio attivo alla volta, vale a dire _activeCircles[0], che corrisponde all'unica possibilità quando l'unico dispositivo puntatore consentito è il mouse.

Successivamente l'istruzione switch verifica evt.type e inoltra il flusso del controllo alla clausola mousedown/MSPointerDown. Per garantire che il cerchio attivo sia sempre in primo piano rispetto a tutti gli altri, lo rimuoviamo e lo aggiungiamo al DOM. L'ultimo elemento aggiunto coincide sempre con quello di cui viene eseguito il rendering per ultimo/primo.

A questo punto, se è definito evt.pointerId, chiamiamo msSetPointerCapture su evt.target, ovvero sul cerchio attivo, in modo che quest'ultimo possa continuare a ricevere tutti gli eventi registrati. Il cerchio viene così fisicamente rimosso e reinserito nel riquadro di visualizzazione del browser.

Registriamo infine il cerchio toccato nell'elenco dei cerchi attivi:


_activeCircles[activeCircleIndex] = evt.target;

Evento move

Quando si sposta il dispositivo di puntamento sull'oggetto window, viene eseguito il gestore dell'evento mousemove/MSPointerMove (handlePointerEvents) che inoltra il flusso del controllo alla clausola mousemove/MSPointerMove dell'istruzione switch. Nel caso in cui un cerchio non sia stato toccato in precedenza, la matrice _activeCircles risulterà vuota e activeCircle = _activeCircles[activeCircleIndex] sarà null. In tal caso passiamo direttamente all'istruzione break e terminiamo l'istruzione switch.

Se invece activeCircle non è null, ovvero se è presente un cerchio attivo da spostare, convertiamo le coordinate del dispositivo di puntamento (evt.clientX, evt.clientY), relative al riquadro di visualizzazione del browser, nel sistema di coordinate SVG da 400 per 400 unità. Per questa operazione usiamo la matrice di trasformazione delle coordinate (CTM). Per altre informazioni, vedi le trasformazioni delle coordinate SVG.

Spostiamo infine il centro del cerchio (cx, cy) in base ai valori delle coordinate trasformate:


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

Evento up

Se si attiva un evento up su un cerchio, il flusso del controllo viene inoltrato alla clausola mouseup/MSPointerUp dell'istruzione switch. Il cerchio toccato o selezionato con il mouse e indicato da activeCircleIndex viene rimosso dall'elenco dei cerchi attivi e, se applicabile, la relativa richiesta di eventi di acquisizione viene rilasciata tramite msRelasePointerCapture.

Movimento di due cerchi

Come per il cerchio singolo, anche qui ci sono tre eventi da gestire: down, move e up.

Eventi down

In questo caso due oggetti evento down del cerchio vengono inseriti quasi contemporaneamente nella clausola mousedown/MSPointerDown dell'istruzione switch. La matrice _activeCircles contiene ora due oggetti cerchio i cui indici usati per l'accesso costituiscono i valori evt.pointerId associati.

Eventi move

Non appena i due oggetti dell'evento move della finestra vengono inseriti quasi contemporaneamente nella clausola mousemove/MSPointerMove dell'istruzione switch, ogni cerchio si muove a turno, come se si trattasse di un cerchio singolo.

Eventi up

Non appena ogni oggetto dell'evento up viene inserito quasi contemporaneamente nella clausola mouseup/MSPointerUp, ognuno viene pulito a turno, come se si trattasse di un cerchio singolo.

Puzzle a immagini

A questo punto, abbiamo tutti i pezzi necessari per creare un gioco di tipo puzzle a immagini utilizzabile su dispositivi abilitati per il tocco perfettamente funzionante, anche se non necessariamente interessante. La discussione è piuttosto lunga. Conviene quindi dividerla in più parti. Inizieremo con l'esempio 6, la struttura essenziale del giorno. Il gioco completo è incluso nell'esempio 7 e verrà illustrato più avanti.

Struttura essenziale

Rispetto al gioco presentato in Animazione SVG avanzata, la struttura essenziale del puzzle è relativamente più semplice, come puoi vedere nell'esempio 6 (fai clic con il pulsante destro del mouse per visualizzare il codice sorgente). Ecco il markup di base dell'esempio 6:


<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'elemento <rect> viene usato per colorare di nero l'intero riquadro di visualizzazione SVG, indipendentemente dalle dimensioni correnti. Questo costituirà il campo da gioco per i pezzi del puzzle. Puoi vederne l'effetto nella cattura di schermata dell'esempio 6 seguente:

Cattura di schermata del "campo da gioco" per i pezzi del puzzle

La parte rimanente della struttura essenziale per l'esempio 6 è costituita dal codice JavaScript seguente:


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

Come spiegato nella sezione precedente sul rilevamento delle funzionalità, se addEventListener è disponibile, possiamo attivare la funzione main. Inizializziamo quindi una matrice imageList, che contiene i percorsi delle immagini da usare nel gioco, ovvero le immagini da suddividere in riquadri, rendere casuali e risolvere. Se l'utente raggiunge il settimo livello, la prima immagine (puzzle0.png) viene riciclata all'infinito.

Richiamiamo quindi la funzione costruttore Game. Il primo parametro 2 indica al costruttore di generare un oggetto game costituito da due righe e due colonne. Il primo parametro è naturalmente l'elenco delle immagini del puzzle da scorrere, se necessario.

All'interno del costruttore inseriamo tutte le variabili "globali" in una comoda variabile gameData. Se non conosci la parola chiave this, l'istruzione this.hasCanvas = !!game.elements.canvas.getContext crea e imposta una proprietà hasCanvas sull'oggetto costruito dal costruttore (la variabile game). La doppia negazione (!!) forza semplicemente l'espressione game.elements.canvas.getContext a usare un valore Booleano (true se è supportato l'elemento Canvas).

Allo stesso modo, this.init = function() { … } definisce un metodo, chiamato init, per tutti gli oggetti creati dal costruttore (ne esiste solo uno di questi oggetti, ovvero game). Richiamando game.init(), oltre ad altre operazioni, si avvia il gioco.

Gioco di tipo puzzle a immagini multitocco

Siamo ora in grado di combinare tutte le informazioni in un gioco di tipo puzzle a immagini multitocco perfettamente funzionante, come illustrato nell'esempio 7. Il codice sorgente associato all'esempio 7 dovrebbe risultare familiare e ben commentato. Ci sono però due componenti per i quali è doverosa un'ulteriore spiegazione:

Definizione della sequenza casuale per i riquadri

Nella funzione createAndAppendTiles generiamo gli oggetti immagine dei riquadri SVG, come nell'esempio 4. Prima però di aggiungerli all'elemento SVG, dobbiamo renderli casuali e assicurarsi che la sequenza casuale risultante non corrisponda all'immagine originale del puzzle originale completo. Questo costituisce davvero un problema solo nel primo livello a quattro riquadri:


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

Per rendere casuali i riquadri in modo più semplice, inseriamo le coppie di coordinate associate in una matrice (coordinatePairs) e usiamo il metodo di ordinamento della matrice di JavaScript come illustrato qui:



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

Come descritto in Metodo sort (JavaScript) e dal momento che Math.random() restituisce un valore compreso tra 0 e 1, questa funzione di ordinamento anonima ordina in modo casuale gli elementi della matrice.

Clausola up estesa del puntatore

Le clausole down e move nell'istruzione switch sono quasi identiche agli esempi precedenti. La clausola up è tuttavia stata notevolmente estesa:



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;

Il primo componente della clausola up di cui parlare è l'aggancio dei riquadri. Per molti giochi di tipo puzzle, incluso questo, è necessario che un pezzo (riquadro) si agganci automaticamente quando viene spostato in prossimità della posizione corretta. Ecco il codice usato per eseguire questa operazione, copiato dall'esempio precedente:


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

In questo frammento di codice otteniamo la posizione del riquadro attivo (quando viene attivato l'evento up) ed eseguiamo una ripetizione di tutti i riquadri per determinare la posizione corretta di ognuno. Se la posizione del riquadro attivo è sufficientemente vicina a una di quelle corrette, il riquadro viene agganciato alla posizione e il ciclo for viene immediatamente interrotto, dal momento che non è necessario esaminare le altre posizioni corrette. La clausola if disegna metaforicamente una piccola casella di rilevamento delle collisioni in prossimità del centro di una posizione corretta e, se la posizione del riquadro attivo rientra in tale casella, la clausola diventa true e il riquadro viene agganciato in posizione tramite le due chiamate al metodo setAttribute seguenti:


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

Tieni presente che aumentando gameData.snapDelta, aumentano anche le dimensioni della casella di rilevamento delle collisioni, quindi l'aggancio dei riquadri diventa meno sensibile.

A questo punto, se il gioco è in corso, verifichiamo che il posizionamento del riquadro attivo è quello finale e corretto eseguendo una ripetizione di tutti i riquadri e una verifica forzata   :


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 i riquadri non sono tutti nelle posizioni corretti, handlePointerEvents verrà immediatamente terminato e si attenderà che il successivo evento puntatore attivi handlePointerEvents. Diversamente, se l'utente ha risolto il puzzle viene eseguito il codice seguente:


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

Dal momento che il livello di questo gioco è stato completato, impostiamo gameData.inProgress su false e visualizziamo il nuovo punteggio. Considerato che le dimensioni correnti (numero di righe e colonne) del gioco vengono usate anche per indicare il livello corrente del gioco, ovvero il numero di puzzle che l'utente è riuscito a risolvere fino a quel momento e dal momento che la difficoltà del gioco è proporzionale al doppio del numero totale di righe o di colonne (perché è uguale), il punteggio viene incrementato del quadrato delle dimensioni correnti del gioco:


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

Incrementiamo quindi il proxy del livello del gioco gameData.size e visualizziamo una delle possibili frasi di vincita incluse in una matrice:


++(gameData.size);

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

Infine rimuoviamo tutti gli elementi immagine SVG preesistenti (riquadri) associati all'elemento SVG per prepararci al prossimo gruppo di elementi immagine SVG diversi che verranno creati e aggiunti all'elemento SVG tramite createAndAppendTiles:


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

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

 

Obiettivo di questa esercitazione era di spiegarvi come gestire eventi multitocco in uno scenario abbastanza realistico, quello dell'implementazione di un gioco di tipo puzzle per dispositivi abilitati per il tocco. Le indicazioni fornite in questo documento dovrebbero garantirvi un livello di conoscenze adeguato per gestire gli eventi di tocco in un certo numero di circostanze, tra cui anche quelle che interessano elementi Canvas e SVG.

Argomenti correlati

Galleria di foto Contoso Images
Eventi gesti
Linee guida per la creazione di siti sensibili al tocco
Come simulare il passaggio del mouse su dispositivi abilitati per il tocco
Grafica HTML5
Esempi ed esercitazioni per Internet Explorer
Eventi puntatore
Aggiornamenti degli eventi puntatore

 

 

Mostra:
© 2015 Microsoft