SVG/Tutorials/Einstieg/SVG mit CSS animieren
- 30min
- mittel
- Kenntnisse in
• Grundformen in SVG
• CSS-Animation
In der SVG-Welt können Sie mit SMIL (auszusprechen wie englisch: smile – lächeln) Elemente direkt animieren. Genau so einfach ist die Verwendung der CSS-Eigenschaften transition und animation. Sie haben gegenüber SMIL, das jeweils einem Element als Kindelement zugeordnet wird, den Vorteil, dass CSS-Animationen im Stylesheet einmal definiert und dann mehrfach aufgerufen werden können. Die hier vorgeschlagenen Transformationen laufen schneller, da sie hardwarebeschleunigt sind, d. h. dass rechenintensive Operationen an die GPU ausgelagert werden. So können Animationen glatt und ruckelfrei ausgeführt werden.
Google sieht dies in Verbindung mit der Web Animations API als zukünftige Methode Animationen im Webdesign zu steuern.
Inhaltsverzeichnis
Animationen
Die Eigenschaft animation, genauer gesagt, die darin enthaltende Einzel-Eigenschaft animation-name, verwendet eine @keyframes
-Liste, in der Sie die einzelnen Schritte einer CSS-Animationssequenz festlegen. Diese Schritte werden durch Wegpunkte beschrieben, die während der Sequenz zu bestimmten Zeitpunkten erreicht werden. Den zu animierenden CSS-Eigenschaften werden dort unterschiedliche Werte zugeordnet. Der Browser übernimmt es, die erforderlichen Zwischenwerte zu berechnen, so dass zwischen den einzelnen Keyframes ein kontinuerlicher Übergang entsteht.
#loading {
animation-name: widen-brighten;
animation-duration: 4s;
animation-direction: alternate;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
@keyframes widen-brighten {
from {
fill:darkgreen;
width: 20px;
}
to {
fill: lime;
width: 200px;
}
}
<svg viewBox="0 0 300 100" >
<rect id="loading" x="10" y="10" width="20" height="20" />
</svg>
Das rect-Element wird über seine id selektiert. Ihm wird eine Animation mit dem Namen widen-brighten
(„verbreitern-aufhellen“) zugewiesen. Diese wird über einige animation-Eigenschaften weiter konfiguriert:
- animation-duration setzt eine Dauer von 4s für einen einzelnen Durchlauf;
- animation-iteration-count: infinite sorgt dafür, dass die Animation nicht automatisch beendet wird.
- animation-direction: alternate bewirkt, dass die Animation bei jedem zweiten Durchlauf in umgekehrter Richtung (vom "to"-Zustand zum "from"-Zustand) durchlaufen wird.
- animation-timing-function: ease-in-out sorgt dafür, dass die Animation an den Endpunkten langsamer wird, wodurch die Richtungsumkehr sanfter erscheint.
An Stelle der vier Einzeleigenschaften können Sie auch die Sammeleigenschaft animation
verwenden. In welcher Reihenfolge Sie die Einzelwerte notieren, ist dabei Ihnen überlassen:
animation: widen-brighten 4s ease-in-out alternate infinite
Laufende Linien
Eine Spezialität von SVG sind animierte Randlinien; auf Englisch auch line-drawing genannt. In diesem einfachen Beispiel wird ein Ladebalken bzw ein loading spinner animiert.
.loading {
fill: none;
stroke: steelBlue;
stroke-width: 10;
stroke-dasharray: 30 10;
animation: strokeAni .7s infinite linear;
}
.inner {
stroke: #c82f04; /* red */
animation-direction: reverse;
}
@keyframes strokeAni {
0% {
stroke-dashoffset: 40;
}
100% {
stroke-dashoffset: 0;
}
}
<path id="linie" d="m20,100 h600" />
<path d="m620,80 v40 l20,-20z" fill="steelBlue"/>
Die Linie besteht aus einem Pfad, der mit mit der Eigenschaft stroke-dasharray einen gestrichelten Rand erhält. Die Werte legen genau fest, wie viel „Randlinie“ und wie viel Lücke gezeichnet werden soll.
Alle Elemente der Klasse .loading
rufen nun die Animation strokeAni
auf. In dieser wird mit stroke-dashoffset der Beginn des Randes verschoben, sodass es aussieht, als ob die Striche sich bewegen. Der Verlauf erhält den Wert linear, damit sie gleichmäßig verläuft und wird wegen infinite
unendlich wiederholt.
Der Spinner besteht aus zwei Kreisen ohne Füllung, deren Randlinie genau wie oben beschrieben gestrichelt ist. Für den circle mit der Klasse .inner
wird ebenfalls die Animation strokeAni
aufgerufen, mit animation-direction:reverse
aber eine andere Richtung festgelegt.
Line-Drawing
Wenn Sie gestrichelte Linien mit einem hohen Wert für stroke-dasharray versehen und diesen dann mit stroke-dashoffset so weit verschieben, dass die Randlinie ursprünglich nicht sichtbar ist, können Sie den Eindruck erwecken, dass das SVG-Objekt gerade gezeichnet wird.
path {
stroke: steelBlue;
fill: none;
stroke-width: 3;
stroke-dasharray: 900 900;
animation: strokeAni 5s infinite linear;
}
@keyframes strokeAni {
0% {
stroke-dashoffset: 900;
}
100% {
stroke-dashoffset: 0;
}
}
<path d="M200,350 v-100 l-50,-51 l-50,52 h100 l-100,100 v-100 l100,100 h-100" />
Siehe auch
Transformationen
Für Transformationen gibt es in SVG das transform-Attribut und seine Verwandten gradientTransform bei Verläufen und patternTransform bei Mustern. Mit ihnen können Größenänderungen wie Skalierungen, Streckungen und Stauchungen, aber auch Drehungen und Verschiebungen vorgenommen werden. In CSS gibt es die gleichnamige transform-Eigenschaft, die sich in allen modernen Browsern auch auf SVG-Elemente anwenden lässt.
Trotz der Ähnlichkeiten gibt es einen gravierenden Unterschied bei dem Bezugspunkt, auf den sich die Transformationen beziehen - den transform-origin. Während er für HTML-Elemente jeweils in deren Mittelpunkt (((50%, 50%)
) liegt, befindet er sich bei SVG-Elementen standardmäßig im Punkt (0, 0)
, dem Koordinatenursprung der Viewbox, in der gerade gezeichnet wird. Gibt man einen transform-origin explizit an, bezieht er sich bei HTML Elementen auf deren linke obere Ecke, bei SVG Elementen dagegen wieder auf den Koordinatenursprung der verwendeten Viewbox. Hinzu kommt eine Lücke bei Safari-Browsern: hier wirkt eine transform-origin Angabe nur auf Transformationen, die mit Hilfe der CSS-Eigenschaft transform
gemacht werden, nicht auf Transformationen durch das SVG-Attribut.
Deshalb müssen Transformationen von SVG Elementen anders aufgebaut werden als bei HTML.
@keyframes rotation {
0% { transform: rotate(0deg);}
100% {transform: rotate(360deg);}
}
@keyframes rotation2 {
0% {transform: translate(250px,100px) rotate(0deg);}
100% {transform: translate(250px,100px) rotate(360deg);}
}
<svg viewBox="-100 -100 600 200">
<symbol id="pinkSym" viewBox="-2 -2 4 4" overflow="visible">
<rect x="-2" y="-2" width="4" height="4" fill="pink" />
</symbol>
<rect id="gelb" x="0" y="0" width="30" height="30" />
<rect id="rot" x="60" y="30" width="30" height="30" />
<rect id="blau" x="-15" y="-15" width="30" height="30" />
<rect id="grün" x="250" y="15" width="30" height="30" transform-origin="265 30" />
<use href="#pinkSym" width="30" height="30" x="100" y="-50" />
</svg>
Das blaue Quadrat wird so gezeichnet, dass sein Mittelpunkt im Ursprung des Koordinatensystems liegt. Die Rotation dreht es deshalb um sich selbst. Die für blau verwendeten Keyframes rotation2
fügen der Transformation aber noch translate(150px,30px)
hinzu, so dass es sich an dieser Position zu drehen scheint.
Bei dem grünen Quadrat wurde dagegen ein transform-origin
gesetzt. Es verwendet die gleichen Animations-Keyframes wie das rote und gelbe Quadrat, rotiert aber durch den geänderten Ursprungspunkt ebenfalls um sich selbst.
translate(x,y)
an die Zielposition. Die Verwendung eines Symbols kann ebenfalls hilfreich sein, um ein geeignetes Koordinatensystem für die Rotation bereitzustellen.Ana Tudor hat einen sehr ausführlichen Artikel auf CSS-Tricks veröffentlicht, in dem die systembedingten Unterschiede und auch die abweichenden Browser-Verhalten vorgestellt werden.[1] In fernerer Zukunft sollen die beiden Spezifikationen zu einer einheitlichen verschmelzen, so wie es bei Masken und Beschneidungen schon erfolgt ist.
SVG-Uhr
Als Anwendungsbeispiel wollen wir nun eine Uhr erstellen, die mit SVG gezeichnet und mit CSS-animation und transform zum Laufen gebracht wird.
Zifferblatt
Wie oben gesehen, gehen Transformationen immer vom Ursprung des Kordinatensystems aus. Also liegt es nahe, dem Mittelpunkt der Uhr, an dem sich auch die Zeiger orientieren, auf 0|0 festzulegen.
Im Normalfall wäre dies die linke, obere Ecke des SVG-Elements. Durch die viewBox können wir es aber passend verschieben, dass der Ursprung in der Mitte liegt:
<svg viewBox="-100 -100 200 200">
<circle id="clockface" r="98" />
<circle id="origin" r="2" />
</svg>
Die beiden circle-Elemente benötigen keine cx
- und cy
-Angaben, da für diese der Standardwert genommen wird.
Während der Ursprung (engl. origin) nur aus einem 4px breiten Punkt besteht (d = 2r), wird das Zifferblatt mit CSS gestylt.
Eine Möglichkeit wäre das Einfügen eines Fotos als Hintergrundbild einer Uhr. Wir wollen jedoch die Indizes für die Stunden per SVG einfügen, damit sie später mit CSS beliebig gestylt werden können.
<svg viewBox="-100 -100 200 200">
<defs>
<line id="index" x1="80" y1="0" x2="90" y2="0" />
</defs>
<circle id="clockface" r="98" />
<circle id="origin" r=" 2" />
<g id="indizes">
<use href="#index" transform="rotate(300)" />
<use href="#index" transform="rotate(330)" />
<use href="#index" transform="rotate(0)" />
<use href="#index" transform="rotate(30)" />
<use href="#index" transform="rotate(60)" />
<use href="#index" transform="rotate(90)" />
...
</g>
</svg>
Im Definitionsabschnitt wird ein line-Element notiert, dass aber nicht gerendert wird.
Mit dem use-Element wird es über die ID mehrfach aufgerufen und dabei mit dem transform-Attribut entsprechend (360° / 12h = 30°) gedreht. Diese Transformation ist bewusst im SVG-Markup geblieben, da sie ja eher zum Inhalt gehört und nicht durch einen anderen Stil geändert werden sollte.
CSS
Nur die Darstellung an sich wird im CSS-Abschnitt festgelegt.
Dabei verwenden wir für die Farben custom properties, damit Sie die Farbgestaltung später schnell ändern können.
svg {
--bgcolor: #fdfcf3;
--primarycolor: #337599;
--accent1color: #dfac20;
--accent2color: #c82f04;
}
#indizes > use {
stroke: var(--accent1color);
stroke-width: 1;
stroke-linecap: round;
}
#indizes > use:nth-child(3n+3) {
stroke-width: 3;
}
Warum startet die Reihe der use-Elemente mit rotate(300)
? Über den use:nth-child(3n+3)
-Selektor wird jedes dritte use-Element (≙ 3, 6, 9, 12 Uhr) selektiert und entsprechend deutlicher markiert.
Zeiger
Genauso können wir mit den drei Zeigern vorgehen:
<svg viewBox="-100 -100 200 200">
<defs>
</defs>
<circle id="clockface" r="98" />
<g id="indizes">
...
</g>
<line class="hand" id="hours" x1="0" y1="0" x2="0" y2="-50" />
<line class="hand" id="minutes" x1="0" y1="0" x2="0" y2="-95" />
<g id="seconds" class="hand">
<line x1="0" y1="0" x2="0" y2="-55" />
<circle cx="0" cy="-60" r="5" fill="none" />
<line x1="0" y1="-65" x2="0" y2="-95" />
</g>
<circle id="origin" r="2" />
</svg>
Die beiden ersten Zeiger bestehen aus line-Elementen, die eine gemeinsame Klasse und individuelle IDs erhalten.
Der Sekundenzeiger besteht aus zwei Linien, die durch einen circle
verbunden werden. Da die drei Elemente innerhalb eines g-Elements gruppiert wurden, erhält dieses die Klassen und Id, deren Festlegungen dann auf die Kindelemente vererbt werden.
Da SVG-Elemente der Reihe nach gerendert werden, würden sie jetzt über dem Ursprungspunkt liegen, weswegen das circle-Element nun ans Ende des SVG-Abschnitts verschoben wurde.
#indizes > use,
.hand {
stroke: var(--primarycolor);
stroke-width: 1;
stroke-linecap: round;
}
#indizes > use:nth-child(3n+3) {
stroke-width: 3;
}
#hours {
stroke: var(--primarycolor);
stroke-width: 4;
}
#minutes {
stroke: var(--primarycolor);
stroke-width: 2;
transform: rotate(33deg);
}
#seconds {
stroke: var(--accent2color);
transform: rotate(9deg);
}
Der Stundenzeiger wird 4x breiter, der Minutenzeiger doppelt so breit wie der Sekundenzeiger. Dieser wird rot eingefärbt. Normalerweise würden die drei Zeiger übereinanderliegen. Damit sie jetzt schon sichtbar sind, wurden Minuten- und Sekundenzeiger mit CSS-transform gedreht.
Nun haben wir ein schönes Bild einer Uhr. Perfekt wäre es allerdings, wenn sich die Zeiger bewegen würden.
Animation
Mit CSS-animations können wir keyframes
definieren, innerhalb der bestimmte Eigenschaften ihre Werte ändern. Das zu animierende Element erhält nun eine Eigenschaft animation
, in der dieser keyframe aufgerufen wird:
svg {
--start-hours: 0;
--start-minutes: 0;
--start-seconds: 0;
height: 250px;
}
#seconds {
stroke: #c32e04;
transform: rotate(calc(var(--start-seconds) * 6deg));
animation: rotateSecondsHand 60s steps(60) infinite;
}
@keyframes rotateSecondsHand {
from {
transform: rotate(calc(var(--start-seconds) * 6deg));
}
to {
transform: rotate(calc(var(--start-seconds) * 6deg + 360deg));
}
}
Zuerst werden nun CSS-Variablen für Stunden, Minuten und Sekunden gesetzt. Diese werden aber erst später gebraucht – im jetzigen Beispiel sind sie noch nicht gesetzt.
Der Sekundenzeiger erhält eine animation
-Eigenschaft, mit der der keyframe rotateSecondsHand
aufgerufen wird. Hier wird eine Drehung des Zeigers erreicht, indem der transform-Eigenschaft die calc()-Funktion calc(var(--start-seconds) * 6deg)
als Wert zugewiesen wird. So wird der Sekundenzeiger innerhalb der Dauer der Animation (60s) um 360° gedreht. Dabei verläuft die Animation nicht durchgehend, sondern wird mit steps(60)
in 60 Schritte geteilt – so springt der Zeiger jede Sekunde eine Position weiter.
Analog werden die Minuten berechnet und der Minutenzeiger entsprechend gedreht:
#minutes {
stroke-width: 2;
transform: rotate(calc(var(--start-minutes) * 6deg));
animation: rotateMinuteHand 3600s steps(60) infinite;
animation-delay: calc(var(--start-seconds) * -1 * 1s);
}
@keyframes rotateMinuteHand {
from {
transform: rotate(calc(var(--start-minutes) * 6deg));
}
to {
transform: rotate(calc(var(--start-minutes) * 6deg + 360deg));
}
}
Die Animation verläuft vergleichbar der Drehung der Sekundenzeiger; über animation-delay wird aber so lange gewartet, bis der Sekundenzeiger seine 360°-Drehung vollendet und die "12" oben erreicht.
#hours {
transform: rotate(calc(var(--start-hours) * 30deg));
animation: rotateHourHand calc(12 * 60 * 60s) linear infinite;
animation-delay: calc(calc(var(--start-minutes) * -60 * 1s) + calc(var(--start-seconds) * -1 * 1s));
}
@keyframes rotateHourHand {
from {
transform: rotate(calc(var(--start-hours) * 30deg));
}
to {
transform: rotate(calc(var(--start-hours) * 30deg + 360deg));
}
}
Analog wird der Stundenzeiger gesteuert. Da wir die Startzeit auf 0:00:00 gesetzt haben, ist dies nun eine gute Uhr, die die Zeit seit dem Aufruf des Beispiels misst. Besser wäre allerdings eine Uhr, die die aktuelle Zeit angibt. Dies ist mit CSS alleine aber nicht möglich.
Zeitabfrage
Nach dem Prinzip der Trennung von Inhalt, Präsentation und Verhalten sind HTML und SVG für die Auszeichnung des Inhalts, CSS für die Darstellung und JavaScript für Interaktion zuständig. Um die Systemzeit des Computers oder eine Zeit im Internet abzufragen, benötigen Sie JavaScript.
Theoretisch kann das script-Element auch innerhalb des svg-Elements notiert werden. Besser ist es jedoch, dies im head des HTML-Dokuments zu tun:
document.addEventListener('DOMContentLoaded', function () {
const svg = document.querySelector('svg');
const currentTime = new Date();
svg.style.setProperty('--start-seconds', currentTime.getSeconds());
svg.style.setProperty('--start-minutes', currentTime.getMinutes());
svg.style.setProperty('--start-hours', currentTime.getHours() % 12);
});
Das Script beinhaltet einen Eventlistener, der nach dem Laden der Webseite mit new Date() ein neues Datumsobjekt mit der aktuellen Zeit (engl. currentTime) erzeugt.
Mittels setProperty() wird dann die aktuelle Zeit den CSS-Variablen zugewiesen.
Fazit:
Mit nur 5 Zeilen JavaScript und drei ähnlichen CSS-Animationen, der Verwendung von CSS-Variablen für Farbe und die Zeitwerte ist eine semantisch passende, und übersichtliche Uhr gelungen. Vielen Dank an John O. Paul für sein englischsprachiges Tutorial, aus dem ich die Animationen verwendet habe.
Die unten verlinkten Beispiele funktionieren mit SMIL und JavaScript und können als Anregungen für weitere Uhren-Themes dienen.
What's the time?
Für den Anfangsunterricht in Englisch habe ich mir diese Uhr „gebastelt“. Sie zeigt ursprünglich die aktuelle Zeit. Mit einem Klick auf den Button kann man eine zufällige Uhrzeit anzeigen lassen:
const svg = document.querySelector('svg');
const currentTime = new Date();
var current = 0;
document.querySelector('#newT').addEventListener('click', newTime);
function currTime() {
svg.style.setProperty('--start-seconds', currentTime.getSeconds());
svg.style.setProperty('--start-minutes', currentTime.getMinutes());
svg.style.setProperty('--start-hours', currentTime.getHours() % 12);
}
function newTime() {
if (current <= 10) {
var number = 0;
} else {
number = rand(0,11)*5;
}
svg.style.setProperty('--start-seconds', 0);
svg.style.setProperty('--start-minutes', number);
svg.style.setProperty('--start-hours', rand(0,11));
current++;
}
function rand (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
In den ersten 10 Durchläufen wird der Wert für Minuten auf 0 gesetzt, um Begriffe wie „eight o'clock“ zu üben. Beim 11. Durchlauf wird der Minutenzeiger ebenfalls zufällig gedreht. Da die Zufallszahl aber nicht aus dem Bereich 0,59 , sondern aus dem Bereich 0,11 gebildet und dann mit 5 multipliziert wird, haben wir anfangs nur „einfachere“ Werte wie „quarter past“ oder „ten to“.
Weblinks
- css-tricks: SVG Properties and CSS (13.05.2019)
- css-tricks: Animating SVG with CSS
- smashing-magazine: Styling and animating SVG with CSS von Sara Soueidan (03.11.2014)
- Hongkiat: How To Create SVG Animation Using CSS
- Tavmjong Bah's Blog Animation in SVG and CSS (The digital river)
- Animated Animals in CSS and SVG David Khourshid (21.03.2016)
Uhren
- David Dailey, SRU: Ball Clock
- tavmjong.free.fr: SteamEngine.svg (ein schöner Zeiger sogar mit Schattenwurf) von Tavmjong Bah
- tavmjong.free.fr: Gear Clocks von Tavmjong Bah
Quellen
- ↑ css-tricks: Transforms on SVG Elements
stroke-daharray
festgelegt wird. Der Wert von 900 ist so hoch, dass die Strichelung länger als der Umriss des Hauses ist. Da er zu Beginn der Animation mitstroke-dashoffset
um 900 verschoben wurde, erscheint der Rand allmählich und es entsteht ein Zeicheneffekt.