Fallstudie - Einführung in HTML5 Canvas

Derek Detweiler
Derek Detweiler

Einleitung

Im letzten Frühjahr 2010 interessierte ich mich für die schnell wachsende Unterstützung für HTML5 und verwandte Technologien. Damals hatten uns ein Freund und ich mich in zweiwöchigen Wettbewerben zur Spieleentwicklung gestellt, um unsere Programmier- und Entwicklungsfähigkeiten zu verbessern und unsere Spieleideen zum Leben zu erwecken. Also begann ich, HTML5-Elemente in meine Konkurrenzbeiträge zu integrieren, um ein besseres Verständnis ihrer Funktionsweise zu erlangen und Dinge zu tun, die mit früheren HTML-Spezifikationen so gut wie nicht möglich waren.

Von den vielen neuen Funktionen in HTML5 bot mir die zunehmende Unterstützung für das Canvas-Tag eine interessante Gelegenheit, interaktive Kunstwerke mithilfe von JavaScript zu implementieren. Das hat mich dazu bewogen, ein Puzzlespiel namens Entanglement zu implementieren. Ich hatte bereits einen Prototyp mit der Rückseite von Siedler von Catan-Kacheln erstellt. Da ich ihn als Vorlage nutzte, gibt es drei wesentliche Bestandteile für die Erstellung der sechseckigen Kachel auf dem HTML5-Canvas für das Web-Spiel: das Zeichnen des Sechsecks, das Zeichnen der Pfade und das Drehen der Kachel. Im Folgenden wird ausführlich erläutert, wie ich diese einzelnen Punkte in ihrer aktuellen Form erreicht habe.

Das Sechseck zeichnen

In der Originalversion von Entanglement habe ich verschiedene Canvas-Zeichenmethoden verwendet, um das Sechseck zu zeichnen. In der aktuellen Form des Spiels wird jedoch drawImage() verwendet, um Texturen zu zeichnen, die von einem Sprite Sheet abgeschnitten wurden.

Sprite Sheet für Kacheln
Kachel-Sprite Sheet

Ich habe die Bilder in einer einzigen Datei kombiniert, sodass es nur eine einzige Anfrage an den Server und nicht in diesem Fall zehn ist. Um ein ausgewähltes Sechseck auf den Canvas zu zeichnen, müssen wir die Tools Canvas, Kontext und Bild zusammenstellen.

Zum Erstellen eines Canvas benötigen wir lediglich das Canvas-Tag in unserem HTML-Dokument:

<canvas id="myCanvas"></canvas>

Ich gebe eine ID an, damit wir sie in unser Skript laden können:

var cvs = document.getElementById('myCanvas');

Zweitens müssen wir den 2D-Kontext für den Canvas abrufen, damit wir mit dem Zeichnen beginnen können:

var ctx = cvs.getContext('2d');

Und schließlich brauchen wir das Bild. Wenn sie den Namen „tiles.png“ im selben Ordner wie unsere Webseite hat, können wir sie folgendermaßen abrufen:

var img = new Image();
img.src = 'tiles.png';

Jetzt haben wir die drei Komponenten und können mit ctx.drawImage() das gewünschte Sechseck vom Sprite Sheet zum Canvas zeichnen:

ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

In diesem Fall verwenden wir das vierte Sechseck von links in der obersten Zeile. Außerdem wird es auf dem Canvas in der oberen linken Ecke gezeichnet, wobei die Größe des Originals beibehalten wird. Wenn die Sechsecke 400 Pixel breit und 346 Pixel hoch sind, sieht das Ganze insgesamt etwa so aus:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';
var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

Wir haben einen Teil des Bildes erfolgreich in den Canvas kopiert. Das Ergebnis ist:

Sechseckige Kachel
Sechseckige Kachel

Pfade zeichnen

Unser Sechseck auf dem Canvas ist jetzt ein paar Linien. Zunächst sehen wir uns einige Geometrie für die Sechseckkacheln an. Wir möchten zwei Linienenden pro Seite, die jeweils 1/4 von den Enden entlang der Kante und 1/2 der Kante voneinander entfernt enden. Dies sieht so aus:

Linienendpunkte auf sechseckigen Kachel
Linienendpunkte auf sechseckigen Kacheln

Wir möchten auch eine schöne Kurve. Also habe ich nach ein wenig Versuch und Irrtum festgestellt, dass, wenn ich von der Kante an jedem Endpunkt eine senkrechte Linie erstelle, die Schnittmenge von jedem Endpunktpaar um einen bestimmten Winkel des Sechsecks einen schönen Bézier-Kontrollpunkt für die gegebenen Endpunkte darstellt:

Kontrollpunkte auf der sechseckigen Kachel
Kontrollpunkte auf sechseckigen Kacheln

