kotori web solutions Maren Arnhold


HTML5: Isometrische Darstellung von 2D-Grafik
mit Hilfe von Transformationsmatrizen

Das in der HTML5-Spezifikation neu aufgenommene <canvas>-Objekt stellt in seinem 2D-Kontext eine Art Zeichenfläche dar, auf deren Eigenschaften lesend und schreibend unter Verwendung von JavaScript zugegriffen werden kann. Angefangen bei grundlegenden grafischen Elementen wie Linien, Rechtecken und Kreisen erlaubt der Canvas (dt. "Leinwand") auch die Konstruktion komplexerer Gebilde wie Bézierkurven sowie die Manipulation von Grafiken bis hin auf die Pixelebene. Die Beschränkung des 2D-Kontexts bringt es allerdings mit sich, dass sämtliche Objekte in einem kartesischen x/y-Koordinatensystem spezifiziert werden, sprich: Eine z-Achse, also die Formulierung von Räumlichkeit durch Tiefe, ist nicht vorhanden.


Abb. 1: Isometrische Darstellung eines Würfels; die Tiefenlinien bilden einen Winkel von 30 Grad zur x-Achse der Projektionsebene.
Für verschiedene Anwendungen kann es aber notwendig oder wünschenswert sein, Inhalte des Canvas perspektivisch verzerrt darzustellen, so dass die Ansicht auf diese Inhalte nicht mehr einer exakten Draufsicht entspricht. Auch wenn wir uns in einem zweidimensionalen Koordinatensystem bewegen, kann ein Tiefeneindruck dennoch dadurch hergestellt werden, dass wir die Koordinaten innerhalb der Leinwand mathematisch - als sogenannte affine Abbildung - so transformieren, dass sie anschließend einer Orthogonalprojektion entsprechen, die eine im Raum gedrehte Ebene samt ihrer Inhalte auf eine andere (die Darstellungsebene, faktisch: auf den Bildschirm) ergeben würde. Wenn wir eine isometrische Abbildung haben wollen, müssen wir dafür sorgen, dass die in die "Tiefe" weisenden Linien einen Winkel von 30 Grad zur x-Achse der Projektionsebene aufweisen (Abb. 1). Die notwendigen Rotations-, Skalierungs- und Scherungsoperationen lassen sich dabei in einer 3x3-Transformationsmatrix zusammenfassen. Diese Transformationsmatrix wird mit den ursprünglichen Koordinaten multipliziert, um die neuen Koordinaten zu ermitteln.

Erstellen wir aber zunächst einmal ein Dokument mit Canvas. Hierzu ist - HTML5-Standards sei Dank - kein komplizierter Doctype mehr notwendig, und das Grundgerüst der Datei, mit der wir im folgenden arbeiten werden, beschränkt sich auf:

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
<!DOCTYPE HTML>
<html>
	<head>
	</head>
	<body>
		<div style="width:100%;text-align:center;">
			<canvas id="mycanvas" width="970" height="500" style="border:1px solid #aaa;">
			</canvas> 
		</div>
	</body>
</html>

Das ist schon alles, was wir an HTML (und ein wenig CSS zum Zentrieren und Hervorheben der Leinwand) brauchen. Der Aufruf der Datei im Browser wird erwartungsgemäß eine leere, grau umrahmte Fläche zeigen. Damit auf dem Canvas etwas passiert, kommt jetzt JavaScript ins Spiel.

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
<script language="JavaScript">
 
	var obj=document.getElementById("mycanvas");
	var ctx=obj.getContext("2d");
 
	ctx.beginPath();			
	ctx.moveTo(10,10);
	ctx.lineTo(30,10);
	ctx.lineTo(30,30);
	ctx.lineTo(10,30);
	ctx.lineTo(10,10);
	ctx.closePath();
	ctx.stroke();
 
</script>


