Kreise animieren (2) SVG stroke-dasharray-Voodoo

Neben der hier beschriebenen Art, HTML-divs via CSS (transform:rotate) zu animieren und zu maskieren, ist auch eine weitere Variante verbreitet: SVG circles.

Diese vektorisierten Grafiken sind recht einfach direkt im HTML-Code generierbar und ihre Eigenschaften via CSS oder JS manipulierbar. Im vorliegenden Fall kann man die Attribute stroke („Pfadkontur“), stroke-dasharray (in etwa: „gestrichelte Kontur“) und stroke-dashoffset (meint so etwas wie: „Verschiebung der gestrichelten Kontur“) nutzen, um drei Kreiskonturen zu animieren:

Worum es geht

Das Grundgerüst

Auch hier bastele ich zuerst eine Bühne (#stage), auf der ich dann mit weiteren Elementen hantiere. Im vorliegenden Fall ist das ein <svg>-Element mit dem Inhalt <circle>:

HTML

<div id="stage">
  <svg viewBox="0 0 100 100" width="20rem" height="20rem">
    <circle cx="50" cy="50" r="40" class="circle1"></circle>
  </svg>
</div>

CSS

#stage {
  background: #111213;
  width: 20rem;
  height: 20rem;
  margin: 2rem auto;
  border-radius: 2rem;
  position: relative;
}
.circle1 {
  fill:#bf304a;
}

Die viewBox (Doku) öffnet eine Art Zeichenfläche, die über Koordinaten und Größenangaben definiert wird ("x-Ankerpunkt y-Ankerpunkt Breite Höhe"). Die Größenangaben der viewBox sind relativ zur width- und height-Angabe des svg-Elements zu verstehen, funktionieren daher ohne Einheiten (px, %, em, ...) und regeln so vor allem a) das Seitenverhältnis der Zeichenfläche und b) die Positionierung von gezeichneten Objekten.

Die Angaben cx (= center x-axis), cy (= center y-axis), r (= Radius), fill (= Füllung), stroke (= Kontur) und weitere SVG Attribute können sowohl als CSS (z.B. cx:10rem;) oder als Element-Attribut in HTML (dann: <svg><circle cx="160"></circle></svg>) angegeben werden. Bei der Angabe direkt im HTML-Code ist eine Angabe verlässlicher und cross-brower-geeigneter, dann ebenfalls ohne Einheiten vorzunehmen. ein cx & cy von je 50 legt den Mittelpunkt des Kreises also genau in die Mitte der viewBox mit den Abmessungen 100 * 100:

Für mehre Ringe benötigen wir nun innerhalb des einen <svg> drei <circle>, und statt einer Füllung (fill) wird nun eine Konturfarbe (stroke) eingesetzt, der man eine Breite (stroke-width) geben kann. Bei einer Konturbreite von 2rem verringert sich der Radius r für jeden folgenden Kreis um ebenfalls 2rem (weil wegen Kontur rundherum, sind also eigentlich 4 rem und Radius ist nur der halbe Durchmesser ... ach, Ihr wisst schon):

HTML

<div id="stage">
  <svg viewBox="0 0 100 100" width="20rem" height="20rem">
    <circle cx="50" cy="50" r="40" class="circle1"></circle>
    <circle cx="50" cy="50" r="30" class="circle2"></circle>
    <circle cx="50" cy="50" r="20" class="circle3"></circle>
  </svg>
</div>

CSS

.circle1 {
  fill:none;
  stroke:#ff284f;
  stroke-width:10;
}
.circle2 {
  fill:none;
  stroke:#b3ff41;
  stroke-width:10;
}
.circle3 {
  fill:none;
  stroke:#11b4f4;
  stroke-width:10;
}

So siehts aus:

Konturgestaltung