Jetzt ordnen wir sowohl die Endpunkte als auch die Kontrollpunkte einer kartesischen Ebene zu, die dem Canvas-Bild entspricht. Jetzt können wir zum Code zurückkehren. Der Einfachheit halber beginnen wir mit einer Zeile. Zuerst zeichnen wir einen Pfad vom oberen linken Endpunkt zum unteren rechten Endpunkt. Unser früheres Sechseckbild hat 400 × 346 Pixel, sodass der obere Endpunkt 150 Pixel breit ist und 0 Pixel nach unten, eine Kurzschrift (150, 0). Als Kontrollpunkt wird (150, 86) verwendet. Der Endpunkt am unteren Rand ist (250, 346) mit einem Kontrollpunkt von (250, 260):

Koordinaten der ersten Bézierkurve
Koordinaten für die erste Bézierkurve

Mit unseren Koordinaten können wir nun mit dem Zeichnen beginnen. Wir beginnen mit ctx.beginPath() neu und wechseln dann mit folgendem Befehl zum ersten Endpunkt:

ctx.moveTo(pointX1,pointY1);

Dann können wir die Linie selbst mithilfe von ctx.bezierCurveTo() wie folgt zeichnen:

ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);

Da die Linie einen schönen Rahmen haben soll, werden wir diesen Pfad zweimal mit einer anderen Breite und Farbe streichen. Die Farbe wird mit der Eigenschaft ctx.strokeStyle und die Breite mit ctx.lineWidth festgelegt. Insgesamt sieht die erste Zeile so aus:

var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.beginPath();
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

Wir haben jetzt eine sechseckige Kachel, bei der sich die erste Linie durchquert:

Einzelne Linie auf sechseckigen Kachel
Einzelne Linie auf sechseckigen Kacheln

Wenn Sie die Koordinaten für die anderen zehn Endpunkte sowie die entsprechenden Kontrollpunkte für die Bézierkurve eingeben, können wir die obigen Schritte wiederholen und eine Kachel in etwa wie folgt erstellen:

Sechseckige Kachel fertiggestellt.
Sechseckige Kachel fertiggestellt

Canvas drehen

Sobald wir unsere Kachel haben, möchten wir sie drehen können, sodass unterschiedliche Pfade im Spiel gewählt werden können. Dazu verwenden wir ctx.translate() und ctx.rotate(). Wir möchten, dass sich die Kachel um ihren Mittelpunkt dreht. Deshalb verschieben wir zuerst den Canvas-Referenzpunkt in die Mitte der sechseckigen Kachel. Dazu verwenden wir:

ctx.translate(originX, originY);

Dabei ist originX die Hälfte der Breite der sechseckigen Kachel und originY die Hälfte der Höhe. Daraus ergibt sich:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);

Jetzt können wir die Kachel mit unserem neuen Mittelpunkt drehen. Da ein Sechseck sechs Seiten hat, möchten wir es um ein Vielfaches von Math.PI geteilt durch 3 drehen. Der Einfachheit halber gehen wir mit einer einzigen Umdrehung im Uhrzeigersinn vor:

ctx.rotate(Math.PI / 3);

Da unser Sechseck und unsere Linien jedoch die alten Koordinaten (0,0) als Ursprung verwenden, müssen wir nach dem Drehen vor dem Zeichnen eine Umwandlung zurückübersetzen. Insgesamt haben wir jetzt:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

Wenn die obige Übersetzung und Drehung vor dem Renderingcode platziert wird, wird jetzt die gedrehte Kachel gerendert:

Gedrehte sechseckige Kachel
Gedrehte sechseckige Kachel

Zusammenfassung

Oben habe ich einige der Funktionen hervorgehoben, die HTML5 bei der Verwendung des Canvas-Tags bietet, darunter das Rendern von Bildern, das Zeichnen von Bézier-Kurven und das Drehen des Canvas. Das HTML5-Canvas-Tag und die zugehörigen JavaScript-Zeichentools für Verstrickungen haben sich als angenehm erwiesen und ich freue mich auf die vielen neuen Anwendungen und Spiele, die andere mit dieser offenen und neuen Technologie entwickeln.

Codereferenz

Alle oben aufgeführten Codebeispiele werden im Folgenden als Referenz kombiniert:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

ctx.beginPath();
var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 250;
pointY1 = 0;
controlX1 = 250;
controlY1 = 86;
controlX2 = 150;
controlY2 = 86;
pointX2 = 75;
pointY2 = 43;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 150;
pointY1 = 346;
controlX1 = 150;
controlY1 = 260;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 43;
controlX1 = 250;
controlY1 = 86;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 130;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 25;
pointY1 = 130;
controlX1 = 100;
controlY1 = 173;
controlX2 = 100;
controlY2 = 173;
pointX2 = 25;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 303;
controlX1 = 250;
controlY1 = 260;
controlX2 = 150;
controlY2 = 260;
pointX2 = 75;
pointY2 = 303;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();