So wird's gemacht: Erstellen eines Puzzlespiels mit Canvas, SVG und Mehrfingereingabe

Dieses Lernprogramm beginnt mit den Grundlagen und schließt mit einem Bildpuzzlespiel für die Mehrfingereingabe ab, bei dem Canvas und SVG verwendet werden. So wird Schritt für Schritt die Behandlung von Zeigerereignissen beschrieben, die beispielsweise durch eine Maus, einen Stift oder die Bewegung eines oder mehrerer Finger (Mehrfingereingabe) erstellt werden.

Hinweis  Für Zeigerereignisse ist Windows 8 oder höher erforderlich.

Hinweis  Die Implementierung der Internet Explorer-Zeigerereignisse hat sich geringfügig geändert, seit dieser Artikel für Internet Explorer 10 geschrieben wurde. Weitere Informationen, wie Sie Ihren Code aktualisieren und zukunftssicher machen, finden Sie unter Updates für Zeigerereignisse.

Einführung

In Internet Explorer 10 und Windows Store-Apps mit JavaScript können Entwickler einen bestimmten Eingabetyp nutzen, der als Zeiger bezeichnet wird. In diesem Kontext ist ein Zeiger ein beliebiger Kontaktpunkt auf dem Bildschirm, der mit einer Maus, mit einem Stift oder mit mindestens einem Finger hergestellt wird. In diesem Lernprogramm werden zunächst die ersten Schritte mit Zeigern beschrieben. Dann werden Sie durch die Implementierung eines Bildpuzzlespiels mit mehreren Zeigern geführt, bei dem Canvas und SVG verwendet werden.

Bild eines Puzzles für die Mehrfingereingabe mit Canvas und SVG

Wenn Sie sich bereits mit der Verwendung von Mauszeigern auskennen, sollten Ihnen Zeigerereignisse sehr vertraut vorkommen: MSPointerDown, MSPointerMove, MSPointerUp, MSPointerOver, MSPointerOut usw. Die Behandlung von Maus- und Zeigerereignissen ist unkompliziert, wie dies im folgenden Beispiel 1 gezeigt wird. Darüber hinaus funktioniert Beispiel 1 für Mehrfingereingabe-Geräte mit so vielen gleichzeitigen Kontaktpunkten (Fingern), wie das Gerät jeweils verarbeiten kann. Dies ist möglich, da die Zeigerereignisse für jeden Kontaktpunkt auf dem Bildschirm ausgelöst werden. Daher unterstützen Apps wie das folgende einfache Zeichenbeispiel die Mehrfingereingabe ohne eine spezielle Codierung:

Beispiel 1 - einfache Zeichen-App


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

Normalerweise werden Fingereingabeereignisse im Browser für eigene Zwecke verwendet. Um beispielsweise auf einer Webseite einen Bildlauf nach oben durchzuführen, kann der Benutzer den Bildschirm berühren (nicht bei einem Link) und den Finger nach unten ziehen. Oder zum Vergrößern einer Seite können zwei Finger auseinandergeführt werden. In Beispiel 1 möchten wir diese Standardverhalten unterdrücken. Andernfalls wird die Seite beim Zeichnen mit einem (oder mehreren) Fingern geschwenkt (oder eventuell vergrößert oder verkleinert). Um zu ermöglichen, dass diese Ereignisse in Ihren JavaScript-Code eingehen, verwenden wir die folgende CSS-Formatvorlage:


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

Dadurch wird der Browser angewiesen, alle Fingereingabeaktionen außer einem Doppeltippen (mit dem eine Seite vergrößert wird) zu "ignorieren". Mit anderen Worten, außer der Möglichkeit zum Erfassen von Doppeltippereignissen sind jetzt alle Fingereingabeereignisse im JavaScript-Code verfügbar. Andere mögliche Werte für -ms-touch-action sind auto, none, manipulation und inherit und werden unter Richtlinien für das Erstellen von Websites für die Fingereingabe beschrieben.

