SVG/Tutorials/Einstieg/SVG mit CSS animieren

Aus SELFHTML-Wiki
< SVG‎ | Tutorials‎ | Einstieg
Wechseln zu: Navigation, Suche

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.

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.

Ladebalken So sieht's aus
#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:

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 
Beachten Sie: SVG sieht keine Einheiten für Längenangaben vor, es gibt aber in CSS keine dimensionslosen Längenangaben. Deshalb müssen Sie in den Keyframes die Einheit px verwenden, wenn Sie die die Viewbox-Koordinaten des SVG Elements meinen. Sie können auch alle übrigen CSS Längeneinheiten nutzen, aber der Browser rechnet sie in px um und überträgt das auf die Viewbox des SVG Elements. Dadurch ergibt sich eine Skalierung, die dem Verhältnis zwischen Viewbox-Breite und SVG-Elementbreite (bzw. Höhe) entspricht.

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.

Ladebalken und Spinner So sieht's aus
.loading {
	fill: none;
	stroke: #306f91;
	stroke-width: 10;
	stroke-dasharray: 30 10;
	animation: strokeAni .7s infinite linear;
}

.inner {
	stroke: #c32e04;
	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="#3983ab"/>

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.

Haus vom Nikolaus So sieht's aus
path {
  stroke: #c32e04;
  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" />
Die Randlinie hat eine Strichelung, deren Wert mit 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 mit stroke-dashoffset um 900 verschoben wurde, erscheint der Rand allmählich und es entsteht ein Zeicheneffekt.

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 rote und das gelbe Quadrat drehen sich um 360 Grad, nehmen als Mittelpunkt der Drehung aber den Ursprung des SVG-Koordinatensystems.

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.

Das pink-farbene Quadrat ist Teil eines Symbols. Hier sieht man, dass die Transformation sich auf die verwendete Viewbox bezieht. Das Quadrat wird in seiner Viewbox so gezeichnet, dass sein Mittelpunkt in deren Ursprung liegt, und die Animation ist auf das rect Element innerhalb des Symbols gerichtet, deswegen dreht es sich innerhalb des Symbols um sich selbst.
Empfehlung: Wenn Sie SVG Elemente mit einer Transformation rotieren möchten, dann zeichnen Sie sie im Koordinatenursprung so, dass sie für die Rotation passend liegen, und verschieben Sie sie erst dann, als Teil der Transformation, mit 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:

Viewbox mit verschobenem Ursprung So sieht's aus
<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.

Zifferblatt mit Stunden-Indizes So sieht's aus
<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 xlink:href="#index" transform="rotate(300)" />	
	<use xlink:href="#index" transform="rotate(330)" />	
	<use xlink:href="#index" transform="rotate(0)" />
	<use xlink:href="#index" transform="rotate(30)" />
	<use xlink:href="#index" transform="rotate(60)" />
	<use xlink: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.

CSS der Stundenmarkierungen So sieht's aus
svg {
  --bgcolor: #fdfcf3;
  --primarycolor: #306f91;
  --accent1color: #dfac20;
  --accent2color: #c32e04;  
}

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

Gestaltung der Zeiger So sieht's aus
#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:

Sie läuft und läuft … So sieht's aus
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:

Animation des Minutenzeigers So sieht's aus
#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.

Animation des Stundenzeigers So sieht's aus
#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:

Zeitabfrage in JavaScript So sieht's aus
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.


Weblinks

Uhren

Quellen

  1. css-tricks: Transforms on SVG Elements