Dividere un'immagine
Il primo passo per creare questo tipo di gioco è realizzare le parti dell'immagine o tessere. Il metodo drawImage delle canvas API consente di dividere facilmente un'immagine sorgente in un canvas, impostando porzioni 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 prossime due illustrazioni mostrano come porzioni di un elemento <img> sono selezionate e visualizzate su un canvas:
Suddividendo l'immagine puzzle sorgente in una serie di righe e colonne (una tabella), il metodo drawImage
può essere applicato a ciascuna cella della tabella, generando le singole tessere dell'immagine richieste:
Questo processo di generazione del tile viene mostrato nell'Esempio 2. Per visualizzare il codice sorgente dell'esempio 2, cliccare con il tasto destro del mouse sulla pagine dell'Esempio 2 e scegliere Visualizza sorgente. La discussione dell'Esempio 2 è suddivisa nelle due sezioni che seguono:
X-UA-Compatible meta tag
Poiché l'Esempio 2 è stato sviluppato in una intranet locale utilizzando Internet Explorer, il tag:
<meta http-equiv="X-UA-Compatible" content="IE=10">
è stato usato per assicurare che Internet Explorer sia posto nel browser e document mode corretto. Per maggiori informazioni su questo aspetto, si veda Defining Document Compatibility. In generale, questo tag dovrebbe essere rimosso prima che la pagina vada in produzione.
Dividere l'immagine
Per convertire l'immagine puzzle 400x400 in pezzi utili del puzzle (o tessere), creiamo un oggetto in memoria dell'immagine da poter dividere (400x400.png
) e invochiamo una funzione anonima quando l'immagine sarà completamente caricata:
var image = new Image();
image.src = "images/400x400.png";
image.onload = function () { ... }
Basandoci sulla dimensione dell'immagine sorgente (image.width
e image.height
) e il numero desiderato di righe e colonne in cui l'immagine dovrà essere divisa (NUM_COLS
e NUM_ROWS
), calcoleremo la grandezza richiesta della tessera all'interno della funzione anonima:
var dx = _canvas.width = image.width / NUM_COLS;
var dy = _canvas.height = image.height / NUM_ROWS;
Diamo al canvas l'altezza e la larghezza di una singola tessera. Utilizzeremo poi il canvas come una maschera, spostando l'immagine in modo che appaia un pezzo alla volta. Ad ogni iterazione memorizzeremo l'immagine della singola tile.
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);
// Place a border around each tile.
ctx.strokeRect(0, 0, dx, dy);
slicedImageTable.push( _canvas.toDataURL() );
}
}
Per capire meglio questo doppio ciclo for
, supponiamo di volere un puzzle di 5 righe per 5 colonne e che le variabili row
e col
siano rispettivametne 2 e 3. In altre parole abbiamo inquadrato la cella della tabella (2, 3) dell'immagine di origine:
Se la dimensione dell'immagine di origine è di 400 per 400 pixel, allora:
dx = canvas.width = 400 / 5 = 80
dy = canvas.height = 400 / 5 = 80
Che si traduce in
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, si prende una snapshot 80 per 80 dell'immagine sorgente alla posizione (240, 160):
E si colloca la snapshot nell'angolo in alto a sinistra di un canvas 80px per 80px:
Questo canvas è quindi convertito in una data URL image e salvato nell'array dell'immagine tagliata, come mostrato qui:
slicedImageTable.push( _canvas.toDataURL() );
Il resto dell'Esempio 2 consente alla puzzle image di essere tagliata con successo mostrando le tessere individuali nello stesso ordine nel quale sono acquisite (tagliate).
Iniziando dalle basi per finire con la costruzione di un puzzle da giocare multi-touch che usi sia canvas che SVG, questo tutorial descrive, in modo graduale, come utilizzare i pointer events per gestire gli eventi del mouse, di una penna, o di una o più dita.
Su Internet Explorer 10 e nelle app Windows Store che usano JavaScript, gli sviluppatori possono usare un tipo di input chiamato pointer. Un pointer, in questo contesto, può essere qualsiasi punto di contatto sullo schermo generato da un mouse, penna, dito o più dita.
Nota: I pointer events richiedono Windows 8 o successivi.
Questo tutorial dapprima descrive come iniziare con i pointer, quindi passa attraverso l'implementazione di un puzzle game multi-pointer che utilizza sia canvas che SVG:
Se sapete già come usare degli eventi mouse, i pointer events ti sembreranno familiari: MSPointerDown
, MSPointerMove
, MSPointerUp
, MSPointerOver
, MSPointerOut
, e così via. Gestire eventi mouse e pointer è semplice, come mostrato nel prossimo esempio.
In aggiunta, per device che supportano il multi-touch, questo esempio funziona così com'è con tanti punti di contatto simultanei (dita) quanti il dispositivo è in grado di gestire. Questo è possibile perché i pointer events si innescano per qualsiasi punto di contatto dello schermo.
Per questo motivo applicazioni come il seguente esempio di disegno elementare supportano il multi-touch senza alcun bisogno di programmazione ad hoc:
Esempio 1. App elementare per il disegno
<!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>
Di norma, il browser consuma degli eventi touch per i suoi scopi. Per esempio, per scrollare in alto una pagina Web l'utente può toccare lo schermo (non un link) e tirare giù. Per effettuare lo zoom in una pagina, può essere usata un'azione di pinch a due dita.
Nell'Esempio 1 non vogliamo che si verifichino questi comportamenti predefiniti, altrimenti creando un disegno con una o più dita si darebbe luogo ad un panning della pagina (o uno zoom). Per consentire a questi eventi di scorrere nel vostro codice JavaScript, possiamo usare il CSS seguente:
html {
-ms-touch-action: double-tap-zoom;
}
Questo istruisce il browser a "ignorare" tutte le azioni touch eccetto un doppio tap (che effettua uno zoom nella pagina). In altre parole, tutti gli eventi touch sono disponibili nel tuo codice JavaScript, eccetto l'abilità di catturare il doppio tap. Gli altri possibili valori per -ms-touch-action
sono auto, none, manipulation, e inherit come descritto nelle Linee guida per costruire siti touch friendly.
Dalla prospettiva di addEventListener perspective
, è importante notare che i pointer events di Windows Internet Explorer e i tradizionali mouse events sono mutuamente esclusivi, questo significa che quando sono disponibili dei pointer events comprendono anche i mouse events. In altre parole, non puoi registrare contemporaneamente l'event listener paintCanvas
sia con mousemove
sia con MSPointerMove
:
// COSE DA NON FARE: _canvas.addEventListener('mousemove', paintCanvas, false); _canvas.addEventListener('MSPointerMove', paintCanvas, false);
Al contrario, se gli MS pointer events, che riportano anche gli eventi del mouse, sono disponibili, li usiamo. In caso contrario, usiamo eventi tradizionali del mouse:
if (window.navigator.msPointerEnabled) {
_canvas.addEventListener('MSPointerMove', paintCanvas, false);
}
else {
_canvas.addEventListener('mousemove', paintCanvas, false);
}
Così, il precedente frammento di codice consente all'applicazione da disegno di lavorare sia con dispositivi touch-enabled, sia con dispositivi tradizionali (non touch).
Dopo aver capito le basi dei pointer events, passiamo a un uso più realistico - la realizzazione di un puzzle game ad immagini:
Convertire pezzi di immagine in SVG
Ora che possiamo generare pezzi di immagini (cioè tessere del puzzle), scopriremo come convertire queste tessere data URL in image objects SVG. Questo processo è dimostrato nell'esempio 3. Per vedere l'output dell'esempio 3 deve essere aperta la vostra debugger console del browser. Per maggiori informazioni, vedere How to use F12 Developer Tools to Debug your Webpages. La principale differenza tra l'Esempio 2 e 3 è come gli elementi dell'immagine SVG sono creati e impostati, come mostrato 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);
}
}
Poiché SVG è una forma di XML, deve essere specificato un namespace quando creiamo un elemento SVG (almeno al di fuori dell'object model SVG):
var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image')
Gli elementi image
di SVG usano un attributo href
invece dell'attributo src
(che è usato con l'elemento non-SVG <img>
). Inoltre, bisogna essere consapevoli del fatto che per Internet Explorer
svgImage.setAttribute ('href', _canvas.toDataURL ())
può essere utilizzata per impostare l'attributo href degli elementi dell'immagine SVG. Altri browser, tuttavia, potrebbero richiedere la sintassi XLink
, motivo per cui è usato invece il seguente:
svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", _canvas.toDataURL());
La larghezza e altezza di default di un'immagine SVG è 0
, per questo dobbiamo impostare esplicitamente questi valori:
svgImage.setAttribute('width', dx);
svgImage.setAttribute('height', dy);
Per ultimo, rispetto al sistema di coordinate associato alla viewport SVG (vedi SVG Trasformazioni delle coordinate), creiamo e impostiamo due proprietà personalizzate, correctX
e correctY
, al fine di registrare dove ogni tessera dovrebbe essere in un puzzle corretto.
Visualizzare immagini SVG
Ora che abbiamo posizionato le immagini SVG salvate nell'array delle tessere, il nostro prossimo compito è visualizzarle sullo schermo. Per farlo in modo semplice, aggiungiamo un elemento SVG <svg>
alla pagina web con altre piccole aggiunte che discuteremo più avanti nell'articolo e che sono mostrate nell'Esempio 4.
Liquid SVG
In più rispetto alla semplice aggiunta di un elemento SVG, usiamo alcune proprietà CSS per rendere la SVG viewport interamente liquida (o fluida). Il primo oggetto da considerare è l'elemento SVG stesso:
<svg width="75%" height="75%" viewbox="0 0 400 400"></svg>
Qui, il quadro della viewport SVG è il 75% della più piccola dimensione che assume la viewport del browser. All'area applichiamo un sistema di coordinate 400x400. Affinché la SVG liquida funzioni come desiderato, assicuratevi di scrivere le seguenti regole CSS :
/* Gli elementi html e body richiedono una altezza del 100%. */
html, body {
margin: 0;
padding: 0;
height: 100%
}
Ora, come la viewport del browser viene ridotta nelle dimensioni, così vengono ridotti i contenuti della SVG viewport. Si noti che il sistema di coordinate 400 per 400 della SVG viewport rimane intatto - solo la dimensione delle unità cambia.
Come l'elemento <img>
, l'elemento SVG è inline. Così per centrarlo all'interno della browser viewport, impostiamo la sua proprietà display
a block
e i suoi margini left
e right
ad auto
:
svg {
display: block;
margin: 0 auto;
}
Feature detection
Poiché stiamo usando addEventListener, canvas, e SVG, cerchiamo di individuare queste funzioni prima di provare a visualizzare un puzzle. L'Esempio 4 fa proprio questo:
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 potete vedere, usiamo la funzione main
solo se il metodo addEventListener
è supportato (in ogni caso, se addEventListener
non è supportato, è estremamente improbabile che canvas ed SVG lo siano).
Dopo essere entrati nella funzione main
, creiamo una variabile game
che contenga tutte le nostre variabili globali e stati relativi al gioco. Come si vede, se il browser dell'utente non supporta tutte le features richieste, viene visualizzato un messaggio che identifica il problema.
Quando siamo certi che tutte le features sono supportate, possiamo continuare con lo stesso approccio che abbiamo usato nell'Esempio 3, ad eccezione che alcuni nomi di variabili sono cambiati, come NUM_ROWS
in game.numRows
e slicedImageTable
in game.tiles
. Quindi, dopo aver completato la creazione di tutti i nostri SVG image elements (le tessere) nel loop for
doppiamente annidato, possiamo mostrarli, come discusso di seguito.
Visualizzare le tessere del puzzle
Con ogni posizione delle tessere già calcolata e impostata, possiamo visualizzare le immagini SVG delle tessere semplicemente aggiungendole all'elemento SVG:
function displaySvgImages() {
for (var i = 0; i < game.tiles.length; i++) {
game.svg.appendChild(game.tiles[i]);
}
}
Per verificare la adattabilità (o fluidità) delle tessere SVG, è necessario cambiare la grandezza della finestra del browser. Da notare come la viewport SVG è sempre il 75% della dimensione più piccola (larghezza o altezza) della viewport del browser.
Spostare le tessere
Ora che abbiamo le tessere del puzzle visibili, la nostra prossima sfida è consentire all'utente di muoverle con un pointer device. Affronteremo questo problema innanzitutto esaminando come spostare tre cerchi SVG, come mostrato nell'Esempio 5.
Per prima cosa creiamo un array globale che contiene i cerchi attualmente attivi. Cioè, quei cerchi che sono stati "cliccati" come indicato da un evento mousedown
o MSPointerDown
:
var _activeCircles = [];
Forse può aiutare a spiegare frammento di codice che segue, pensare che se agganciassimo il gestore di eventi per mousemove
direttamente ai cerchi SVG (come sembra ragionevole), (purtroppo) potrebbe essere possibile per l'utente muovere il mouse abbastanza rapidamente da "perdere" il cerchio. Cioè, il movimento del cerchio SVG non può tenere il passo con i movimenti del mouse rapidissimi dell'utente.
Assumendo che il gestore eventi per mousemove
sia responsabile dello spostamento di un cerchio attraverso lo schermo, non appena il mouse e cerchio sono fisicamente separati, l'handler per mousemove cessa di essere in esecuzione e di conseguenza il movimento del cerchio si arresta. Questo è il motivo per cui noi agganciamo il gestore di eventi per mousemove
all'oggetto window
invece che ai circle
SVG - non importa quanto velocemente un utente sposta il mouse, non potrà mai perdere l'oggetto window
che è onnipresente:
if (navigator.msPointerEnabled) {
window.addEventListener('MSPointerMove', handlePointerEvents, false);
} else {
window.addEventListener('mousemove', handlePointerEvents, false);
}
Come mostrato nel precedente snippet di codice, registriamo un event handler MSPointerMove
se disponibile (che gestisce i movimenti del mouse tradizionali) e se non lo è registriamo un handler mousemove
. Ricordiamo che si possono registrare entrambi i tipi di eventi (MSPointerMove
e mousemove
) sullo stesso oggetto (come descritto nell'Esempio 1).
Successivamente, registriamo gli event handler per "pointer down
" e "pointer up
" su ogni elmento dei cerchi 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
Si presti attenzione alla funzione handlePointerEvents
che vedremo di 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 meglio descrivere handlePointerEvents
, vedremo due scenari - spostamento di un singolo cerchio, per poi passare a due cerchi contemporaneamente.
Spostamento di un cerchio singolo
Ci sono tre eventi da gestire: "down", "move" e "up".
Evento Down
Quando un utente tocca un solo cerchio (con mouse, penna o dito), l'evento "down" si innesca invocando handlePointerEvents
. Se i pointer events sono supportati, evt.pointerId
non sarà nullo e activeCircleIndex
sarà uguale evt.pointerId
; altrimenti, activeCircleIndex
sarà 0
(grazie a toevt.pointerId || 0
).
Se evt.pointerId
è nullo, possiamo vedere attivo soltanto un cerchio alla volta, vale a dire _activeCircles[0]
, che è l'unica possibilità quando abbiamo il mouse come unico pointer device.
Successivamente, l'istruzione switch
controlla evt.type
e smista il control flow alla clausola mousedown/MSPointerDown
. Per assicurarsi che il cerchio attivo sia sempre sopra gli altri, possiamo semplicemente rimuoverlo e quindi aggiungerlo al DOM (l'ultimo elemento aggiunto è sempre l'ultimo elemento visualizzato più in alto).
Poi, se evt.pointerId
è definito, chiamiamo msSetPointerCapture su evt.target
(cioè il cerchio attivo) in modo che il cerchio possa continuare a ricevere tutti gli eventi registrati. Ciò permette al cerchio di essere "fisicamente" inserito e tolto dalla viewport del browser.
Infine, registriamo il cerchio selezionato (toccato o cliccato) nell'elenco dei cerchi attivi:
_activeCircles[activeCircleIndex] = evt.target;
Evento Move
Quando il dispositivo di puntamento viene spostato (sulla window object), viene eseguito il gestore eventi per MouseMove/MSPointerMove (handlePointerEvents
), che devia il flusso del programma alla clausola mousemove / MSPointerMove
del nostro switch
.
Se un cerchio non viene toccato, l'array _activeCircles
risulta vuoto e activeCircle = _activeCircles [activeCircleIndex]
nullo. In questo caso, con l'istruzione break
usciremmo dallo switch.
Se invece activeCircle
non è nullo (cioè, c'è un cerchio attivo che deve essere spostato), convertiamo le coordinate del dispositivo di puntamento (evt.clientX
, evt.clientY
), che sono relative alla viewport del browser, nel sistema di coordinate SVG 400x400. Ciò viene fatto attraverso la matrice di trasformazione di coordinate (CTM). Per ulteriori informazioni, vedere SVG Coordinate Transformations.
Infine, spostiamo il centro del cerchio (cx, cy
) verso i valori delle coordinate trasformate:
activeCircle.setAttribute('cx', svgPoint.x);
activeCircle.setAttribute('cy', svgPoint.y);
L'evento Up
Se viene generato l'evento up su uno dei cerchi, viene elaborato il caso mouseup/MSPointerUp
del costrutto switch
. Il cerchio toccato (o cliccato), è indicato da byactiveCircleIndex
, viene rimosso dalla lista dei cerchi attivi e, se necessario, viene rilasciato l'elemento grazie a msReleasePointerCapture
.
Movimento a doppio cerchio
Come nel caso del cerchio singolo, ci sono tre eventi da gestire: in basso (down), spostamento (move), e in alto (up).
Eventi Down
In questo caso abbiamo due eventi down, generati sui cerchi in modo quasi simultaneo, da gestire nel ramo mousedown/MSPointerDown
della switch
. L'array associativo _activeCircles
ora contiene due oggetti cerchio (e come indici abbiamo utilizzato i valori dei relativi evt.pointerId
).
Eventi Move
Gli eventi di movimento, registrati sull'oggetto window
, vengono gestiti (quasi contemporaneamente) dal ramo mousemove/MSPointerMove
della switch e ogni cerchio viene spostato a sua volta, proprio come nel caso singolo cerchio.
Eventi Up
Anche in questo caso ogni cerchio che viene rilasciato viene gestito dallo snippet sotto mouseup/MSPointerUp
e per ciascuno viene rimosso il listener, come nel caso del singolo cerchio.
Il gioco del puzzle
Ora abbiamo tutto l'essenziale per creare un "puzzle game" completamente funzionante con il touch. Divideremo il lavoro in modo da renderlo più facilmente digeribile. Si comincerà con l'Esempio 6, lo scheletro del gioco (il gioco completo è presentato in Esempio 7 e viene discusso più avanti).
Struttura del gioco
Diversamente dal gioco presentato in Advanced SVG Animation, la struttura del puzzle game è relativamente semplice, come mostrato nell'Esempio 6 (clic destro per visualizzare il sorgente). Il cuore del markup dell'Esempio 6 è il seguente:
<table>
<tr>
<td colspan="3"><h1>A Multi-Touch Enabled Image Puzzle Using Canvas & 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>
è usato per colorare di nero l'intera viewport SVG (indipendentemente dalla sua dimensione attuale). Essa diventa l'area di gioco in cui inserire le tessere del puzzle. Ecco come appare:
Il resto della struttura per l'Esempio 6 consiste nel seguente JavaScript:
if (window.addEventListener) {
window.addEventListener('load', main, false);
} else {
document.getElementsByTagName('body')[0].innerHTML = "<h1>Il metodo addEventListener non è supportato - prova ancora dopo aver aggiornato il browser.</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"]; // Contiene i percorsi per le immagini del puzzle
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("Effettuata la chiamata al metodo init().") };
} // Game
if (!game.hasCanvas) {
document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas non è supportato - per favore riprova dopo aver aggiornato il browser.</h1>";
return;
}
if (!game.hasSVG) {
document.getElementsByTagName('body')[0].innerHTML = "<h1>SVG non è supportato - per favore riprova dopo aver aggiornato il browser.</h1>";
return;
}
// Si da per assodato che il browser dell'utente supporti tutte le feature richieste.
game.init(); // Fa partire i gioco
} // main
Come abbiamo già visto, sfruttiamo la feature detection: se addEventListener
è disponibile, lanciamo la fuzione main
. Quindi inizializziamo l'array imageList
, che contiene i percorsi delle immagini da usare nel gioco (cioè le immagini da spezzare in tessere che disporremo casualmente). Sono sei immagini, quindi dal settimo livello in poi si ripropone la prima.
Successivamente invochiamo la funzione costruttore Game
. Il primo parametro, 2
, istruisce il costruttore a generare un oggetto game
che ha due colonne e due righe. L'ultimo parametro è naturalmente, la lista delle immagini puzzle da elaborare.
Nel costruttore racchiudiamo le variabili "global" all'interno di una variabile gameData
. Se non siete pratici della keyword this
, l'espressione this.hasCanvas = !!game.elements.canvas.getContext
crea e imposta una proprietà chiamata hasCanvas
sull'oggetto creato dal costruttore creato (nel nostro caso la variabile game
). La doppia negazione (!!
) semplicemente forza l'espressione game.elements.canvas.getContext
ad essumere un valore Booleano(true
se canvas è supportato).
In modo simile, this.init = function() { … }
definisce un metodo, chiamato init
, per tutti gli oggetti creati dal constructor (c'è soltanto un oggetto di questo tipo, cioè game). Invocando game.init()
, tra le altre cose, avviamo il gioco.
Un puzzle multi-touch
Ora possiamo combinare tutte le informazioni accumulate in un vero e proprio puzzle da giocare con il multi-touch, come mostrato nell'Esempio 7. Il codice sorgente associato all'Esempio 7 potrebbe sembrare familiare ed è ben commentato, tuttavia due componenti potrebbero essere meglio spiegati con qualche nota aggiuntiva.
Randomizzazione delle tessere
Nella funzione createAndAppendTiles
, generiamo gli oggetti SVG (image) delle tessere (come nell'Esempio 4). Ma prima di aggiungerle all'elemento SVG, le randomizziamo e facciamo in modo che il pattern casuale risultante non corrisponda esattamente all'immagine originale (completa) del puzzle (che in realtà è un problema soltanto nel primo livello con quattro tessere):
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 randomizzare facilmente le tessere, memorizziamo le coppie di coordinate ad esse associate in un array (coordinatePairs
) e usiamo il metodo sort
per gli array di JavaScript come segue:
coordinatePairs.sort(function() { return 0.5 - Math.random(); });
Come descritto in sort Method (JavaScript) e poiché Math.random()
ritorna un valore tra 0
e 1
, la funzione anonima "sort" indica in modo casuale se un elemento sia maggiore o minore di 0, quindi l'ordinamento dell'array diventa casuale.
Estendere il caso 'Up' del pointer event
I casi "down" e "move" del nostro switch
sono praticamente identici a quelli visti negli esempi precedenti. La clausola "up" invece va significativamente 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 = "Score: " + 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 del caso "up" da discutere è la collocazione delle tessere. Per molti puzzle (incluso questo), è necessario che una tessera vada da sola al suo posto, una volta che l'utente la abbia posizionata abbastanza vicino. Ecco il codice per realizzare questo effetto "snap":
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; // Abbiamo posizionato la tessera attiva correttamente, usciamo dal ciclo FOR
} // if
} // for
In questo frammento di codice, otteniamo la posizione della tessera attiva (quando si innesca l'evento "up") e scorriamo tutte le tessere per determinare la corretta posizione per ciascuna. Se la posizione di una tessera attiva è sufficientemente vicina ad una di queste posizioni corrette, la collochiamo al suo posto e subito usciamo dal ciclo for
(dal momento che non c'è alcun bisogno di guardare ad altre posizioni corrette).
Nell'istruzione if
disegnamo una specie di "collision detection box" attorno al centro della posizione corretta. Se la tessera attiva rientra nel box, l'espressione viene verificata la tessera scatta al suo posto impostando con setAttribute
le coordinate precise della posizione corretta.
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
Siate consapevoli che se gameData.snapDelta
cresce, con essa cresce anche la dimensione dell'area di collisione ("collision detection box"), rendendo il posizionamento delle tessere menso sensibile.
Poi, se il gioco è avviato, verifichiamo che il posizionamento della tessera attiva sia quello giusto scorrendo tutte le tessere e controllandole una ad una.
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 alcune tessere non risultano nella posizione correttra, usciamo immediatamente da handlePointerEvents
e attendiamo il prossimo pointer event che inneschi handlePointerEvents
. In caso contrario, l'utente ha risolto il puzzle e viene eseguita questa parte del codice:
gameData.inProgress = false;
gameData.score += gameData.size * gameData.size;
gameData.elements.scoreBox.innerHTML = "Score: " + 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";
Poiché questo livello del gioco è stato superato, impostiamo gameData.inProgress
a false
e mostriamo il nuovo punteggio. Poiché la dimensione corrente (il numero di righe e colonne) del gioco è anche usata per indicare il livello attuale del gioco (cioè quanti puzzle l'utente ha risolto fino ad ora) e poiché la difficoltà del gioco è proporzionale al quadrato del numero delle righe (o numero di colonne, dato che sono la stessa cosa), il punteggio viene aumentato per il quadrato della dimensione attuale del gioco:
gameData.score += gameData.size * gameData.size;
Quindi incrementiamo il nostro livello di gioco, gameData.size
, e visualizziamo una frase casuale da un array di possibili battute di congratulazioni, tipo "hai vinto":
++(gameData.size);
var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];
Infine, rimuoviamo ogni elemento immagine SVG preesistente (tessere) che popolavano l'elemento SVG per preparare il round successivo con di elementi immagine SVG diversi da creare e aggiungere 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";
Il nostro scopo qui è stato quello di mostrare, attraverso questo tutorial, come gestire eventi multi-touch in uno scenario realistico: l'implementazione di un puzzle. L'articolo dovrebbe offrire una conoscenza sufficiente per gestire degli eventi touch in diverse cirostanze (possibilmente includendo quelle riguardanti canvas e SVG).