Im Hinblick auf addEventListener ist zu beachten, dass sich Windows Internet Explorer-Zeigerereignisse und herkömmliche Mausereignisse gegenseitig ausschließen. Wenn also Zeigerereignisse verfügbar sind, umfassen sie auch automatisch Mausereignisse. Mit anderen Worten, Sie können den paintCanvas-Ereignislistener nicht gleichzeitig für mousemove und MSPointerMove registrieren:



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

Wenn daher MS-Zeigerereignisse verfügbar sind, die auch Mausereignisse melden, dann verwenden wir sie. Andernfalls verwenden wir herkömmliche Mausereignisse:


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

Demnach ermöglicht das vorherige Codefragment, dass die Zeichen-App mit Geräten, die die Fingereingabe unterstützen, oder mit herkömmlichen Geräten (ohne Unterstützung der Fingereingabe) funktioniert.

Nach dieser Einführung in die Grundlagen von Zeigerereignissen fahren wir mit einer praxisnahen Verwendung fort – der Implementierung eines Bildpuzzlespiels:

Bild eines Puzzles für die Mehrfingereingabe mit Canvas und SVG

Aufteilen eines Bilds

Zum Erstellen dieses Puzzlespiels werden im ersten Schritt die einzelnen Bildsegmente, die sogenannten Kacheln, erstellt. Mit der drawImage-Methode der Canvas-API können Sie ein Quellbild mühelos auf eine Canvas aufteilen, indem Sie Teile des Bild kopieren oder anzeigen. Die Syntax lautet drawImage(image, i_x, i_y, i_width, i_eight, c_x, c_y, c_width, c_height). In den nächsten beiden Abbildungen wird veranschaulicht, wie Teile eines <img>-Elements ausgewählt und auf einer Canvas angezeigt werden:

Abbildung zur Verdeutlichung, wie Sie das Bild in verschiedene Segmente oder Kacheln aufteilen können

Quellbild

Abbildung zur Verdeutlichung, wie Sie das Bild in verschiedene Segmente oder Kacheln aufteilen können

Canvas

Indem das Quellpuzzlebild in mehrere Zeilen und Spalten (eine Tabelle) unterteilt wird, kann die drawImage-Methode auf jede Tabellenzelle angewendet werden, sodass die benötigten einzelnen Bildkacheln erstellt werden:

Abbildung zur Verdeutlichung, wie sich das Gesamtbild aus den einzelnen Kacheln zusammensetzt

Das Erstellen der Kacheln wird in Beispiel 2 veranschaulicht. Wenn Sie den Quellcode für Beispiel 2 anzeigen möchten, klicken Sie mit der rechten Maustaste auf die Seite für Beispiel 2, und wählen Sie Quellcode anzeigen aus. Beispiel 2 wird in den folgenden beiden Abschnitten besprochen:

X-UA-kompatibles META-Tag

Da Beispiel 2 in einem lokalen Intranet mit Internet Explorer entwickelt wurde, wurde das <meta http-equiv="X-UA-Compatible" content="IE=10">-Tag verwendet, um sicherzustellen, dass Internet Explorer im richtigen Browser und Dokumentmodus platziert ist. Weitere Informationen dazu finden Sie unter Definieren der Dokumentkompatibilität. Im Allgemeinen sollte dieses Tag erst kurz vor der Veröffentlichung der Seite entfernt werden.

Bildaufteilung

Um das oben dargestellte 400 x 400-Puzzlebild in einzelne Puzzleteile (oder Kacheln) umzuwandeln, erstellen wir zuerst ein speicherinternes Bildobjekt des aufzuteilenden Bilds (400x400.png) und rufen eine anonyme Funktion auf, nachdem das Bild vollständig geladen wurde:


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

Anhand der Größe des Quellbilds (image.width und image.height) sowie der gewünschten Anzahl der Zeilen und Spalten, in die das Bild aufgeteilt werden soll (NUM_COLS und NUM_ROWS), berechnen wir die gewünschte Kachelgröße in der anonymen Funktion:


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