Abb. 2: Unser definierter Pfad in Quadratform.
Dieser Skriptabschnitt wird für die Zwecke dieses Beispiels direkt nach der Canvas-Definition eingefügt und zeichnet ein kleines Quadrat in Standard-Strichstärke und -Farbe. Nachdem der Canvas als DOM-Objekt geholt und mit der Variable ctx dessen 2D-Kontext spezifiziert wird, beginnen wir einen Pfad und bewegen wir den Cursor mit der moveTo-Methode an die Position (10,10); dies ist nur wenig von der oberen linken Ecke des Canvas entfernt, die intern standardmäßig als (0,0) definiert ist.

Im folgenden werden von (10,10) aus Linien zu den weiteren Ecken des Quadrates beschrieben, die letztlich zu dessen oberer linker Ecke zurückführen und mit der .stroke()-Methode schließlich auch gezeichnet werden (bis zum Aufruf von ctx.stroke() findet noch keine sichtbare Darstellung statt). Das Ganze sollte dann aussehen wie in Abbildung 2.

Das Ganze ist natürlich auch kürzer formulierbar, denn wie oben bereits angedeutet, besitzt der 2D-Kontext auch eine Methode, mit der wir Rechtecke direkt auf den Canvas platzieren können. Das kantenweise Konstruieren so grundlegender geometrischer Formen ist also selbstverständlich möglich, aber nicht notwendig. Hier der äquivalente Code:

1:
2:
3:
4:
5:
6:
7:
8:
<script language="JavaScript">
 
	var obj=document.getElementById("mycanvas");
	var ctx=obj.getContext("2d");
 
	ctx.strokeRect(10,10,20,20);
 
</script>

Dies übernimmt sowohl das anfängliche Bewegen des Cursors auf (10,10) als auch die Definition eines 20x20-Quadrates und das Zeichnen der Linien in einem einzigen Methodenaufruf.

Definieren wir jetzt ein aus 10x10 Quadraten zusammengesetztes Gitter. Dies ist mit zwei for-Schleifen leicht zu bewerkstelligen.

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
<script language="JavaScript">
 
	var obj=document.getElementById("mycanvas");
	var ctx=obj.getContext("2d");
	var loopx,loopy;
 
	for (loopx=0;loopx<10;loopx++){
 
		for (loopy=0;loopy<10;loopy++){
 
			ctx.strokeRect(10+(loopx*20),10+(loopy*20),20,20);
 
		};
	}
 
</script>


Abb. 3: Gitternetz aus 10x10 Quadraten von jeweils 20 Pixel Höhe/Breite.
Wir erhalten die Ausgabe aus Abbildung 3. Das sieht schon ein wenig aus wie das Feld eines Brettspiels oder das Koordinatennetz einer großmaßstäbigen Landkarte. Man könnte jetzt die einzelnen Quadrate z. B. mit jeweils eigenen Farben füllen (fillRect()-Methode) oder auch mittels der drawImage()-Methode durch extern eingebundene Grafikdateien ersetzen. Für dieses kleine Tutorial belassen wir es aber beim vorliegenden Gitternetz.


Abb. 4: Die durch setTransform(a,b,c,d,e,f) definierte Transformationsmatrix und deren Anwendung auf Punktkoordinaten im Canvas.
Wie verzerren wir dieses Netz nun dahingehend, dass sich eine isometrische Ansicht ergibt? Die Antwort ist die setTransform()-Methode des 2D-Kontextes. Diese Methode übernimmt sechs Parameter und spezifiziert dadurch die 3x3-Transformationsmatrix, die auf das Koordinatensystem des Canvas multiplikativ angewandt wird (Abb. 4).