Jetzt wirds ein wenig frickelig, da die Konturlinie mittels stroke-dasharray in Segmente "zerlegt" werden muss und diese Segmente mittels stroke-dashoffset verschoben werden. Dafür benötigt man die Länge der Kontur (man könnte sie auch "Umfang" nennen 🙈).

stroke-dasharray

Bei Kreisen ist der Umfang bekanntlich bei U = π * 2 * r, also für den äußeren Kreis bei 3,1415... * 2 * 40 = ~ 251,3. Wenn man beispielhaft das dasharray mal auf 1/10 des Umfangs (= 25.13 ... Punkt statt Komma 🙄) setzt, erhält man fünf Abschnitte und fünf "Lücken":

CSS

.circle1 {
  /* siehe oben */
  stroke-dasharray: 25.13;
}

stroke-dashoffset

Die Offset-Einstellung kann nun diese Kontur-Segmente verschieben. Eine Animation von stroke-dashoffset um 50.26; (1 Strich + 1 Lücke, je 25.13;) führt zu einer lustigen Kreisbewegung:

CSS

.circle1 {
  /* siehe oben */
  stroke-dasharray: 25.13;
  animation: strokes 1s linear infinite;
}
@keyframes strokes {
  100%  {stroke-dashoffset: 50.26;}
}

Einsatz von Array & Offset für eine Animation

Wenn man das Grundprinzip einmal (halbwegs! gilt für mich) begriffen hat, wird klar: Wenn stroke-dasharray = Umfang ist, ist der Kreis "unsichtbar", da die erste Lücke der Segmentierung genau so groß wie die gesamte Kreislinie lang ist. Und wenn man dann stroke-dashoffset von x nach 0 animiert (bei xmax = Umfang), läuft die Kreiskontur "von 0 auf voll":

CSS

.circle1 {
  /* siehe oben */
  stroke-dasharray: 251.3;   /* entspricht dem Umfang π * 2 * r */
  stroke-dashoffset: 251.3;  /* entspricht dasharray */
  animation: strokes 3s ease-in-out infinite;
}
@keyframes strokes {
    0% {stroke-dashoffset: 251.3;}
   50% {stroke-dashoffset: 0;}
}

Gleiches Vorgehen für die anderen beiden Kreise - andere Radien machen andere dasharray- und dashoffset-Werte notwendig. Um die Öffnung der Kreise nach oben zu drehen, drehe ich einfach das ganze svg-Objekt. Zur weiteren Veranschaulichung habe ich ein animation-delay von .2s bzw. .4s für .circle2 und .circle3 eingebaut:

CSS

svg {
  transform:rotate(-90deg);
}

.circle1 {
  /* siehe oben */
  stroke-dasharray: 251.3;
  stroke-dashoffset: 251.3;
  animation: strokes1 3s ease-in-out infinite;
}
.circle2 {
  /* siehe oben */
  stroke-dasharray: 188.5;
  stroke-dashoffset: 188.5;
  animation: strokes2 3s ease-in-out .2s infinite;
}
.circle3 {
  /* siehe oben */
  stroke-dasharray: 125.7;
  stroke-dashoffset: 125.7;
  animation: strokes3 3s ease-in-out .4s infinite;
}

@keyframes strokes1 {
   0% {stroke-dashoffset: 251.3;}
  50% {stroke-dashoffset: 0;}
}
@keyframes strokes2 {
   0% {stroke-dashoffset: 188.5;}
  50% {stroke-dashoffset: 0;}
}
@keyframes strokes3{
   0% {stroke-dashoffset: 125.7;}
  50% {stroke-dashoffset: 0;}
}

So weit, so cool 🙂

Feinschliff

"Scalable Vector Graphics Stroke-linecap" von Eric Baas, Quelle, CC-by-sa.

Um das Ganze im Wortsinne abzurunden, kann man mit stroke-linecap (Doku) runde "Endkappen" aufsetzen, muss dann aber dashoffset evtl. um die "Kappengröße" ändern 🙄