Wir legen die Breite und Höhe der Canvas auf die gleiche Größe wie die einer Kachel fest, da wir aus dieser Canvas alle Kacheln erstellen werden. Die einzelnen Kachelbilder werden wie folgt erstellt:


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

Stellen Sie sich zum besseren Verständnis dieser doppelt verschachtelten for-Schleife vor, dass wir ein Puzzle mit 5 Zeilen und 5 Spalten erstellen möchten und dass für die Variablen row und col derzeit der Wert 2 bzw. 3 festgelegt ist. Wir befinden uns also gerade in der Tabellenzelle mit den Koordinaten (2, 3) des Quellbilds:

Bild eines Puzzles mit 5 Zeilen und 5 Spalten mit ausgewählter Tabellenzelle mit den Koordinaten (2, 3)

Wenn das Quellbild 400 x 400 Pixel groß ist, dann gilt:


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

Dies ergibt:


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

Oder:


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

Anders ausgedrückt, erstellen wir eine 80 x 80-Pixel-Momentaufnahme des Quellbilds bei Position (240, 160):

Momentaufnahme des Puzzles mit der Tabellenzelle mit den Koordinaten (240, 160)

Dann positionieren wir die Momentaufnahme oben links in einer Canvas mit einer Größe von 80 x80 Pixel:

Bild der Momentaufnahme der Tabellenzelle mit den Koordinaten (240, 160)

Diese Canvas wird dann in ein URL-Datenbild konvertiert und in dem Array mit den Bildsegmenten gespeichert. Siehe dazu den folgenden Code:


slicedImageTable.push( _canvas.toDataURL() );

Im weiteren Verlauf von Beispiel 2 wird sichergestellt, dass das Puzzlebild erfolgreich aufgeteilt wurde. Dazu werden die einzelnen Kacheln in genau der Reihenfolge angezeigt, in der sie erfasst (aufgeteilt) wurden.

Konvertieren von Bildsegmenten in SVG

Da wir nun Bildsegmente (d. h. Kacheln) erstellen können, befassen wir uns als Nächstes damit, wie wir diese URL-Datenbildkacheln in SVG-Bildobjekte konvertieren können. Dies wird in Beispiel 3 veranschaulicht. Damit Sie die Ausgabe von Beispiel 3 sehen können, muss das Debuggingkonsolenfenster des Browsers geöffnet sein. Weitere Informationen finden Sie unter Verwenden der Entwicklertools (F12) zum Debuggen von Webseiten. Der wesentliche Unterschied zwischen Beispiel 2 und 3 besteht in der Erstellung und Festlegung der SVG-Bildelemente. Siehe dazu den folgenden Code:


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

Da SVG ein XML-Format ist, muss beim Erstellen eines SVG-Elements ein Namespace angegeben werden (zumindest außerhalb des SVG-Objektmodells):


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

SVG-Bildelemente verwenden im Gegensatz zum src-Attribut (das mit dem <img>-Nicht-SVG-Element verwendet wird) ein href-Attribut. Beachten Sie auch, dass svgImage.setAttribute('href', _canvas.toDataURL()) für Internet Explorer zum Festlegen des href-Attributs der SVG-Bildelemente verwendet werden kann. Andere Browser benötigen jedoch möglicherweise die XLink-Syntax. Daher wird stattdessen der folgende Code verwendet:


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

Der Standardwert für width und height eines SVG-Bilds lautet jeweils 0, sodass wir diese Werte ausdrücklich festlegen müssen:


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

Zuletzt erstellen wir in Bezug auf das dem SVG-Viewport zugeordnete Koordinatensystem (siehe SVG-Koordinatentransformationen) zwei benutzerdefinierte Eigenschaften: correctX und correctY, und legen sie anschließend fest, um zu erfassen, an welcher Position sich jede Kachel jeweils in einem korrekten (d. h. gewonnenen) Puzzlespiel befindet.

Anzeigen von SVG-Bildern