Die Grundlagen der Matrizenmultiplikation auszuführen, würde an dieser Stelle zu weit führen; hier sei auf entsprechende einführende Literatur zur linearen Algebra verwiesen. Der Vektor (x',y',1) setzt sich jedenfalls zusammen aus: (x' = ax+cy+e), (y' = bx+dy+f), (1 = 0 + 0 + 1); damit sollte bereits ersichtlich sein, dass die Parameter e und f eine Verschiebung des Koordinatensystems in x- bzw. y-Richtung definieren. Entsprechend dienen die Parameter a und d zur horizontalen bzw. vertikalen Skalierung, b und c zur horizontalen und vertikalen Scherung. Die Kombination aller Parameter erlaubt damit auch eine Rotation des Koordinatensystems.

Drehen wir das Gitternetz aus Abbildung 3 doch einmal um φ = 45 Grad gegen den Uhrzeigersinn. Hierfür setzen wir a = cos(φ), b = -sin(φ), c = sin(φ), d = cos(φ). Der Parameter φ ist dabei im Bogenmaß anzugeben; 45 Grad entsprechen hier π/4 = 0,78534. In JavaScript formulieren wir das nun so:

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
<script language="JavaScript">
 
	var obj=document.getElementById("mycanvas");
	var ctx=obj.getContext("2d");
	var loopx,loopy;
	var a,b,c,d,e,f;
	var angle = (Math.PI)/4; // 45 Grad
 
	a = Math.cos(angle);
	b = Math.sin(angle)*(-1);
	c = Math.sin(angle);
	d = Math.cos(angle);
	e = 0;
	f = 150;
 
	ctx.setTransform(a,b,c,d,e,f);
 
	for (loopx=0;loopx<10;loopx++){
 
		for (loopy=0;loopy<10;loopy++){
 
			ctx.strokeRect(10+(loopx*20),10+(loopy*20),20,20);
 
		};
	}
 
</script>


Abb. 5: Gitternetz, um 45 Grad gegen den Uhrzeigersinn gedreht.
Mit dem Parameter f haben wir das Koordinatensystem gleichzeitig noch um 150 Pixel nach unten verschoben, weil das Gitternetz ansonsten zur Hälfte außerhalb des sichtbaren Bereiches liegen würde (siehe Abbildung 5, der Ursprung des Koordinatensystems liegt nun in der nach links weisenden Ecke des Gitternetzes).

Um nun schließlich aus dem rotierten Netz eine isometrische Ansicht zu generieren, sind die Parameter a bis f ganz ähnlich zu wählen. Wir brauchen als zusätzliche Größe nur noch eine Winkelangabe, nämlich den Winkel, um den die zu projizierende Ebene gegenüber der Projektionsebene um deren x-Achse verdreht sein soll. Diesen Winkel nennen wir einfach α. Entsprechend definieren wir in der Transformationsmatrix a = cos(φ), b = -sin(φ)cos(α), c = sin(φ), d = cos(φ)cos(α).

α ist dabei folgendermaßen zu bestimmen: Bei einem Drehwinkel α der zu projizierenden Ebene von 0 Grad weisen die Tiefenlinien dieser Ebene in der Projektion einen Winkel von 45 Grad zur x-Achse der Projektionsebene auf. Bei einem Drehwinkel α von 90 Grad liegen die Tiefenlinien alle genau in der x-Achse der Projektionsebene, haben also zu ihr in der Projektion einen Winkel von 0 Grad. Wir möchten aber, dass letzterer Winkel 30 Grad beträgt und suchen einen Winkel α, für den sich genau dies ergibt.

Unabhängig vom Winkel α hat das Gitternetz auf dem Bildschirm immer eine Breite von 282,84 Pixel; dies ergibt sich nach dem Satz des Pythagoras aus den Diagonalen eines 200x200-Quadrates. Die sichtbare Höhe des Netzes und damit das Verhältnis von Breite zu Höhe variieren aber mit α, und zwar mit dessen Cosinus. Bei einem α = 60 Grad hat das Netz exakt halbe Höhe (cos(60°) = 0,5)), bei einem α = 75,522 Grad Viertelhöhe, da cos(75,522°) = 0,25. Wir suchen also einen Winkel α, der das Gitternetz so hoch sein lässt, dass der Tangens aus halber Höhe und halber Breite genau 0,577 ist, was tan(30°) entspricht. Wir rechnen also α = arccos(0,577) = 0,9557, was im Gradmaß ungefähr 54,76 Grad entspricht. Diesen Winkel übernehmen wir in unser Programm.

