6. Dynamisches Verhalten im Browser mit Server
6.3 Zeichnen mit SVG
Die wohl einfachste Methode, Grafiken in HTML zu erstellen, ist es, direkt SVG-Objekte einzubinden. Diese schauen im Quelltext aus wie gewöhnliche Html-Elemente. Sie sehen einen Ausschnitt der Datei 01-graphics-svg.html.
<svg viewBox="-50 -50 100 100">
<line x1="-80" y1="-25" x2="80" y2="25" stroke="black" />
<circle cx="0" cy="0" r="20" />
<circle cx="50" cy="0" r="10" />
<circle cx="-50" cy="0" r="10" />
<circle cx="80" cy="0" r="5" />
<circle cx="-80" cy="0" r="5" />
</svg>
Die Svg-Elemente verhalten sich konzeptuell ziemlich so wie Html-Elemente. Wir können sie beispielsweise mit Css gestalten. Ändern Sie den Code:
<circle id="saturn" cx="0" cy="0" r="20" />
und dann fügen Sie unten ein <style>
-Element ein:
<style>
#saturn {
fill: burlywood
}
</style>
Sie können auch Event-Listener definieren. Der Saturn soll auf Knopfdruck seine Farbe ändern:
<script>
counter = 0;
colors = ["burlywood", "chocolate"];
function changeColor() {
counter = (counter + 1) % (colors.length);
document.getElementById("saturn").style.fill = colors[counter];
}
</script>
Statt im Html-Text das onclick
-Attribut zu definieren, können wir auch per
Javascript
die Funktion addEventListener
aufrufen. Dies ist
beispielsweise hilfreich, wenn wir allen cirlce
-Elemente
einen Listener geben wollen:
Code anzeigen
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<title>Drawing with SVG</title>
<style>
svg {
width: 100%;
height: 100%;
border: 1pt red solid;
}
.svgdiv {
width: 100vw;
height: 80vh;
border: 1pt grey solid;
}
</style>
</head>
<body>
<h2>Drawing with SVG</h2>
<div class="svgdiv">
<svg viewBox="-100 -100 200 200">
<line x1="-80" y1="-25" x2="80" y2="25" stroke="black" />
<circle id="saturn" cx=" 0" cy="0" r="20" />
<circle cx="50" cy="0" r="10" />
<circle cx="-50" cy="0" r="10" />
<circle cx="80" cy="0" r="5" />
<circle cx="-80" cy="0" r="5" />
</svg>
</div>
</body>
<script>
colors = ["azure", "burlywood", "chocolate", "gray"];
for (c of document.querySelectorAll("circle")) {
let counter = 0;
let circle = c;
c.addEventListener("click", function () {
counter++;
console.log(`circle clicked; counter is now ${counter}`);
circle.style.fill = colors[counter % colors.length];
});
}
</script>
</html>
Beachten Sie die lokalen Variablen counter
und circle
, die wir in
Zeilen 49 und 50 definieren. Diese existieren innerhalb einer Closure in der anonymen
Funktion
(Zeilen 51-55) weiter und sorgen dafür, dass jeder Kreis seinen eigenen Zähler bekommt.
Das Attribut viewBox
Werfen Sie mal einen Blick auf die Zeile 34:
<svg viewBox="-100 -100 200 200">
.
Dies sind nicht die Maße des Svg-Objekts in Pixeln. In der Tat: wenn
Sie die Größe des Fensters ändern, dann ändert sich die Größe Ihrer Svg-Grafik,
ohne dass wir das extra programmieren mussten. Wir haben die Größe des Svg-Elements
und des umgebenden Divs einfach per Css relativ zur Fenstergröße gesetzt. Was bedeutet
nun also <svg viewBox="-100 -100 200 200">
?
Sie legen das svg-interne Koordinatensystem fest. Ich sage im Prinzip, dass
die linke obere Ecke bei (-100, -100) liegt. Was geschieht nun, wenn ich die erste Zahl,
also -100, erhöhe? Dann schiebt sich das "Svg-Fenster" nach rechts, die Objekte liegen
also relativ zum Svg-Koordinatensystem nun weiter links, und alle Kreise verschieben
sich nach links. Schreiben wir Code, der diese Zahl in einem festen
Intervall auf Knopfdruck erhöht oder verringert:
Code anzeigen
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<title>Drawing with SVG</title>
<style>
svg {
width: 100%;
height: 100%;
border: 1pt red solid;
}
.svgdiv {
width: 100vw;
height: 80vh;
border: 1pt grey solid;
}
</style>
</head>
<body>
<h2>Drawing with SVG</h2>
<div class="svgdiv">
<svg viewBox="-100 -100 200 200">
<line x1="-80" y1="-25" x2="80" y2="25" stroke="black" />
<circle id="saturn" cx=" 0" cy="0" r="20" />
<circle cx="50" cy="0" r="10" />
<circle cx="-50" cy="0" r="10" />
<circle cx="80" cy="0" r="5" />
<circle cx="-80" cy="0" r="5" />
</svg>
</div>
</body>
<script>
let left = -100;
let speedX = 1;
moveXinterval = setInterval(function () {
left = left + speedX;
document.querySelector("svg").setAttribute("viewBox", left + " -100 200 200");
}, 100);
document.querySelector("svg").addEventListener("click", function () {
speedX = -speedX;
});
</script>
</html>
Die dritte Zahl im viewBox
-Attribut beschreibt die Breite
des Svg-Objekts, wieviele "Svg-Einheiten" also die Gesamtbreite sein soll.
Was passiert, wenn wir diesen Wert erhöhen? Dann werden die fünf Kreise
relativ zur Breite in Svg-Einheiten kleiner. Wir zoomen also raus:
<script>
let width = 200;
let height = 200;
let zoomChangeFactor = 1.01;
moveXinterval = setInterval(function () {
width = width * zoomChangeFactor;
height = height * zoomChangeFactor;
document.querySelector("svg").setAttribute("viewBox", `-100 -100 ${width} ${height}`);
}, 100);
document.querySelector("svg").addEventListener("click", function () {
zoomChangeFactor = 1 / zoomChangeFactor;
});
</script>
</html>
Das tut allerdings nicht wirklich das, was wir wollen. Der Fokus
des Zooms ist nicht der Mittelpunkt, sondern die linke obere Ecke.
Denken wir nach: wenn Breite (und Höhe)
und ursprünglich 200 auf 400 angewachsen sind, dann sollte die linke obere
Ecke ja nicht mehr bei (-100, -100) liegen, sondern bei (-200, -200).
Also immer bei (-width/2, -height/2)
:
document.querySelector("svg").setAttribute("viewBox", `${-width / 2} ${-height / 2} ${width} ${height}`);
Das Attribute transform
Nun da Sie verstehen, was viewBox
bedeutet, sollten Sie lieber
die Finger davon lassen. Also: am Anfang definieren, dann aber nicht mehr
ändern. Besser verwenden Sie das Attribut transform
.
Das ist auch menschenlesbarer. Translationen erreichen Sie per
transform="translate(x, y)"
<script>
let x = 0;
let y = 0;
let speedX = 10;
let speedY = 5;
moveXinterval = setInterval(function () {
x = x + speedX;
y = y + speedY;
document.querySelector("svg").setAttribute("transform", `translate (${x}, ${y})`);
}, 100);
document.querySelector("svg").addEventListener("click", function () {
speedX = -speedX;
speedY = -speedY;
});
</script>
Das ist jetzt aber seltsam: das ganze Svg bewegt sich samt Rand und sprengt die Dimensionen unserer Fensters, was so hässlichen Scrollbalken führt. Besser, wir definieren eine Gruppe und geben der das Attribut:
Code anzeigen
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<title>Drawing with SVG</title>
<style>
svg {
width: 100%;
height: 100%;
border: 1pt red solid;
}
.svgdiv {
width: 100vw;
height: 80vh;
border: 1pt grey solid;
}
</style>
</head>
<body>
<h2>Drawing with SVG</h2>
<div class="svgdiv">
<svg viewBox="-100 -100 200 200">
<g id="saturn-and-moons">
<line x1="-80" y1="-25" x2="80" y2="25" stroke="black" />
<circle id="saturn" cx=" 0" cy="0" r="20" />
<circle cx="50" cy="0" r="10" />
<circle cx="-50" cy="0" r="10" />
<circle cx="80" cy="0" r="5" />
<circle cx="-80" cy="0" r="5" />
</g>
</svg>
</div>
</body>
<script>
let x = 0;
let y = 0;
let speedX = 10;
let speedY = 5;
moveXinterval = setInterval(function () {
x = x + speedX;
y = y + speedY;
document.querySelector("#saturn-and-moons").setAttribute("transform", `translate (${x}, ${y})`);
}, 100);
document.querySelector("svg").addEventListener("click", function () {
speedX = -speedX;
speedY = -speedY;
});
</script>
</html>
Jetzt können Sie auch problemlos rotieren, ohne dabei über Sinus und Cosinus nachdenken zu müssen:
<script>
let angle = 0; // in degrees, not radians!
let angleSpeed = 15; // in degrees, not radians!
moveXinterval = setInterval(function () {
angle += angleSpeed
document.querySelector("#saturn-and-moons").setAttribute("transform", `rotate (${angle})`);
}, 200);
document.querySelector("svg").addEventListener("click", function () {
speedX = -speedX;
speedY = -speedY;
});
</script>
Man kann auch um einen anderen Punkt als den
Ursprung rotieren. Probieren Sie mal
setAttribute("transform", `rotate (${angle}, 70, 0)`);
aus.
Sie können auch noch skalieren und verzerren. Lesen Sie sich am Besten mal www.mediaevent.de/tutorial/svg-transform.html durch und arbeiten alles in meine Beispieldatei ein.
Elemente erzeugen
Für interessantere Beispiele können Sie nicht alle Svg-Objekte per Hand im Html-Code schreiben. Sie müssen Sie dynamisch mit Javascript erzeugen. Das geht so:
let circle = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
Danach können Sie mit circle.setAttribute("cx", 200)
oder Analogem ganz normal Attribute setzen. So wie Sie im
Html-Quelltext auch cx="200"
geschrieben haben.
Übungsaufgabe Ändern Sie 01-graphics-svg.html, dass es dynamisch 10 Saturnmonde mit zufälligen Koordinaten erzeugt.
Maus-Koordinaten
sWenn wir die Maus-Koordinaten lesen wollen, beispielsweise jedes Mal, wenn das Svg-Element geklickt worden ist, dann können wir dem Svg-Element selbst einen Eventlistener hinzufügen:
svgElement.addEventListener("click", showMouseCoordinates);
Bei jedem Klick wird dann eine Funktion showMouseCoordinates(event)
aufgerufen. Wenn Sie statt Klicks bei jeder Bewegung einen
Event kriegen wollen, dann können Sie "mousemove"
statt "click"
schreiben. Was macht nun die Funktion
showMouseCoordinates
? Die bekommt als Argument ein
Objekt event
, das alle Informationen über den Mausklick (oder die
Mausbewegung) enthält. Wichtig für uns sind dabei in erster Linie
die Koordinaten:
function showMouseCoordinates(event) {
console.log(event);
let text = `Mouse clicked. The coordinates are
on svg object: (${event.offsetX},${event.offsetY})
on dom: (${event.clientX},${event.clientY})
on layer: (${event.layerX},${event.layerY})
on screen: (${event.screenX},${event.screenY})`;
document.querySelector("#mouse-coordinates").innerText = text;
}
Probieren Sie's aus. Den gesamten Code finden Sie in
04-graphics-svg-mouse.html.
Wir bekommen nicht nur ein Koordinatenpaar, sondern gleich vier Paar.
Schauen Sie mal, welche Bedeutung welches hat. Bei mir
haben screenX
und screenY
auch mal
negative Werte angenommen. Wie kann das passieren?
Übungsaufgabe Speichern Sie 04-graphics-svg-mouse.html und ändern es so ab, dass jeder Klick einen neuer Mond erzeugt.
Hinweis: die Schwierigkeit ist hier natürlich,
dass der Mausklick Ihnen Pixel-Koordinaten liefert, Sie aber
für das Erzeugen eines neuen Svg-Kreises Svg-Koordinaten benötigen,
die Sie in der viewBox
festgelegt haben. Sie müssen also erstmal
Pixel in Svg-Koordinaten umrechnen. Hierfür brauchen Sie unter
anderem die Breite und Höhe des Svg-Elements in Pixels. Diese
bekommen Sie mit svgElement.clientWidth
.
Übungsaufgabe Erweitern Sie den Code, das das Klicken auf einen von Ihnen erzeugten "Mond" dort keinen weiteren Mond erzeugt, sondern die Farbe des Mondes ändert.
Tip: Wenn ein Mond einen Klick erzeugt, werden
standardmäßig auch die Event-Handlers aller oberen Elemente aufgerufen,
also in diesem Fall kriegt auch das gesamte Svg-Element einen
Klick zu spüren; dessen Event-Handler würde dann an dieser Stelle wiederum
einen neuen Mond erzeugen. Man nennt dies "bubbling up" - die Events blubbern
nach oben. Dies stoppen Sie mit
event.stopPropagation();
Übergänge mit Css geschmeidig machen
In der Seite 05-graphics-bounce-ruckelt.html
fliegt ein Ball über das Spielfeld (Bewegung jede Zehntelsekunde). An der Bande
prallt er ab und ändert seine Farbe. Ein Frame pro Zehntelsekunde ist
etwas niedrig, deswegen ruckelt es sehr stark. Um das zu beheben,
können wir das Interval beschleunigen. Die Frage ist allerdings, ob wir das wollen.
Wenn es sich zum Beispiel um ein Web-basiertes Spiel handelt, dann löst vielleicht
jeder Uhrentick eine Kommunikation mit dem Server aus (zum Beispiel über einen
Websocket). Eine Erhöhung der Framerate belastet also nicht nur den Browser mehr,
sondern auch das Netzwerk.Eine andere Möglichkeit ist, das
transition
-Attribut per Css zu definieren:
#spaceship {
transition: cy 0.1s ease-out, cx 0.1s ease-out, fill 1s ease-out;
}
Die Semantik ist in etwa diese: wenn sich das Attribut
cy
des Elements mit der ID spaceship
ändert, dann wird die Änderung nicht sofort angezeigt,
sondern der alte Wert wird über einen Zeitraum von 0.1s
in den neuen geändert. Das gleiche mit cx
.
Bei einer Farbänderung definieren wir mit
fill 1s ease-out
einen gleitenden Übergang
innerhalb von einer Sekunde.