Nachdem wir nun die im Kachelarray gespeicherten SVG-Bilder positioniert haben, müssen wir sie auf dem Bildschirm anzeigen. Dies können wir auf einfache Weise umsetzen, indem wir der Webseite ein <svg>-SVG-Element sowie einige weitere Augmentationen hinzufügen, die in den nächsten drei Abschnitten besprochen werden. Siehe dazu Beispiel 4 (klicken Sie mit der rechten Maustaste, um den Quellcode anzuzeigen).

Flüssiges SVG

Wir fügen nicht nur ein SVG-Element hinzu, sondern verwenden auch einige CSS-Eigenschaften, damit der SVG-Viewport vollkommen flüssig (oder fließend) wird. Zunächst müssen wir uns mit dem SVG-Element an sich beschäftigen:


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

Hier beträgt der quadratische SVG-Viewport 75 % der kleinsten Viewportabmessung des Browsers. Zudem wird ein Koordinatensystem mit 400 x 400 Einheiten für den Viewport verwendet. Damit flüssiges SVG wie gewünscht funktioniert, müssen Sie darauf achten, dass die folgenden CSS-Regeln angewendet werden:

  • Für die Elemente html und body muss eine height von 100% festgelegt sein:
    
    html, body {
      margin: 0;
      padding: 0;
      height: 100%
    }
    
    

    Da der Viewport des Browsers jetzt kleiner ist, haben sich auch die Inhalte des SVG-Viewports verkleinert. Beachten Sie, dass das eigene Koordinatensystem mit 400 x 400 Einheiten des SVG-Viewports davon nicht betroffen ist – es ändert sich lediglich die Größe der Koordinateneinheiten.

  • Wie das <img>-Element ist auch das SVG-Element ein Inlineelement. Um es also innerhalb des Viewports des Browsers zu zentrieren, legen wir seine display-Eigenschaft auf block und die Werte left und right für die Ränder auf auto fest:
    
    svg {
      display: block; 
      margin: 0 auto;
    }
    
    

Featureerkennung

Da wir addEventListener, Canvas und SVG verwenden, sollten wir uns mit diesen Features beschäftigen, bevor wir versuchen, ein Puzzle anzuzeigen. In Beispiel 4 wird dies veranschaulicht:


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

Wie Sie sehen können, geben wir lediglich die main-Funktion ein, sofern die addEventListener-Methode unterstützt wird (wenn addEventListener nicht unterstützt wird, ist es übrigens äußerst unwahrscheinlich, dass Canvas oder unterstützt werden).

Nachdem die main-Funktion geöffnet wurde, erstellen wir eine game-Variable, die alle "globalen" Variablen und Statuswerte im Hinblick auf unser Spiel enthalten wird. Wie Sie feststellen können, wird eine entsprechende Meldung angezeigt, wenn der Browser des Anwenders keines der benötigten Features unterstützt.

Wenn wir sicher sind, dass alle benötigten Features unterstützt werden, fahren wir genau wie zuvor in Beispiel 3 fort, außer, dass sich einige der Variablennamen geändert haben, z. B. NUM_ROWS in game.numRows und slicedImageTable in game.tiles. Nachdem wir alle SVG-Bildelemente (Kacheln) in der doppelt verschachtelten for-Schleife erstellt haben, zeigen wir sie an, wie dies im Folgenden beschrieben wird.

Anzeigen von Kacheln

Da die Positionen der einzelnen Kacheln bereits berechnet und festgelegt sind, können wir die SVG-Kachelbilder anzeigen, indem wir sie einfach an das SVG-Element anfügen:


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

Ändern Sie die Größe des Browserfensters, um die Flüssigkeit der SVG-Kacheln zu überprüfen. Beachten Sie dabei, dass der SVG-Viewport immer 75 % der kleinsten Abmessung (Breite oder Höhe) des Viewports des Browsers beträgt.

Verschieben von Kacheln

Da die Puzzlekacheln nun angezeigt werden, besteht unsere nächste Aufgabe darin, festzulegen, dass der Benutzer sie mit einem Zeigegerät verschieben kann. Wir nähern uns dieser Aufgabe an, indem wir zunächst drei SVG-Kreise verschieben. Dies wird in Beispiel 5 erklärt (klicken Sie mit der rechten Maustaste, um den Quellcode anzuzeigen).