Wenn wir noch eine Skalierung hinzufügen wollen, multiplizieren wir die Parameter a bis d noch jeweils mit dieser Skalierung. Hier das Ganze in JavaScript:

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
<script language="JavaScript">
 
	var obj=document.getElementById("mycanvas");
	var ctx=obj.getContext("2d");
	var loopx,loopy;
	var a,b,c,d,e,f,g;
	var angle = (Math.PI)/4; // 45 Grad
	var alpha = 0.9557;
 
	var scaling = 1;
	var verschiebungx = 0;
	var verschiebungy = 150;
 
	var cs = Math.cos(angle), sn = Math.sin(angle);
 
	g = Math.cos(alpha);
	a = scaling*cs;
	b = g*scaling*-sn;
	c = scaling*sn;
	d = g*scaling*cs;
	e = verschiebungx;
	f = verschiebungy;
 
	ctx.setTransform(a,b,c,d,e,f);
 
	for (loopx=0;loopx<10;loopx++){
 
		for (loopy=0;loopy<10;loopy++){
 
			ctx.strokeRect(10+(loopx*20),10+(loopy*20),20,20);
 
		};
	}
 
</script>


Abb. 6: Isometrische Darstellung des Gitternetzes.
Es ergibt sich die Darstellung aus Abbildung 6 und damit das Ziel dieser kleinen Abhandlung. In einem späteren Tutorial werde ich noch zeigen, wie man die Darstellung interaktiv, also z. B. frei dreh- und kippbar gestalten kann - ebenso wie einen Ansatz zur Realisierung von Zentralperspektive. Unten noch einmal das gesamte Listing.

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
<!DOCTYPE HTML>
<html>
	<head>
	</head>
	<body>
 
		<div style="width:100%;text-align:center;">
			<canvas id="mycanvas" width="970" height="500" style="border:1px solid #aaa;">
			</canvas> 
		</div>
 
		<script language="JavaScript">
 
			var obj=document.getElementById("mycanvas");
			var ctx=obj.getContext("2d");
			var loopx,loopy;
			var a,b,c,d,e,f,g;
			var angle = (Math.PI)/4; // 45 Grad
			var alpha = 0.9557;
 
			var scaling = 1;
			var verschiebungx = 0;
			var verschiebungy = 150;
 
			var cs = Math.cos(angle), sn = Math.sin(angle);
 
			g = Math.cos(alpha);
			a = scaling*cs;
			b = g*scaling*-sn;
			c = scaling*sn;
			d = g*scaling*cs;
			e = verschiebungx;
			f = verschiebungy;
 
			ctx.setTransform(a,b,c,d,e,f);
 
			for (loopx=0;loopx<10;loopx++){
 
				for (loopy=0;loopy<10;loopy++){
 
					ctx.strokeRect(10+(loopx*20),10+(loopy*20),20,20);
 
				};
			}
 
		</script>
 
	</body>
</html>

Autorin: Maren Arnhold


comments powered by Disqus
ANZEIGE
kotori web solutions Maren Arnhold bietet einen Komplettservice rund um Webdesign, Webprogrammierung und Webhosting. Suchen Sie nach einer Lösung für Ihre private Homepage? Möchten Sie ein eigenes Blog betreiben und suchen dafür ein geeignetes CMS und entsprechenden Webspace? Oder interessieren Sie sich für E-Commerce und benötigen einen Webshop? Dann sollten wir uns kennenlernen - eine kostenlose Erstberatung ist selbstverständlich!
© 2013 kotori web solutions Maren Arnhold. Alle Rechte vorbehalten/All rights reserved..