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

s

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