Wir erstellen zuerst ein globales Array, in dem angegeben ist, welche Kreise derzeit aktiv sind. Dabei handelt es sich um Kreise, auf die "geklickt" wurde, wie dies durch ein mousedown- oder MSPointerDown-Ereignis angegeben wird:


var _activeCircles = [];

Zur Erläuterung des folgenden Codefragments: Wenn ein mousemove-Ereignishandler direkt an die SVG-Kreise angefügt wird (wie dies vernünftig scheint), kann der Benutzer die Maus (leider) so schnell bewegen, dass ein Kreis "verloren gehen" kann. Das heißt, die Bewegung des SVG-Kreises kann nicht mit den sehr schnellen Mausbewegungen des Benutzers mithalten. Wir gehen davon aus, dass der mousemove-Ereignishandler für das Bewegen eines Kreises auf dem Bildschirm zuständig ist. Wenn also die Maus und der Kreis physisch getrennt werden, wird der mousemove-Ereignishandler nicht mehr ausgeführt. Damit stoppt auch die Bewegung des Kreises. Daher fügen wir den mousemove-Ereignishandler nicht an die SVG-Kreise, sondern an das window-Objekt an – Und egal, wie schnell ein Benutzer die Maus bewegt, das allgegenwärtige window-Objekt kann nie mehr verloren gehen:


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

Wie im vorherigen Codefragment dargestellt, registrieren wir einen Ereignishandler für den MSPointerMove-Zeiger, sofern verfügbar (der auch herkömmliche Mausereignisse behandelt). Andernfalls registrieren wir einen mousemove-Ereignishandler. Denken Sie daran, dass Sie nicht beide Ereignistypen (MSPointerMove und mousemove) für das gleiche Objekt registrieren können (siehe dazu Beschreibung in Beispiel 1).

Anschließend registrieren wir pointer-down- und pointer-up-Ereignishandler für jedes SVG-Kreiselement:


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

Achten Sie besonders auf die handlePointerEvents-Funktion, da wir sie als Nächstes besprechen:


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

Zur Beschreibung von handlePointerEvents befassen wir uns mit zwei Szenarien: Wir verschieben zunächst einen Kreis und dann zwei Kreise gleichzeitig.

Verschieben eines Kreises

Drei Ereignisse müssen behandelt werden: down, move und up.

down-Ereignis

Wenn der Benutzer einen einzelnen Kreis berührt (mit der Maus, einem Stift oder einem Finger), wird das down-Ereignis ausgelöst und ruft handlePointerEvents auf. Wenn Zeigerereignisse unterstützt werden, hat evt.pointerId einen Wert ungleich null und activeCircleIndex ist gleich evt.pointerId. Andernfalls ist activeCircleIndex gleich 0 (dank evt.pointerId || 0). Wenn evt.pointerId gleich null ist, kann nur jeweils ein Kreis aktiv sein, und zwar _activeCircles[0]. Dies ist die einzige Möglichkeit, wenn nur eine Maus als Zeigegerät zulässig ist.

Als Nächstes prüft die switch-Anweisung evt.type und verlagert die Ablaufsteuerung auf die mousedown/MSPointerDown-Klausel. Um sicherzustellen, dass der aktive Kreis sich immer über allen anderen befindet, entfernen wir ihn einfach und fügen ihn an DOM an (das zuletzt angefügte Element wird immer als letztes/oberstes Element gerendert).

Wenn evt.pointerId definiert ist, rufen wir dann msSetPointerCapture für evt.target (d. h. den aktiven Kreis) auf, damit der Kreis weiterhin alle registrierten Ereignisse empfangen kann. Dadurch kann der Kreis physisch vom Viewport des Browsers weg und wieder zurück gezogen werden.

Schließlich erfassen wir den berührten Kreis in der Liste der aktiven Kreise:


_activeCircles[activeCircleIndex] = evt.target;

move-Ereignis

Wenn das Zeigegerät (im window-Objekt) bewegt wird, wird der mousemove/MSPointerMove-Ereignishandler (handlePointerEvents) ausgeführt, der die Ablaufsteuerung auf die mousemove/MSPointerMove-Klausel der switch-Anweisung verlagert. Wenn ein Kreis zuvor nicht berührt wurde, ist das _activeCircles-Array leer und activeCircle = _activeCircles[activeCircleIndex] ist null. In diesem Fall fahren wir direkt mit der break-Anweisung fort und beenden die switch-Anweisung.

Wenn activeCircle hingegen nicht null ist (d. h., ein aktiver Kreis soll verschoben werden), konvertieren wir die Koordinaten des Zeigegeräts (evt.clientX, evt.clientY), die relativ zum Viewport des Browsers sind, in das 400 x 400-SVG-Koordinatensystem. Dies erfolgt durch die Koordinatentransformationsmatrix (CTM). Weitere Informationen finden Sie unter SVG-Koordinatentransformationen.

Zuletzt verschieben wir den Kreismittelpunkt (cx, cy) in die transformierten Koordinatenwerte:


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

up-Ereignis

Wenn ein up-Ereignis für einen Kreis ausgelöst wird, verlagert sich die Ablaufsteuerung auf die mouseup/MSPointerUp-Klausel der switch-Anweisung. Der Kreis, der berührt oder auf den geklickt wurde und der durch activeCircleIndex angegeben wird, wird aus der Liste der aktiven Kreise entfernt, und die zugehörige Anforderung zum Erfassen von Ereignissen wird gegebenenfalls über msRelasePointerCapture freigegeben.

Verschieben zweier Kreise

Wie im Beispiel für einen Kreis müssen drei Ereignisse behandelt werden: down, move und up.

down-Ereignisse

In diesem Fall gehen zwei (nahezu gleichzeitige) circle-down-Ereignisobjekte in die mousedown/MSPointerDown-Klausel der switch-Anweisung ein. Das _activeCircles-Array enthält nun zwei Kreisobjekte (deren Indizes, die auf sie zugreifen, die zugeordneten evt.pointerId-Werte sind).

move-Ereignisse

Wenn die beiden (nahezu gleichzeitigen) window-move-Ereignisobjekte in die mousemove/MSPointerMove-Klausel der switch-Anweisung eingehen, wird wiederum jeder der beiden Kreise bewegt, wie dies zuvor auch der Fall des einen Kreises war.

up-Ereignisse

Da jedes (der beiden nahezu gleichzeitigen) circle-up-Ereignisobjekt in die mouseup/MSPointerUp-Klausel eingeht, werden genau wie beim Beispiel für einen Kreis wiederum beide Kreise jeweils bereinigt.

Bildpuzzle

Wir haben nun alle wesentlichen Bestandteile zum Erstellen eines voll funktionsfähigen (aber nicht unbedingt fesselnden) Bildpuzzlespiels für die Fingereingabe gesammelt. Wir untergliedern diese recht lange Beschreibung in mehrere leicht verdauliche Abschnitte. Wir beginnen mit Beispiel 6, dem Grundgerüst für das Spiel (das fertiggestellte Spiel wird in Beispiel 7 vorgestellt und später erläutert).

Grundgerüst

Im Gegensatz zu dem unter SVG-Animation für Experten vorgestellten Spiel ist das Grundgerüst für das Puzzle relativ einfach, wie Sie in Beispiel 6 sehen können (klicken Sie mit der rechten Maustaste, um den Quellcode anzuzeigen). Das Kernmarkup von Beispiel 6 sieht wie folgt aus:


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

Mithilfe des <rect>-Elements wird der gesamte SVG-Viewport schwarz gefärbt (unabhängig von seiner aktuellen Größe). Dadurch wird ein "Spielfeld" für die Kachelteile des Puzzles geschaffen. Diese Änderung können Sie im folgenden Screenshot für Beispiel 6 sehen:

