SVG Verstehen und Animieren
Einführung in das Dateiformat SVG und Animationen mit JavaScript
SVG ist der offizielle Webstandard zur Beschreibung zweidimensionaler Vektorgrafiken. Im Gegensatz zu Pixelgrafiken können Vektorgrafiken verlustfrei beliebig skaliert und verzerrt werden. Für Zeichnungen und schematische Grafiken, die aus geometrischen Formen zusammengesetzt sind, sind Vektorgrafiken eine sehr platzsparendes Dateiformat. Darüber hinaus können SVGs in Websites eingebettet und über JavaScript und CSS manipuliert werden. Während HTML viele historische Altlasten mitbringt, ist SVG ein sehr aufgeräumtes Format, das von allen relevanten Browsern standardkonform umgesetzt wird.
SVG ist ein Textformat, das auf XML basiert. Im Rahmen des Vortrags wird praktisch demonstriert werden, wie man mit einem Texteditor SVG-Grafiken erstellen und mit JavaScript im Browser animieren kann. Wir beginnen mit einfachen Formen und Pfaden und lernen, wie aus diesen komplexe Grafiken und User Interfaces zusammengesetzt werden können. Dazu betrachten wir unter anderem Konzepte wie Masken, Clipping und Pfadtransformationen, die von SVG nativ unterstützt werden. Es ist kein Vorwissen, aber verstärktes Interesse an Programmiersprachen und Dateiformaten notwendig. Es geht nicht primär um Fragen der graphischen Gestaltung oder Grafikprogramme, sondern ausschließlich um das Format SVG und dessen Einsatz und Kombination mit Webtechniken.
Vortrag im Rahmen der Night of Open Knowledge im November 2019 in Lübeck.
Einleitung
Ziele dieses Vortrags
- Vektor-Grafiken lieben lernen
- Pfade und Bezier-Kurven verstehen
- SVG-Dateien selber lesen und schreiben können
- Kennen lernen, wie man SVGs mit JavaScript animieren kann
Gliederung
Erstes Beispiel
Vereinfachtes Beispiel einer SVG-Datei:
<svg width="100" height="100">
<circle cx="50" cy="50" r="50"/>
</svg>
Beispiele für SVG
- Google-Logo
- Font-Awesome Icons
- Cliparts bei publicdomainvectors.org
- NooK-Emblem und Hintergrundbild
- Karte des kulturhistorischen Wanderwegs durch Neuendeich
Vorteile / Eigenschaften von SVG
- XML-Dateiformate, d.h. relative einfaches menschenlesbares Textformat mit klar definierter Syntax
- Es gibt ein XML-Schema für SVG Version 1.1
- Scaleable Vector Graphics (SVG) sind skalierbare Vektorgrafiken
- Vektorgrafiken sind keine Fotos
- Grafiken können aus semantisch sinnvoll gruppiert und benannten Formen zusammengesetzt werden.
- Diese semantischen Elemente können später wieder editiert und animiert werden
- Browser beherrschen SVG seit vielen Jahrzehnten
- Besser als HTML für Grafiken
- 2D-Leinwand mit Koordinaten statt Fließtext
- Kein verwirrendes Box-Model oder Floats
- Teilgrafiken können unabhängig gerendert werden
- Viele Grafikprogramme können SVG exportieren
- Inkscape kann SVG editieren
Grundlagen
Geometrische Formen
Wir verwenden SVG Preview für Brackets, um die Auswirkungen von Quellcode-Änderungen live sehen zu können.
- Command + Shift + H blendet die Sidebar aus
- Command + Ctrl + F für Fullscreen
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 20 20"
width="300" height="300"
xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="5"/>
</svg>
The value of the
viewBox
attribute is a list of four numbersmin-x
,min-y
,width
andheight
, separated by whitespace.
Einheiten der Attribute cx
und cy
von circle
ergänzen. Ohne Einheit entspricht der Einheit px
. Andere Mögliche Einheiten pt
, mm
, …
Füge fill="blueviolet" stroke="#00AA99"
hinzu. Setze fill="none"
.
Ergänze einen weiteren Kreis
<circle cx="12" cy="14" r="5" fill="red"/>
Verschiebe den Kreis durch Änderung der Koordinaten. Zeige, dass der Umriss des ersten Kreise genau halb überdeckt wird, wenn sich beide Kreise genau übereinander befinden.
Ändere den zweiten Kreis in eine ellipse
. Ersetze r
durch rx
und ry
.
Mache aus ellipse
ein rect
. Ersetze cx
und cy
durch x
und y
, sowie rx
und ry
durch width
und height
.
Ergänze rx
am Rechteck, um runde Ecken zu aktivieren. (Wenn nicht angegeben, wird für ry
der Wert von rx
übernommen.)
Ergänze ein Polygon zum Rechteck
<rect x="5" y="5" width="10" height="5" />
<polygon points="5,11 5,15 15,18 15,11" />
Ergänze eine Linie (statt des Polygons) zum Rechteck
<rect x="5" y="5" width="10" height="5" />
<line x1="5" y1="12" x2="15" y2="12" stroke="black" stroke-width="2" />
Ergänze das Attribut stroke-linecap
mit den Werten butt
(default), square
und round
.
Pfade
Wir beginnen mit linearen Pfaden:
<path d="M5,5 L10,10 m0,-5 h4 v4 z" fill="none" stroke="black" />
M 5,5
move – bewege den Cursor an die KoordinateL 10,10
line - gerader Strich an die Koordinatem 0,-5
– bewege den Cursor um 5 nach oben- Großbuchstaben für absolute und Kleinbuchstaben für relative Koordinaten
h 4
horizontal line – Strich um 4 nach rechtsv 4
vertical line – Strich um 4 nach untenl-4,-4
– Dreieck vollendenz
– Vollendet ebenfalls das Dreieck, schließt dabei allerdings den Pfad
Die Bézierkurve ist eine parametrisch modellierte Kurve, die ein wichtiges Werkzeug bei der Beschreibung von Freiformkurven darstellt.
<path d="M4,8 c4,4 8,4 12,0" fill="none" stroke="black" />
c4,4 8,4 12,0
cubic curve – Zwei Kontrollpunkte und der Zielpunkt- Relative Koordinaten sind alle relativ zur letzten Koordinate, nicht zu einander
<path d="M4,8 c4,4 8,4 12,0 c-4,-4 -8,-4 -12,0 z" fill="none" stroke="black" />
c-4,-4 -8,-4 -12,0
– die relativen Koordinaten sind alle negiert, sodass wir ein Auge erhalten.z
– Pfad schließen
Inkscape
- Datei in Inkscape öffnen und an den Pfaden ziehen.
- Wir speichern die Datei als Inkscape SVG und sind kurz erschreckt über die ganzen Metadaten im XML.
- Wir speichern die Datei als Optimized SVG und sehen, dass nun (je nach Konfiguration) alle zusätzlichen Metadaten weg sind.
- Wir machen alle Änderungen rückgängig, markieren den rechten Knoten und drücken Make selected nodes smooth (Shift+S) in den Tool Controls des Node Tools. Jetzt kann man die beiden Anfasser dieses Punktes nur noch so verdrehen, sodass sie beide in einer Linie liegen, also die Kurve keinen Sprung hat.
- Wir markieren den rechten Knoten und drücken Make selected nodes symmetric (and smooth) (Shift+Y) in den Tool Controls des Node Tools. Jetzt kann man die beiden Anfasser dieses Punktes nur noch so verschieben, dass sie beide gleich lang sind.
-
Wir speichern die Datei als Optimized SVG und sehen, dass Inkscape folgenden Pfad erzeugt hat:
<path d="m4 8c4 4 12 5.7 12 0 0-5.7-8-4-12 0z" fill="none" stroke="#000"/>
- Wir ergänzen wieder Kommata und Leerzeichen und das redundante zweite
c
und erkennen wieder Struktur:
m4,8 c4,4 12,5.7 12,0 c0,-5.7 -8,-4 -12,0 z
- Statt des zweiten
c
kann man auch eins
verwenden, und den ersten Kontrollpunkt weglassen. Dann wird der letzte vorherige Kontrollpunkt invertiert:
m4,8 c4,4 12,5.7 12,0 s-8,-4 -12,0 z
Interessante SVG-Elemente
Polyline
Neues Beispiel, wir verwenden polyline
, um einen Graphen zu plotten:
<polyline points="2,2 2,18 18,18" fill="none" stroke="black" />
<polyline points="4,8 8,6 10,12 12,10 16,15" fill="none" stroke="grey" />
Marker
Wir wollen die einzelnen Punkte jeweils durch einen Kreis hervorheben:
<circle cx="5" cy="5" r="5" fill="red" />
Wir verschieben den Kreis in einen Definitionsbereich, um einen Marker zu definieren:
<defs>
<marker id="dot" viewBox="0 0 10 10" refX="5" refY="5"
markerWidth="2" markerHeight="2">
<circle cx="5" cy="5" r="5" fill="red" />
</marker>
</defs>
Jetzt kann der Marker an alle Verbindungspunkte des Plots gesetzt werden:
marker-mid="url(#dot)"
Um den Anfang und das Ende auch zu markieren, ergänzen wir: marker-start="url(#dot)" marker-end="url(#dot)"
Jetzt wollen wir Pfeile an die Enden der Koordinaten-Achsen. Dafür zeichnen wir ein Dreieck:
<path d="M 0 0 L 10 5 L 0 10 z" fill="blue" />
Auch daraus machen wir einen Marker:
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5"
markerWidth="3" markerHeight="3"
orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="blue" />
</marker>
Diesen Marker setzen wir jetzt als Anfangs- und End-Markierung für die Koordinatenachsen:
marker-start="url(#arrow)" marker-end="url(#arrow)"
Man beachte die Option orient="auto-start-reverse"
an diesem Marker, der dafür sorgt, dass die Pfeile automatisch in die richtige Richtung zeigen.
Gruppen
Wir zeichnen zwei Linien
<line x1="10" y1="5" x2="10" y2="10" stroke="red"
stroke-linecap="round" stroke-width="2" />
<line x1="10" y1="10" x2="10" y2="15" stroke="blue"
stroke-linecap="round" stroke-width="2" />
Wenn wir jetzt beide Linien halbtransparent darstellen, in dem wir jeweils opacity="0.5"
ergänzen, dann sehen wir in der Mitte eine Mischung aus rot und blau.
Wenn wir stattdessen beide Linien gruppieren und die Gruppe halbtransparent darstellen, dann sehen wir in der Mitte nur blau:
<g opacity="0.5">
<line x1="10" y1="5" x2="10" y2="10" stroke="red"
stroke-linecap="round" stroke-width="2" />
<line x1="10" y1="10" x2="10" y2="15" stroke="blue"
stroke-linecap="round" stroke-width="2" />
</g>
Transformationen
Wir ergänzen an der Gruppe
transform="rotate(50,10,5)"
wodurch beide Linien in der Gruppe um 50 Grad linksrum um den Punkt (10,5) rotiert werden.
Wir ergänzen am blauen Strich
transform="rotate(-50,10,10)"
wodurch diese Linie um 50 Grad rechtsrum um den Punkt (10,10) rotiert wird, sodass sie wieder gerade ist.
Grundlegendes Prinzip von Transformationen: Die Kombination dieser beiden Rotationen erspart uns die manuelle Berechnung der entsprechenden Koordinaten.
Es gibt viele weitere Transformationsfunktionen: matrix
, translate
, scale
, rotate
, skewX
und skewY
.
Animationen mit JavaScript
Wir legen eine HTML-Datei an, in der unsere beiden Striche eingebettet sind:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Striche</title>
</head>
<body>
<svg viewBox="-5 -10 30 30" width="500" height="500"
xmlns="http://www.w3.org/2000/svg">
<g id="arm">
<line x1="10" y1="5" x2="10" y2="10" stroke="red"
stroke-linecap="round" stroke-width="2" />
<line x1="10" y1="10" x2="10" y2="15" stroke="blue"
stroke-linecap="round" stroke-width="2" id="blue" />
</g>
</svg>
<script>
console.log(arm);
console.log(blue);
</script>
</body>
</html>
Wir sehen uns im Chrome Debugger die Console an, in der beide Elemente jetzt dargestellt werden.
Wir nutzen hier aus, dass alle Elemente, die im HTML-Dokument ein ID haben, im globalen JavaScript-Objekt als gleichnamiges Attribut verfügbar sind. Wir könnten die Elemente auch über die API des Document Object Models erreichen:
var arm = document.getElementById("arm");
var blue = document.getElementById("blue");
Wir ergänzen:
arm.setAttribute("transform", "rotate(50,10,5)");
blue.setAttribute("transform", "rotate(-50,10,10)");
Manuelle Animation
Wir bauen daraus folgende Animation:
let angle = 0;
setInterval(function () {
angle += 2;
arm.setAttribute("transform", "rotate(" + angle + ",10,5)");
blue.setAttribute("transform", "rotate(" + -angle + ",10,10)");
}, 20);
Durch das Interval von 20 ms ergibt sich eine Framerate von etwa 1000/20 = 50 Hz, sodass eine flüssige Animation entsteht.
Durch leichte Variationen der Rotationen lassen sich verschiedene lustig Effekte erzielen.
User-Interaktion
Wir ergänzen einen Slider:
<p><input type="range" id="position" min="-90" max="90"></p>
Dann ergänzen wir einen Event-Listener, damit wir mitbekommen, wenn der Slider bewegt wurde:
position.addEventListener("change", () => {
console.log(position.value);
});
Wenn wir das mit der Transformation kombinieren, erhalten wir:
position.addEventListener("change", () => {
let angle = position.value;
arm.setAttribute("transform", "rotate(" + angle + ",10,5)");
blue.setAttribute("transform", "rotate(" + -angle + ",10,10)");
});
SVG.js
Wenn wir das ganze mit SVG.js machen, dann geht das etwas komfortabler. Dazu müssen wir als erstes die Library einbinden:
<script src="svg.js"></script>
Als nächstes müssen die beiden Objekte bei SVG.js registriert werden:
let arm = SVG.adopt(window.arm);
let blue = SVG.adopt(window.blue);
Jetzt stehen mehr Funktionen zur Verfügung, sodass wir direkt die Funktion transform
aufrufen können und dieser Parameter mitgeben können, statt die Transformationsfunktion als String zusammenzubasteln:
position.addEventListener("change", () => {
let angle = position.value;
arm.transform({rotate: angle, ox: 10, oy: 5});
blue.transform({rotate: -angle, ox: 10, oy: 10});
});
Animationen mit svg.easing.js
Insbesondere können wir jetzt Animationen ergänzen, in dem wir animate()
in dem Aufruf ergänzen:
arm.animate().transform({rotate: angle, ox: 10, oy: 5});
blue.animate().transform({rotate: -angle, ox: 10, oy: 10});
Die Art der Animation kann durch eine Easing-Funktion konfiguriert werden, die die vergangene Zeit auf die Position abbildet. Dazu wird zunächst die Library svg.easing.js als Plugin von SVG.js geladen:
<script src="svg.easing.js"></script>
Jetzt können wir jeweils direkt nach dem Aufruf von animate()
ein .ease("elastic")
ergänzen.
Maskieren und Filtern mit svg.filter.js
Als finales Beispiel wollen wir das Maskieren von Elementen betrachten.
In diesem Beispiel verwenden wir direkt SVG.js, um die SVG zu erzeugen.
Wir erzeugen eine HTML-Datei, die mit SVG.js eine neue SVG erzeugt und dort ein Foto einbindet. Außerdem laden wir die Library svg.filter.js für erweiterte Filter als zweites Plugin von SVG.js.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Maskieren und Filtern</title>
</head>
<body>
<script src="svg.js"></script>
<script src="svg.filter.js"></script>
<script>
var draw = SVG().addTo('body').size(1600, 900);
draw.image('foto.jpg');
</script>
</body>
</html>
Darüber legen wir jetzt eine Gruppe, in der sich ein schwarzes Rechteck befindet:
let group = draw.group();
group.rect(1600, 900);
In diese Gruppe laden wir darüber nochmal das Bild:
let image = group.image('foto.jpg');
Dieses zweite Bild konvertieren wir aber nun in Graustufen.
image.filterWith(function (add) {
add.colorMatrix('saturate', 0);
});
Und mischen es mit dem darunter liegenden schwarzen Rechteck:
image.opacity(0.3);
Jetzt erzeugen wir eine Maske, die aus einem schwarzen Kreis auf einem weißen Hintergrund besteht:
let mask = draw.mask();
mask.rect(1600, 900).fill("white");
let circle = mask.circle(200).move(100,100).fill("black");
Mit dieser Maske maskieren wir das dunkle Foto, sodass das farbige Foto durch das Loch hindurch scheint:
group.maskWith(mask);
Zum krönenden Abschluss bewegen wir den Kreis mit der Maus:
group.mousemove(function (event) {
circle.move(event.clientX - 100, event.clientY - 100);
});