Diese ist im Falle von stroke-linecap:round; gleich der Hälfte von stroke-width, hier also 10 / 2 = 5. Je nach gewünschter Überlappung der beiden Kappen (Anfang und Ende) muss das Offset z.B. bei Endpunkten einer Animation entsprechend vergrößert werden. Wenn sie gar nicht überlappen sollen, muss man Offset auf "zwei Kappengrößen", also eine ganze stroke-width setzen.

Die Abbildung zeigt schematisch die Funktionsweise von linecaps und die dadurch provozierte Verlängerung einer Konturlinie, das Beispiel unten einen Animations-End-Offset von 7.5, also "fast" zwei Kappengrößen. Dass nun die Öffnung der kreise nicht mehr exakt oben ist, kann man halbwegs mittels Justierung der Rotation des svg-Elements ausgleichen.


Eine weitere Option ist das Arbeiten mit Farbverläufen innerhalb von stroke: Dafür muss man innerhalb des svgs einen <defs> ... </defs>-Bereich (Doku) anlegen, dort linearGradients definieren und stroke statt einer Füllfarbe einen Verlauf via ID zuordnen. Standardmäßig verläuft ein Verlauf von oben nach unten, gradientTransform dreht die Verlaufsrichtung. Die <stop>-Definitionen legen in % die Punkte über den Verlauf fest, für die Farben hinterlegt sind. Die Ausrichtung von Rotation und Stop-Punkten verlangt je nach Anwendungsfall viel Ausprobieren.

HTML

<svg viewBox="0 0 100 100" width="20rem" height="20rem">
  <defs>
    <linearGradient id="gradient1" gradientTransform="rotate(78)">
      <stop offset="32%" stop-color="#fa114f" />
      <stop offset="100%" stop-color="#f93885" />
    </linearGradient>
    <linearGradient id="gradient2" gradientTransform="rotate(78)">
      <stop offset="32%" stop-color="#99ff01" />
      <stop offset="100%" stop-color="#d8ff00" />
    </linearGradient>
    <linearGradient id="gradient3" gradientTransform="rotate(78)">
      <stop offset="32%" stop-color="#00d8fe" />
      <stop offset="100%" stop-color="#02ffa9" />
    </linearGradient>
  </defs>
  <circle cx="50" cy="50" r="40" class="circle1"></circle>
  <circle cx="50" cy="50" r="30" class="circle2"></circle>
  <circle cx="50" cy="50" r="20" class="circle3"></circle>
</svg>

CSS

.circle1 {
  /*siehe oben*/
  stroke:url(#gradient1);
}
.circle2 {
/*siehe oben*/
  stroke:url(#gradient2);
}
.circle3 {
/*siehe oben*/
  stroke:url(#gradient3);
}

Varianten

Darüber hinaus kann man auch hier (wie bei der rotierende-divs-Variante) Gedanken über einen Auslöser der Animation machen. Das können auch hier wieder z.B. :hover oder JavaScript-Werte sein.

Trigger durch :hover

Beim :hover-Trigger empfiehlt es sich, statt mit CSS animations lieber mit CSS transitions zu arbeiten, da diese hübscher in den ":not(:hover)"-Status zurückschwingen. Das ist bei Keyframe-Animationen nicht so einfach zu realisieren:

Trigger durch JavaScript / jquery

Durch das Binden des Offset-Wertes an eine Variable können die Ringe auch als Fortschritts-, Timer- oder Counter-Anzeige genutzt werden. Hier kommt der Wert - zur Veranschaulichung - aus einem Schieberegler: Close your rings!

Bewegung heute (0-100%)

Training heute (0-100%)

Stehen heute (0-100%)

So weit.

Ergänzungen, Hinweise, Verbesserungen gerne in die Kommentare. Und - viel Spaß beim Basteln!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Folgende HTML-Tags sind möglich:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Zustimmung zur Datenverarbeitung