Screenshot mit dem "Spielfeld" für die Kachelteile des Puzzles

Das Grundgerüst für Beispiel 6 besteht weiterhin aus dem folgenden JavaScript-Code:


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

Wie im Abschnitt zur Featureerkennung weiter oben beschrieben, lösen wir die main-Funktion aus, wenn addEventListener verfügbar ist. Dann initialisieren wir das imageList-Array, das die Pfade zu den im Spiel zu verwendenden Bildern enthält (d. h. den Bildern, die in Kacheln unterteilt, zufällig angeordnet und richtig gelegt werden sollen). Wenn der Benutzer es bis zum siebten Level schafft, wird wieder das erste Bild (puzzle0.png) verwendet. – Dies geht endlos so weiter.

Wir rufen dann die Game-Konstruktorfunktion auf. Der erste Parameter 2 weist den Konstruktor an, ein game-Objekt mit zwei Spalten und zwei Zeilen zu generieren. Der letzte Parameter ist natürlich die Liste der Puzzlebilder, die (gegebenenfalls) durchlaufen werden kann.

Im Konstruktor platzieren wir alle zugehörigen "globalen" Variablen in einer nützlichen gameData-Variable. Wenn Sie mit dem this-Schlüsselwort nicht vertraut sind, erstellt und legt die this.hasCanvas = !!game.elements.canvas.getContext-Anweisung die Eigenschaft mit dem Namen hasCanvas für das Objekt fest, das der Konstruktor erstellt hat (die game-Variable). Die doppelte Negierung (!!) legt für den game.elements.canvas.getContext-Ausdruck zwingend einen booleschen Wert fest (true, wenn Canvas unterstützt wird).

Ähnlich definiert this.init = function() { … } die Methode mit dem Namen init für alle Objekte, die vom Konstruktor erstellt wurden (es gibt nur ein solches Objekt, und zwar game). Durch Aufrufen von game.init() wird das Spiel u. a. gestartet.

Bildpuzzlespiel für die Mehrfacheingabe

Wir sind nun in der Lage, alle oben genannten Informationen in ein voll funktionsfähiges Bildpuzzlespiel für die Mehrfingereingabe einfließen zu lassen, wie dies in Beispiel 7 dargestellt ist. Der in Beispiel 7 verwendete Quellcode sollte Ihnen bekannt vorkommen und ist zudem gut kommentiert. Die folgenden zwei Komponenten sind jedoch wahrscheinlich verständlicher mit näheren Erklärungen:

Zufällige Anordnung der Kacheln

In der createAndAppendTiles-Funktion generieren wir die SVG-Kachelbildobjekte (wie in Beispiel 4). Aber bevor wir sie an das SVG-Element anfügen, ordnen wir sie zufällig an und stellen sicher, dass das zufällig angeordnete Muster nicht genau mit dem ursprünglichen (vollständigen) Puzzlebild übereinstimmt (dies betrifft eigentlich nur den ersten Level mit 4 Kacheln):


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

Um die Kacheln auf einfache Weise zufällig anzuordnen, positionieren wir ihre zugeordneten Koordinatenpaare in einem Array (coordinatePairs) und verwenden die JavaScript-Methode zum Sortieren von Arrays wie folgt:



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

Wie unter sort-Methode (JavaScript) beschrieben und vorausgesetzt, dass Math.random() einen Wert zwischen 0 und 1 zurückgibt, werden die Elemente des Arrays mit dieser anonymen Sortierfunktion in zufälliger Reihenfolge sortiert.

Erweiterte pointer-up-Klausel

Die down- und move-Klauseln in der switch-Anweisung sind fast identisch mit den vorherigen Beispielen. Die up-Klausel hat sich jedoch wesentlich erweitert:



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;

Die erste Komponente der up-Klausel, die wir besprechen, betrifft das Andocken der Kacheln. Bei vielen Puzzlespielen (auch bei diesem) muss ein frei schwebendes Puzzleteil (Kachel) andocken, wenn es in die entsprechende Nähe seiner korrekten Position bewegt wird. Der dazu verwendete Code, der im vorhergehenden Beispiel kopiert wurde, ist hier dargestellt:


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

Mit diesem Codefragment rufen wir die Position der aktiven Kachel ab (wenn das up-Ereignis ausgelöst wird) und durchlaufen alle Kacheln, um jeweils die korrekte Position für die einzelnen Kacheln zu bestimmen. Wenn die Position der aktiven Kachel "nah genug" an einer dieser korrekten Positionen ist, docken wir sie an und unterbrechen sofort die for-Schleife (daher muss nicht auf die anderen korrekten Positionen geachtet werden). Die if-Klausel zeichnet bildlich ein kleines Kollisionserkennungsfeld um den Mittelpunkt einer korrekten Position. Wenn die Position der aktiven Kachel in diesen Bereich fällt, ändert sich der Wert der Klausel in true, und die Kachel wird über die folgenden beiden Aufrufe der setAttribute-Methode angedockt:


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

Beachten Sie dabei, dass sich beim Erhöhen von gameData.snapDelta auch das Kollisionserkennungsfeld vergrößert, sodass die Kacheln weniger schnell andocken.

Wenn das Spiel derzeit ausgeführt wird, überprüfen wir als Nächstes, ob die Positionierung der aktiven Kachel die endgültige und korrekte Position ist, indem wir alle Kacheln durchlaufen und dabei die Brute-Force-Methode anwenden: 


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

Wenn sich nicht alle Kacheln in ihrer jeweils korrekten Position befinden, unterbrechen wir sofort handlePointerEvents und warten, dass handlePointerEvents durch das nächste Zeigerereignis ausgelöst werden. Andernfalls hat der Benutzer das Puzzle gelöst, und der folgende Code wird ausgeführt:


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

Da dieser Level des Spiels erreicht wurde, legen wir gameData.inProgress auf false fest und zeigen den neuen Punktestand an. Vorausgesetzt, dass die aktuelle Größe (die Anzahl der Zeilen und Spalten) des Spiels auch zum Angeben des aktuellen Levels des Spiels verwendet wird (d. h., wie viele Puzzles der Benutzer bisher gelöst hat), und da der Schwierigkeitsgrad des Spiels proportional zur quadratischen Fläche seiner Zeilenanzahl (oder Spaltenanzahl, da beide gleich sind) ist, wird der Punktestand um die Fläche der aktuellen Größe des Puzzlespiels erhöht:


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

Wir erhöhen dann den Spiellevel gameData.size und zeigen einen zufällig ausgewählten Spruch aus einem Array mit möglichen Sätzen zum Spielgewinn des Benutzers an:


++(gameData.size);

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

Schließlich entfernen wir alle eventuell vorhandenen SVG-Bildelemente (Kacheln), die an das SVG-Element angefügt sind. Damit wird die nächste Runde mit (anderen) SVG-Bildelementen vorbereitet, die erstellt und über createAndAppendTiles an das SVG-Element angefügt werden:


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

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

 

Unser Ziel war es, Ihnen in diesem Lernprogramm zu zeigen, wie Sie Mehrfingereingabeereignisse in einem halbwegs realistischen Szenario – und zwar der Implementierung eines für die Fingereingabe geeigneten Puzzlespiels – behandeln können. Sie sollten jetzt über ausreichend Kenntnisse verfügen, um Fingereingabeereignisse unter verschiedenen Bedingungen zu behandeln (eventuell auch diejenigen mit Canvas und SVG).

Verwandte Themen

Contoso Images-Fotogalerie
Gestikereignisse
Richtlinien für das Erstellen von Websites für die Fingereingabe
So wird's gemacht: Simulieren von "Darauf zeigen" auf Geräten, die die Fingereingabe unterstützen
HTML5-Grafiken
Beispiele und Lernprogramme zu Internet Explorer
Zeigerereignisse
Updates für Zeigerereignisse

 

 

Anzeigen:
© 2014 Microsoft