Erstellen einer Stern-zu-Herz-Animation mit SVG und Vanilla JavaScript

Avatar of Ana Tudor
Ana Tudor am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

In meinem vorherigen Artikel habe ich gezeigt, wie man mit Vanilla JavaScript sanft von einem Zustand in einen anderen übergeht. Sehen Sie sich diesen unbedingt zuerst an, da ich auf einige Dinge verweisen werde, die ich dort im Detail erklärt habe, wie z. B. Demos als Beispiele, Formeln für verschiedene Timing-Funktionen oder wie man die Timing-Funktion nicht umkehrt, wenn man vom Endzustand eines Übergangs zum Anfangszustand zurückkehrt.

Das letzte Beispiel zeigte, wie man die Form eines Mundes von traurig zu froh macht, indem man das d-Attribut des path ändert, mit dem dieser Mund gezeichnet wurde.

Die Manipulation von Pfaddaten kann auf die nächste Stufe gehoben werden, um interessantere Ergebnisse zu erzielen, wie z. B. einen Stern, der sich in ein Herz verwandelt.

Gif recording of a star to heart animation. We start with a five-point golden star. All of its tips are rounded and one of them points up. On a first click, the golden star shape morphs into a crimson heart shape and it rotates clockwise by half a circle. On a second clip, the crimson heart shape morphs back into a golden star shape and rotates by another half a circle, completing thus a full turn.
Die Stern-zu-Herz-Animation, die wir codieren werden.

Die Idee

Beide bestehen aus fünf kubischen Bézier-Kurven. Die interaktive Demo unten zeigt die einzelnen Kurven und die Punkte, an denen diese Kurven verbunden sind. Das Klicken auf eine Kurve oder einen Punkt hebt sie hervor, ebenso wie ihre entsprechenden Kurven/Punkte der anderen Form.

Sehen Sie sich den Pen von thebabydino (@thebabydino) auf CodePen an.

Beachten Sie, dass alle diese Kurven als kubische Kurven erstellt werden, auch wenn bei einigen davon die beiden Kontrollpunkte übereinstimmen.

Die Formen sowohl des Sterns als auch des Herzens sind ziemlich simpel und unrealistisch, aber sie reichen aus.

Der Startcode

Wie im Gesichtsanimationsbeispiel zu sehen ist, wähle ich oft, solche Formen mit Pug zu generieren. Hier, da diese Pfaddaten, die wir generieren, auch mit JavaScript für den Übergang manipuliert werden müssen, scheint die Wahl von reinem JavaScript, einschließlich der Berechnung der Koordinaten und ihrer Platzierung im d-Attribut, die beste Option zu sein.

Das bedeutet, wir müssen nicht viel Markup schreiben

<svg>
  <path id='shape'/>
</svg>

In Bezug auf JavaScript beginnen wir damit, das SVG-Element und das path-Element abzurufen – dies ist die Form, die sich von einem Stern zu einem Herz und zurück verwandelt. Wir setzen auch ein viewBox-Attribut auf das SVG-Element, sodass seine Abmessungen entlang der beiden Achsen gleich sind und der Punkt (0,0) genau in der Mitte liegt. Das bedeutet, dass die Koordinaten der oberen linken Ecke (-.5*D,-.5*D) sind, wobei D der Wert für die viewBox-Abmessungen ist. Und zu guter Letzt erstellen wir ein Objekt, um Informationen über den Anfangs- und Endzustand des Übergangs zu speichern und darüber, wie von den interpolierten Werten zu den tatsächlichen Attributwerten gelangt wird, die wir auf unsere SVG-Form anwenden müssen.

const _SVG = document.querySelector('svg'), 
      _SHAPE = document.getElementById('shape'), 
      D = 1000, 
      O = { ini: {}, fin: {}, afn: {} };

(function init() {
  _SVG.setAttribute('viewBox', [-.5*D, -.5*D, D, D].join(' '));
})();

Nachdem wir dies hinter uns gebracht haben, können wir zum interessanteren Teil übergehen!

Die Geometrie der Formen

Die Anfangskoordinaten der Endpunkte und Kontrollpunkte sind diejenigen, für die wir den Stern erhalten, und die Endkoordinaten sind diejenigen, für die wir das Herz erhalten. Der Bereich für jede Koordinate ist die Differenz zwischen ihrem Endwert und ihrem Anfangswert. Hier drehen wir die Form auch während der Verwandlung, weil wir möchten, dass der Stern nach oben zeigt, und wir ändern die fill, um vom goldenen Stern zum purpurroten Herz zu wechseln.

In Ordnung, aber wie bekommen wir die Koordinaten der End- und Kontrollpunkte in den beiden Fällen?

Stern

Im Fall des Sterns beginnen wir mit einem regelmäßigen Pentagramm. Die Endpunkte unserer Kurven befinden sich an den Schnittpunkten der Pentagrammkanten, und wir verwenden die Eckpunkte des Pentagramms als Kontrollpunkte.

Illustration showing the five cubic Bézier curves forming our star and the regular pentagram created by the support lines of the segments connecting the end points and the control points of these curves. The two control points for each curve coincide and represent the vertices of a regular pentagram. In a cyclical manner, the start point of any curve is the end point of the previous one and these points are where the pentagram edges cross each other.
Regelmäßiges Pentagramm mit hervorgehobenen Eckpunkten und Schnittpunkten der Kanten als Kontrollpunkte und Endpunkte von fünf kubischen Bézier-Kurven (live).

Das Ermitteln der Eckpunkte unseres regelmäßigen Pentagramms ist ziemlich einfach, gegeben den Radius (oder Durchmesser) seines Umkreises, den wir als Bruchteil der viewBox-Größe unseres SVG wählen (hier als Quadrat angenommen, wir wollen hier keine enge Packung). Aber wie bekommen wir ihre Schnittpunkte?

Zunächst betrachten wir das kleine Pentagon, das im folgenden Bild im Pentagramm hervorgehoben ist. Da das Pentagramm regelmäßig ist, ist auch das kleine Pentagon, dessen Eckpunkte mit den Schnittpunkten der Pentagrammkanten zusammenfallen, regelmäßig. Es hat auch den gleichen Inkreis wie das Pentagramm und daher den gleichen Inradius.

Illustration showing a regular pentagram. The five intersection points of its edges are the vertices of a small regular pentagon whose edges are on the same support lines as the edges of the pentagram. Furthermore, the regular pentagram and its inner regular pentagon have the same incircle (and thus the same inradius).
Regelmäßiges Pentagramm und inneres regelmäßiges Pentagon teilen sich den gleichen Inkreis (live).

Wenn wir also den Inradius des Pentagramms berechnen, haben wir auch den Inradius des kleinen Pentagons, der uns zusammen mit dem Zentralwinkel, der einer Kante eines regelmäßigen Fünfecks entspricht, ermöglicht, den Umkreisradius dieses Fünfecks zu ermitteln, was uns wiederum ermöglicht, seine Eckpunktkoordinaten zu berechnen, und das sind genau die Schnittpunkte der Pentagrammkanten und die Endpunkte unserer kubischen Bézier-Kurven.

Unser regelmäßiges Pentagramm wird durch das Schläfli-Symbol {5/2} dargestellt, was bedeutet, dass es 5 Eckpunkte hat und, gegeben diese 5 Eckpunkte, die gleichmäßig auf seinem Umkreis verteilt sind, 360°/5 = 72° voneinander entfernt sind, wir vom ersten ausgehen, den nächsten Punkt auf dem Kreis überspringen und mit dem zweiten verbinden (das ist die Bedeutung der 2 im Symbol; 1 würde ein Fünfeck beschreiben, da wir keinen Punkt überspringen, sondern zum ersten verbinden). Und so weiter – wir überspringen immer den Punkt direkt danach.

Wählen Sie in der interaktiven Demo unten entweder Fünfeck oder Pentagramm, um zu sehen, wie sie konstruiert werden.

Sehen Sie sich den Pen von thebabydino (@thebabydino) auf CodePen an.

Auf diese Weise erhalten wir, dass der Zentralwinkel, der einer Kante des regelmäßigen Pentagramms entspricht, doppelt so groß ist wie der, der einem regelmäßigen Fünfeck mit denselben Eckpunkten entspricht. Wir haben 1·(360°/5) = 1·72° = 72° (oder 1·(2·π/5) im Bogenmaß) für das Fünfeck gegenüber 2·(360°/5) = 2·72° = 144° (2·(2·π/5) im Bogenmaß) für das Pentagramm. Im Allgemeinen ist für ein regelmäßiges Polygon (ob konvex oder Sternpolygon spielt keine Rolle) mit dem Schläfli-Symbol {p,q} der Zentralwinkel, der einer seiner Kanten entspricht, q·(360°/p) (q·(2·π/p) im Bogenmaß).

Illustration showing the central angle corresponding to an edge of a regular polygon: pentagram vs. pentagon. This angle is twice as big in the pentagram case as, having five points equally spaced around the circle, edges connect from one of these points to the next in the pentagon case, but always skip the first one right near and connect to the second in the pentagram case. This makes the edges and the corresponding central angles bigger.
Zentralwinkel, der einer Kante eines regelmäßigen Polygons entspricht: Pentagramm (links, 144°) vs. Fünfeck (rechts, 72°) (live).

Wir kennen auch den Umkreisradius des Pentagramms, von dem wir sagten, dass wir ihn als Bruchteil der Größe des quadratischen viewBox wählen. Das bedeutet, wir können den Inradius des Pentagramms (der gleich dem des kleinen Fünfecks ist) aus einem rechtwinkligen Dreieck erhalten, in dem wir die Hypotenuse (es ist der Umkreisradius des Pentagramms) und einen spitzen Winkel (die Hälfte des Zentralwinkels, der einer Pentagrammkante entspricht) kennen.

Illustration highlighting a right triangle from where we can compute a regular pentagram's inradius. The hypotenuse of this triangle is the pentagram circumradius and the acute angle between the two is half the central angle corresponding to the pentagram edge.
Berechnung des Inradius eines regelmäßigen Pentagramms aus einem rechtwinkligen Dreieck, bei dem die Hypotenuse der Umkreisradius des Pentagramms ist und der spitze Winkel dazwischen die Hälfte des Zentralwinkels ist, der einer Pentagrammkante entspricht (live).

Der Kosinus der Hälfte des Zentralwinkels ist der Inradius geteilt durch den Umkreisradius, was uns ergibt, dass der Inradius der Umkreisradius multipliziert mit diesem Kosinuswert ist.

Jetzt, da wir den Inradius des kleinen regelmäßigen Fünfecks innerhalb unseres Pentagramms haben, können wir seinen Umkreisradius aus einem ähnlichen rechtwinkligen Dreieck berechnen, das den Umkreisradius als Hypotenuse, die Hälfte des Zentralwinkels als einen der spitzen Winkel und den Inradius als die Kathete neben diesem spitzen Winkel hat.

Die folgende Abbildung hebt ein rechtwinkliges Dreieck hervor, das aus einem Umkreisradius eines regelmäßigen Fünfecks, seinem Inradius und der Hälfte einer Kante gebildet wird. Aus diesem Dreieck können wir den Umkreisradius berechnen, wenn wir den Inradius und den Zentralwinkel kennen, der einer Fünfeckkante entspricht, da der spitze Winkel zwischen diesen beiden Radien die Hälfte dieses Zentralwinkels ist.

Illustration highlighting a right triangle from where we compute a regular pentagon's circumradius. The hypotenuse of this triangle is the desired circumradius, while the catheti are the pentagon inradius and half the pentagon edge. The acute angle between the two radii is half the central angle corresponding to the pentagon edge.
Berechnung des Umkreisradius eines regelmäßigen Fünfecks aus einem rechtwinkligen Dreieck, bei dem es die Hypotenuse ist, während die Katheten der Inradius und die Hälfte der Fünfeckkante sind und der spitze Winkel zwischen den beiden Radien die Hälfte des Zentralwinkels ist, der einer Fünfeckkante entspricht (live).

Denken Sie daran, dass in diesem Fall der Zentralwinkel nicht derselbe ist wie beim Pentagramm, er ist die Hälfte davon (360°/5 = 72°).

Gut, jetzt, da wir diesen Radius haben, können wir alle gewünschten Koordinaten erhalten. Es sind die Koordinaten von Punkten, die in gleichen Winkeln auf zwei Kreisen verteilt sind. Wir haben 5 Punkte auf dem äußeren Kreis (dem Umkreis unseres Pentagramms) und 5 auf dem inneren (dem Umkreis des kleinen Fünfecks). Das sind insgesamt 10 Punkte, mit Winkeln von 360°/10 = 36° zwischen den radialen Linien, auf denen sie liegen.

Illustration showing the end and control points of the cubic curves making up our rounded tip star being distributed on two circles - the control points on an outer circle which is the circumcircle of the pentagram and the end points on an inner circle which is the circumcircle of the inner pentagon whose vertices are the points where the pentagram edges cross each other.
Die End- und Kontrollpunkte sind auf dem Umkreisradius des inneren Fünfecks und auf dem des Pentagramms verteilt (live).

Wir kennen die Radien beider Kreise. Der Radius des äußeren ist der Umkreisradius des regelmäßigen Pentagramms, den wir als einen beliebigen Bruchteil der viewBox-Dimension wählen (.5 oder .25 oder .32 oder irgendeinen Wert, von dem wir glauben, dass er am besten funktioniert). Der Radius des inneren ist der Umkreisradius des kleinen regelmäßigen Fünfecks, das im Pentagramm gebildet wird, den wir als Funktion des Zentralwinkels, der einer seiner Kanten entspricht, und seines Inradius berechnen können, der dem des Pentagramms gleich ist und den wir daher aus dem Umkreisradius des Pentagramms und dem Zentralwinkel, der einer Pentagrammkante entspricht, berechnen können.

An diesem Punkt können wir also die Pfaddaten generieren, die unseren Stern zeichnen, er hängt von nichts Unbekanntem ab.

Lassen Sie uns das also tun und alles oben Genannte in Code umsetzen!

Wir beginnen mit der Erstellung einer Funktion getStarPoints(f), die von einem beliebigen Faktor (f) abhängt, der uns hilft, den Umkreisradius des Pentagramms aus der viewBox-Größe zu erhalten. Diese Funktion gibt ein Array von Koordinaten zurück, die wir später für die Interpolation verwenden.

Innerhalb dieser Funktion berechnen wir zunächst die konstanten Dinge, die sich im Verlauf nicht ändern – den Umkreisradius des Pentagramms (Radius des äußeren Kreises), die zentralen (Basis-)Winkel, die einer Kante eines regelmäßigen Pentagramms und Polygons entsprechen, den Inradius, den sich das Pentagramm und das innere Fünfeck teilen, dessen Eckpunkte die Punkte sind, an denen sich die Pentagrammkanten kreuzen, den Umkreisradius dieses inneren Fünfecks und schließlich die Gesamtzahl der eindeutigen Punkte, deren Koordinaten wir berechnen müssen, und den Basiswinkel für diese Verteilung.

Danach berechnen wir innerhalb einer Schleife die Koordinaten der gewünschten Punkte und fügen sie dem Koordinatenarray hinzu.

const P = 5; /* number of cubic curves/ polygon vertices */

function getStarPoints(f = .5) {
  const RCO = f*D /* outer (pentagram) circumradius  */, 
        BAS = 2*(2*Math.PI/P) /* base angle for star poly */, 
        BAC = 2*Math.PI/P /* base angle for convex poly */, 
        RI = RCO*Math.cos(.5*BAS) /*pentagram/ inner pentagon inradius */, 
        RCI = RI/Math.cos(.5*BAC) /* inner pentagon circumradius */, 
        ND = 2*P /* total number of distinct points we need to get */, 
        BAD = 2*Math.PI/ND /* base angle for point distribution */, 
        PTS = [] /* array we fill with point coordinates */;

  for(let i = 0; i < ND; i++) {}

  return PTS;
}

Um die Koordinaten unserer Punkte zu berechnen, verwenden wir den Radius des Kreises, auf dem sie liegen, und den Winkel der radialen Linie, die sie mit dem Ursprung verbindet, in Bezug auf die horizontale Achse, wie in der folgenden interaktiven Demo veranschaulicht (ziehen Sie den Punkt, um zu sehen, wie sich seine kartesischen Koordinaten ändern).

Sehen Sie sich den Pen von thebabydino (@thebabydino) auf CodePen an.

In unserem Fall ist der aktuelle Radius der Radius des äußeren Kreises (Umkreisradius des Pentagramms RCO) für gerade Indexpunkte (0, 2, …) und der Radius des inneren Kreises (Umkreisradius des inneren Fünfecks RCI) für ungerade Indexpunkte (1, 3, …), während der Winkel der radialen Linie, die den aktuellen Punkt mit dem Ursprung verbindet, der Punktindex (i) multipliziert mit dem Basiswinkel für die Punktverteilung (BAD, der in unserem speziellen Fall 36° oder π/10 ist) ist.

Innerhalb der Schleife haben wir also

for(let i = 0; i < ND; i++) {
  let cr = i%2 ? RCI : RCO, 
      ca = i*BAD, 
      x = Math.round(cr*Math.cos(ca)), 
      y = Math.round(cr*Math.sin(ca));
}

Da wir einen ziemlich großen Wert für die Größe der viewBox gewählt haben, können wir die Koordinatenwerte sicher runden, damit unser Code übersichtlicher wird, ohne Dezimalstellen.

Beim Hinzufügen dieser Koordinaten zum Punkte-Array tun wir dies zweimal, wenn wir uns auf dem äußeren Kreis befinden (bei geraden Indizes), da wir dort tatsächlich zwei überlappende Kontrollpunkte haben, aber nur für den Stern. Daher müssen wir jeden dieser überlappenden Punkte an verschiedene Positionen verschieben, um das Herz zu erhalten.

for(let i = 0; i < ND; i++) {
  /* same as before */
  
  PTS.push([x, y]);
  if(!(i%2)) PTS.push([x, y]);
}

Als Nächstes legen wir Daten in unser Objekt O. Für das Pfaddaten-Attribut (d) speichern wir das Array von Punkten, das wir durch Aufrufen der obigen Funktion erhalten, als Anfangswert. Wir erstellen auch eine Funktion zur Generierung des tatsächlichen Attributwerts (in diesem Fall die Pfaddatenzeichenfolge – Einfügen von Befehlen zwischen den Koordinatenpaaren, damit der Browser weiß, was er mit diesen Koordinaten tun soll). Schließlich nehmen wir jedes Attribut, für das wir Daten gespeichert haben, und setzen seinen Wert auf den Wert, der von der zuvor genannten Funktion zurückgegeben wird.

(function init() {
  /* same as before */
  
  O.d = {
    ini: getStarPoints(), 
    afn: function(pts) {
      return pts.reduce((a, c, i) => {
        return a + (i%3 ? ' ' : 'C') + c
      }, `M${pts[pts.length - 1]}`)
    }
  };
	
  for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p].ini))
})();

Das Ergebnis ist im folgenden Pen zu sehen.

Sehen Sie sich den Pen von thebabydino (@thebabydino) auf CodePen an.

Dies ist ein vielversprechender Anfang. Wir möchten jedoch, dass die erste Spitze des generierenden Pentagramms nach unten zeigt und die erste Spitze des resultierenden Sterns nach oben. Derzeit zeigen beide nach rechts. Das liegt daran, dass wir bei (3 Uhr) beginnen. Um also bei 6 Uhr zu beginnen, addieren wir 90° (π/2 im Bogenmaß) zu jedem aktuellen Winkel in der Funktion getStarPoints().

ca = i*BAD + .5*Math.PI

Dadurch zeigt die erste Spitze des generierenden Pentagramms und des resultierenden Sterns nach unten. Um den Stern zu drehen, müssen wir sein transform-Attribut auf eine halbe Kreisdrehung setzen. Dazu setzen wir zunächst einen Anfangsdrehwinkel von -180. Danach setzen wir die Funktion, die den tatsächlichen Attributwert generiert, auf eine Funktion, die einen String aus einem Funktionsnamen und einem Argument generiert.

function fnStr(fname, farg) { return `${fname}(${farg})` };

(function init() {
  /* same as before */
  
  O.transform = { ini: -180,  afn: (ang) => fnStr('rotate', ang) };
	
  /* same as before */
})();

Wir geben unserem Stern auf ähnliche Weise auch eine goldene fill. Wir setzen ein RGB-Array als Anfangswert im Fall von fill und verwenden eine ähnliche Funktion, um den tatsächlichen Attributwert zu generieren.

(function init() {
  /* same as before */
  
  O.fill = { ini: [255, 215, 0],  afn: (rgb) => fnStr('rgb', rgb) };
	
  /* same as before */
})();

Wir haben jetzt einen schönen goldenen SVG-Stern, der aus fünf kubischen Bézier-Kurven besteht.

Sehen Sie sich den Pen von thebabydino (@thebabydino) auf CodePen an.

Herz

Da wir den Stern haben, sehen wir uns als Nächstes an, wie wir das Herz bekommen können!

Wir beginnen mit zwei sich schneidenden Kreisen gleicher Radien, beide ein Bruchteil (sagen wir für den Moment .25) der viewBox-Größe. Diese Kreise schneiden sich so, dass das Segment, das ihre Mittelpunkte verbindet, auf der x-Achse liegt und das Segment, das ihre Schnittpunkte verbindet, auf der y-Achse liegt. Wir nehmen auch an, dass diese beiden Segmente gleich sind.

Illustration showing the helper circles we start with, their radii and the segments connecting their central points and their intersection points.
Wir beginnen mit zwei Kreisen gleichen Radius, deren Mittelpunkte auf der Horizontalachse liegen und die sich auf der Vertikalachse schneiden (live).

Als Nächstes zeichnen wir Durchmesser durch den oberen Schnittpunkt und dann Tangenten durch die gegenüberliegenden Punkte dieser Durchmesser. Diese Tangenten schneiden sich auf der y-Achse.

Illustration showing the helper circles we start with, their passing through their upper intersection point, the tangents at the diametrically opposite points and their intersection.
Konstruktion von Durchmessern durch den oberen Schnittpunkt und Tangenten an den Kreis an den gegenüberliegenden Enden dieser Durchmesser, Tangenten, die sich auf der Vertikalachse schneiden (live).

Der obere Schnittpunkt und die diametral gegenüberliegenden Punkte ergeben drei der fünf benötigten Endpunkte. Die anderen beiden Endpunkte teilen die äußeren Halbkreisbögen in zwei gleiche Teile, was uns vier Viertelkreisbögen ergibt.

Illustration highlighting the end points of the cubic Bézier curves that make up the heart and the coinciding control points of the bottom one of these curves.
Hervorhebung der Endpunkte der kubischen Bézier-Kurven, aus denen das Herz besteht, und der zusammenfallenden Kontrollpunkte der unteren dieser Kurven (live).

Beide Kontrollpunkte für die untere Kurve fallen mit dem Schnittpunkt der beiden zuvor gezeichneten Tangenten zusammen. Aber was ist mit den anderen vier Kurven? Wie können wir von Kreisbögen zu kubischen Bézier-Kurven gelangen?

Wir haben keine kubische Bézier-Kurven-Entsprechung für einen Viertelkreisbogen, aber wir können eine sehr gute Annäherung finden, wie in diesem Artikel erklärt.

Der Kernpunkt ist, dass wir von einem Viertelkreisbogen mit Radius R ausgehen und Tangenten an die Endpunkte dieses Bogens (N und Q) zeichnen. Diese Tangenten schneiden sich bei P. Das Viereck ONPQ hat alle Winkel gleich 90° (oder π/2), drei davon per Konstruktion (O entspricht einem 90° Bogen und die Tangente an einen Punkt dieses Kreises ist immer senkrecht zur radialen Linie zum selben Punkt) und der letzte per Berechnung (die Summe der Winkel in einem Viereck beträgt immer 360° und die anderen drei Winkel ergeben 270°). Das macht ONPQ zu einem Rechteck. Aber ONPQ hat auch zwei aufeinanderfolgende gleiche Kanten (ON und OQ sind beide radiale Linien mit einer Länge von R), was es zu einem Quadrat der Kante R macht. Die Längen von NP und QP sind also ebenfalls gleich R.

Illustration showing the control points we need to approximate a quarter circle arc with a cubic Bézier curve.
Annäherung eines Viertelkreisbogens mit einer kubischen Bézier-Kurve (live).

Die Kontrollpunkte der kubischen Kurve, die unseren Bogen annähert, liegen auf den Tangentenlinien NP und QP, im Abstand von C·R von den Endpunkten, wobei C die Konstante ist, die der zuvor verlinkte Artikel mit .551915 berechnet.

Angesichts all dessen können wir nun mit der Berechnung der Koordinaten der Endpunkte und Kontrollpunkte der kubischen Kurven beginnen, aus denen unser Stern besteht.

Aufgrund der Art und Weise, wie wir dieses Herz konstruiert haben, ist TO0SO1 (siehe Abbildung unten) ein Quadrat, da es alle gleichen Kanten hat (alle sind Radien eines unserer beiden gleich großen Kreise) und seine Diagonalen per Konstruktion gleich sind (wir sagten, dass die Entfernung zwischen den Mittelpunkten gleich der Entfernung zwischen den Schnittpunkten ist). Hier ist O der Schnittpunkt der Diagonalen und OT ist die Hälfte der ST-Diagonale. T und S liegen auf der y-Achse, also ist ihre x-Koordinate 0. Ihre y-Koordinate im Betrag entspricht dem Segment OT, das die Hälfte der Diagonale ist (ebenso wie das Segment OS).

Illustration showing how the central points and the intersection points of the two helper circles form a square.
Das Quadrat TO0SO1 (live).

Wir können jedes Quadrat der Kantenlänge l in zwei gleiche rechtwinklige gleichschenklige Dreiecke teilen, bei denen die Katheten mit den Quadratkanten und die Hypotenuse mit einer Diagonale zusammenfallen.

Illustration showing how a square can be split into two congruent right isosceles triangles.
Jedes Quadrat kann in zwei kongruente rechtwinklige gleichschenklige Dreiecke geteilt werden (live).

Mithilfe eines dieser rechtwinkligen Dreiecke können wir die Hypotenuse (und damit die Quadratdiagonale) mit dem Satz des Pythagoras berechnen: d² = l² + l². Dies ergibt die Quadratdiagonale als Funktion der Kante d = √(2∙l) = l∙√2 (umgekehrt ist die Kante als Funktion der Diagonale l = d/√2). Es bedeutet auch, dass die Hälfte der Diagonale d/2 = (l∙√2)/2 = l/√2 ist.

Wenn wir dies auf unser Quadrat TO0SO1 der Kantenlänge R anwenden, erhalten wir, dass die y-Koordinate von T (die im Betrag der Hälfte dieser Quadratdiagonale entspricht) -R/√2 ist und die y-Koordinate von S R/√2 ist.

Illustration showing the coordinates of the vertices of the TO₀SO₁ square.
Die Koordinaten der Eckpunkte des Quadrats TO0SO1 (live).

Ähnlich liegen die Punkte Ok auf der x-Achse, sodass ihre y-Koordinaten 0 sind, während ihre x-Koordinaten durch die halbe Diagonale OOk gegeben sind: ±R/√2.

Dass TO0SO1 ein Quadrat ist, bedeutet auch, dass alle seine Winkel 90° (π/2 im Bogenmaß) Winkel sind.

Illustration showing TAₖBₖS quadrilaterals.
Die Vierecke TAkBkS (live).

In der obigen Abbildung sind die Segmente TBk Durchmessersegmente, was bedeutet, dass die Bögen TBk Halbkreise oder 180° Bögen sind und wir sie mit den Punkten Ak in zwei gleiche Hälften geteilt haben, was uns zwei gleiche 90° Bögen ergibt – TAk und AkBk, die zwei gleichen 90° Winkeln entsprechen, ∠TOkAk und ∠AkOkBk.

Angesichts der Tatsache, dass die Winkel ∠TOkS 90° Winkel sind und die Winkel ∠TOkAk ebenfalls 90° Winkel per Konstruktion sind, ergibt sich, dass die Segmente SAk ebenfalls Durchmessersegmente sind. Dies führt dazu, dass in den Vierecken TAkBkS die Diagonalen TBk und SAk senkrecht, gleich sind und sich in der Mitte schneiden (TOk, OkBk, SOk und OkAk sind alle gleich dem ursprünglichen Kreisradius R). Das bedeutet, die Vierecke TAkBkS sind Quadrate, deren Diagonalen 2∙R betragen.

Von hier aus können wir die Kantenlänge der Vierecke TAkBkS als 2∙R/√2 = R∙√2 ermitteln. Da alle Winkel eines Quadrats 90° sind und die Kante TS mit der vertikalen Achse zusammenfällt, bedeutet dies, dass die Kanten TAk und SBk horizontal und parallel zur x-Achse sind und ihre Länge uns die x-Koordinaten der Punkte Ak und Bk gibt: ±R∙√2.

Da TAk und SBk horizontale Segmente sind, entsprechen die y-Koordinaten der Punkte Ak und Bk denen der Punkte T (-R/√2) und S (R/√2).

Illustration showing the coordinates of the vertices of the TAₖBₖS squares.
Die Koordinaten der Eckpunkte der Quadrate TAkBkS (live).

Eine weitere Erkenntnis ist, dass, da TAkBkS Quadrate sind, AkBk parallel zu TS sind, das auf der y-Achse (vertikal) liegt, daher sind die Segmente AkBk vertikal. Zusätzlich, da die x-Achse parallel zu den Segmenten TAk und SBk ist und die TS-Achse schneidet, schneidet sie folglich auch die Segmente AkBk in der Mitte.

Nun kommen wir zu den Kontrollpunkten.

Wir beginnen mit den zusammenfallenden Kontrollpunkten für die untere Kurve.

Illustration showing the TB₀CB₁ quadrilateral.
Das Viereck TB0CB1 (live).

Das Viereck TB0CB1 hat alle Winkel gleich 90° (∠T, da TO0SO1 ein Quadrat ist; ∠Bk per Konstruktion, da die Segmente BkC Tangenten an den Kreis bei Bk sind und daher senkrecht zu den radialen Linien OkBk an diesem Punkt; und schließlich muss ∠C 90° sein, da die Summe der Winkel in einem Viereck 360° beträgt und die anderen drei Winkel zusammen 270° ergeben), was es zu einem Rechteck macht. Es hat auch zwei aufeinanderfolgende gleiche Kanten – TB0 und TB1 sind beides Durchmesser der ursprünglichen Quadrate und daher beide gleich 2∙R. All dies macht es zu einem Quadrat der Kante 2∙R.

Von hier aus können wir seine Diagonale TC ermitteln – sie ist 2∙R∙√2. Da C auf der y-Achse liegt, ist seine x-Koordinate 0. Seine y-Koordinate ist die Länge des Segments OC. Das Segment OC ist das Segment TC minus das Segment OT: 2∙R∙√2 - R/√2 = 4∙R/√2 - R/√2 = 3∙R/√2.

Illustration showing the coordinates of the vertices of the TB₀CB₁ square.
Die Koordinaten der Eckpunkte des Quadrats TB0CB1 (live).

Somit haben wir nun die Koordinaten der beiden zusammenfallenden Kontrollpunkte für die untere Kurve: (0,3∙R/√2).

Um die Koordinaten der Kontrollpunkte für die anderen Kurven zu erhalten, zeichnen wir Tangenten durch ihre Endpunkte und ermitteln die Schnittpunkte dieser Tangenten bei Dk und Ek.

Illustration showing the TOₖAₖDₖ and AₖOₖBₖEₖ quadrilaterals.
Die Vierecke TOkAkDk und AkOkBkEk (live).

In den Vierecken TOkAkDk haben wir, dass alle Winkel 90° (rechte) Winkel sind, drei davon per Konstruktion (∠DkTOk und ∠DkAkOk sind die Winkel zwischen den radialen und Tangentenlinien bei T und Ak bzw., während ∠TOkAk die Winkel sind, die den Viertelkreisbögen TAk entsprechen) und der vierte per Berechnung (die Summe der Winkel in einem Viereck beträgt 360° und die anderen drei ergeben 270°). Dies macht TOkAkDk zu Rechtecken. Da sie zwei aufeinanderfolgende gleiche Kanten haben (OkT und OkAk sind radiale Segmente der Länge R), sind sie auch Quadrate.

Das bedeutet, die Diagonalen TAk und OkDk sind R∙√2 lang. Wir wissen bereits, dass TAk horizontal sind und da die Diagonalen eines Quadrats senkrecht zueinander stehen, ergibt sich, dass die Segmente OkDk vertikal sind. Das bedeutet, die Punkte Ok und Dk haben die gleiche x-Koordinate, die wir bereits für Ok als ±R/√2 berechnet haben. Da wir die Länge von OkDk kennen, können wir auch die y-Koordinaten ermitteln – sie sind die Diagonallänge (R∙√2) mit einem Minus davor.

Ähnlich haben wir in den Vierecken AkOkBkEk, dass alle Winkel 90° (rechte) Winkel sind, drei davon durch Konstruktion (∠EkAkOk und ∠EkBkOk sind die Winkel zwischen der Radialen und der Tangente bei Ak bzw. Bk, während ∠AkOkBk die Winkel sind, die den Viertelkreisbögen AkBk entsprechen) und der vierte durch Berechnung (die Summe der Winkel in einem Viereck beträgt 360° und die anderen drei ergeben zusammen 270°). Das macht AkOkBkEk zu Rechtecken. Da sie zwei aufeinanderfolgende gleich lange Kanten haben (OkAk und OkBk sind radiale Segmente der Länge R), sind sie auch Quadrate.

Von hier aus erhalten wir, dass die Diagonalen AkBk und OkEk R∙√2 lang sind. Wir wissen, dass die Segmente AkBk vertikal sind und von der horizontalen Achse halbiert werden, was bedeutet, dass die Segmente OkEk auf dieser Achse liegen und die y-Koordinaten der Punkte Ek 0 sind. Da die x-Koordinaten der Punkte Ok ±R/√2 sind und die Segmente OkEk R∙√2 lang sind, können wir auch die der Punkte Ek berechnen – sie sind ±3∙R/√2.

Illustration showing the coordinates of the newly computed vertices of the TOₖAₖDₖ and AₖOₖBₖEₖ squares.
Die Koordinaten der neu berechneten Eckpunkte der Quadrate TOₖAₖDₖ und AₖOₖBₖEₖ (live).

Nun gut, aber diese Schnittpunkte für die Tangenten sind nicht die Kontrollpunkte, die wir für die Kreisbogen-Approximationen benötigen. Die Kontrollpunkte, die wir wollen, liegen auf den Segmenten TDk, AkDk, AkEk und BkEk etwa 55% (dieser Wert wird durch die Konstante C aus dem zuvor erwähnten Artikel gegeben) von den Endpunkten der Kurve (T, Ak, Bk) entfernt. Das bedeutet, dass die Segmente von den Endpunkten zu den Kontrollpunkten C∙R lang sind.

In dieser Situation sind die Koordinaten unserer Kontrollpunkte 1 - C derer der Endpunkte (T, Ak und Bk) plus C derer der Punkte, an denen sich die Tangenten an den Endpunkten schneiden (Dk und Ek).

Also, legen wir das alles in JavaScript-Code!

Genau wie im Sternfall beginnen wir mit einer Funktion getStarPoints(f), die von einem beliebigen Faktor (f) abhängt, der uns hilft, den Radius der Hilfskreise aus der Größe des viewBox zu erhalten. Diese Funktion gibt auch ein Array von Koordinaten zurück, das wir später für die Interpolation verwenden.

Im Inneren berechnen wir die Dinge, die sich während der Funktion nicht ändern. Zuerst der Radius der Hilfskreise. Davon ausgehend, die halbe Diagonale der kleinen Quadrate, deren Kantenlänge diesem Radius des Hilfskreises entspricht, eine halbe Diagonale, die auch der Umkreisradius dieser Quadrate ist. Danach die Koordinaten der Endpunkte unserer kubischen Kurven (die Punkte T, Ak, Bk), im Absolutwert für die entlang der horizontalen Achse. Dann gehen wir zu den Koordinaten der Punkte über, an denen sich die Tangenten durch die Endpunkte schneiden (die Punkte C, Dk, Ek). Diese fallen entweder mit den Kontrollpunkten (C) zusammen oder können uns helfen, die Kontrollpunkte zu erhalten (dies ist der Fall für Dk und Ek).

function getHeartPoints(f = .25) {
  const R = f*D /* helper circle radius  */, 
        RC = Math.round(R/Math.SQRT2) /* circumradius of square of edge R */, 
        XT = 0, YT = -RC /* coords of point T */, 
        XA = 2*RC, YA = -RC /* coords of A points (x in abs value) */, 
        XB = 2*RC, YB = RC /* coords of B points (x in abs value) */, 
        XC = 0, YC = 3*RC /* coords of point C */, 
        XD = RC, YD = -2*RC /* coords of D points (x in abs value) */, 
        XE = 3*RC, YE = 0 /* coords of E points (x in abs value) */;
}

Die interaktive Demo unten zeigt die Koordinaten dieser Punkte bei Klick

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Nun können wir auch die Kontrollpunkte aus den Endpunkten und den Punkten, an denen sich die Tangenten durch die Endpunkte schneiden, ermitteln

function getHeartPoints(f = .25) {
  /* same as before */
  const /* const for cubic curve approx of quarter circle */
        C = .551915, 
        CC = 1 - C, 
        /* coords of ctrl points on TD segs */
        XTD = Math.round(CC*XT + C*XD), YTD = Math.round(CC*YT + C*YD), 
        /* coords of ctrl points on AD segs */
        XAD = Math.round(CC*XA + C*XD), YAD = Math.round(CC*YA + C*YD), 
        /* coords of ctrl points on AE segs */
        XAE = Math.round(CC*XA + C*XE), YAE = Math.round(CC*YA + C*YE), 
        /* coords of ctrl points on BE segs */
        XBE = Math.round(CC*XB + C*XE), YBE = Math.round(CC*YB + C*YE);

  /* same as before */
}

Als Nächstes müssen wir die relevanten Koordinaten in ein Array packen und dieses Array zurückgeben. Im Fall des Sterns begannen wir mit der unteren Kurve und gingen dann im Uhrzeigersinn vor, also machen wir dasselbe hier. Für jede Kurve fügen wir zwei Sätze von Koordinaten für die Kontrollpunkte und dann einen Satz für den Punkt hinzu, an dem die aktuelle Kurve endet.

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Beachten Sie, dass im Fall der ersten (unteren) Kurve die beiden Kontrollpunkte zusammenfallen, sodass wir denselben Koordinatenpaar zweimal hinzufügen. Der Code sieht nicht annähernd so gut aus wie im Fall des Sterns, aber er muss ausreichen

return [
  [XC, YC], [XC, YC], [-XB, YB], 
  [-XBE, YBE], [-XAE, YAE], [-XA, YA], 
  [-XAD, YAD], [-XTD, YTD], [XT, YT], 
  [XTD, YTD], [XAD, YAD], [XA, YA], 
  [XAE, YAE], [XBE, YBE], [XB, YB]
];

Wir können nun unsere Stern-Demo nehmen und die Funktion getHeartPoints() für den Endzustand verwenden, ohne Rotation und stattdessen mit einer karminroten fill. Dann setzen wir den aktuellen Zustand auf die Endform, nur damit wir das Herz sehen können

function fnStr(fname, farg) { return `${fname}(${farg})` };

(function init() {	
  _SVG.setAttribute('viewBox', [-.5*D, -.5*D, D, D].join(' '));
	
  O.d = {
    ini: getStarPoints(), 
    fin: getHeartPoints(), 
    afn: function(pts) {
      return pts.reduce((a, c, i) => {
        return a + (i%3 ? ' ' : 'C') + c
      }, `M${pts[pts.length - 1]}`)
    }
  };
	
  O.transform = {
    ini: -180, 
    fin: 0, 
    afn: (ang) => fnStr('rotate', ang)
  };
	
  O.fill = {
    ini: [255, 215, 0], 
    fin: [220, 20, 60], 
    afn: (rgb) => fnStr('rgb', rgb)
  };
	
  for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p].fin))
})();

Das ergibt ein schön aussehendes Herz

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Sicherstellung einer konsistenten Ausrichtung der Formen

Wenn wir jedoch die beiden Formen übereinander legen, ohne fill oder transform, nur mit einem stroke, sehen wir, dass die Ausrichtung ziemlich schlecht aussieht

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Der einfachste Weg, dieses Problem zu lösen, ist, das Herz um einen Betrag nach oben zu verschieben, der vom Radius der Hilfskreise abhängt

return [ /* same coords */ ].map(([x, y]) => [x, y - .09*R])

Wir haben jetzt eine viel bessere Ausrichtung, unabhängig davon, wie wir den Faktor f in beiden Fällen anpassen. Dies ist der Faktor, der den Umkreisradius des Pentagramms im Verhältnis zur viewBox-Größe bestimmt (wenn der Standard .5 ist) und den Radius der Hilfskreise im Verhältnis zur gleichen viewBox-Größe im Fall des Herzens (wenn der Standard .25 ist).

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Wechseln zwischen den beiden Formen

Wir wollen bei jedem Klick von einer Form zur anderen wechseln. Um dies zu erreichen, legen wir eine Richtungsvariable dir fest, die 1 ist, wenn wir vom Stern zum Herz wechseln, und -1, wenn wir vom Herz zum Stern wechseln. Anfangs ist sie -1, als ob wir gerade vom Herz zum Stern gewechselt hätten.

Dann fügen wir einen 'click'-Event-Listener zum _SHAPE-Element hinzu und programmieren, was in dieser Situation passiert – wir ändern das Vorzeichen der Richtungsvariable (dir) und ändern die Attribute der Form, sodass wir von einem goldenen Stern zu einem karminroten Herz wechseln oder umgekehrt

let dir = -1;

(function init() {	
  /* same as before */
	
  _SHAPE.addEventListener('click', e => {
    dir *= -1;
		
    for(let p in O)
      _SHAPE.setAttribute(p, O[p].afn(O[p][dir > 0 ? 'fin' : 'ini']));
  }, false);
})();

Und wir wechseln nun bei jedem Klick zwischen den beiden Formen

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Morphing von einer Form zur anderen

Was wir uns jedoch wirklich wünschen, ist keine abrupte Änderung von einer Form zur anderen, sondern eine allmähliche. Daher verwenden wir die Interpolationstechniken, die im vorherigen Artikel erklärt wurden, um dies zu erreichen.

Wir legen zunächst eine Gesamtzahl von Bildern für unsere Übergänge fest (NF) und wählen die Art der Timing-Funktionen, die wir verwenden möchten – eine ease-in-out-Funktion für den Übergang der path-Form von Stern zu Herz, eine bounce-ini-fin-Funktion für den Rotationswinkel und eine ease-out-Funktion für die fill. Wir nehmen nur diese auf, obwohl wir später andere hinzufügen könnten, falls wir unsere Meinung ändern und andere Optionen erkunden möchten.

/* same as before */
const NF = 50, 
      TFN = {
        'ease-out': function(k) {
          return 1 - Math.pow(1 - k, 1.675)
        }, 
        'ease-in-out': function(k) {
          return .5*(Math.sin((k - .5)*Math.PI) + 1)
        },
        'bounce-ini-fin': function(k, s = -.65*Math.PI, e = -s) {
          return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s))
        }
      };

Dann geben wir an, welche dieser Timing-Funktionen wir für jede Eigenschaft verwenden, die wir animieren

(function init() {	
  /* same as before */
	
  O.d = {
    /* same as before */
    tfn: 'ease-in-out'
  };
	
  O.transform = {
    /* same as before */
    tfn: 'bounce-ini-fin'
  };
  	
  O.fill = {
    /* same as before */
    tfn: 'ease-out'
  };

  /* same as before */
})();

Wir fahren fort, Variablen für die Anforderungs-ID (rID) und den aktuellen Frame (cf) hinzuzufügen, eine Funktion update(), die wir zuerst bei Klick aufrufen, dann bei jeder Aktualisierung der Anzeige, bis der Übergang abgeschlossen ist, und eine Funktion stopAni(), um diese Animationsschleife zu verlassen. Innerhalb der Funktion update() aktualisieren wir… nun ja, den aktuellen Frame cf, berechnen einen Fortschritt k und entscheiden, ob wir das Ende des Übergangs erreicht haben und die Animationsschleife verlassen müssen oder ob wir weitermachen.

Wir fügen auch eine Multiplikatorvariable m hinzu, die wir verwenden, damit wir die Timing-Funktionen nicht umkehren, wenn wir vom Endzustand (Herz) zurück zum Anfangszustand (Stern) gehen.

let rID = null, cf = 0, m;

function stopAni() {
  cancelAnimationFrame(rID);
  rID = null;  
};

function update() {
  cf += dir;
	
  let k = cf/NF;
  
  if(!(cf%NF)) {
    stopAni();
    return
  }
  
  rID = requestAnimationFrame(update)
};

Dann müssen wir ändern, was bei Klick passiert

addEventListener('click', e => {
  if(rID) stopAni();
  dir *= -1;
  m = .5*(1 - dir);
  update();
}, false);

Innerhalb der Funktion update() möchten wir die Attribute, die wir animieren, auf Zwischenwerte setzen (abhängig vom Fortschritt k). Wie im vorherigen Artikel gesehen, ist es gut, die Bereiche zwischen den End- und Anfangswerten von Anfang an vorab zu berechnen, noch bevor der Listener gesetzt wird. Das ist also unser nächster Schritt: eine Funktion erstellen, die den Bereich zwischen Zahlen berechnet, sei es als solche oder in Arrays, egal wie tief sie sind, und dann diese Funktion verwenden, um die Bereiche für die Eigenschaften festzulegen, die wir animieren wollen.

function range(ini, fin) {
  return typeof ini == 'number' ? 
         fin - ini : 
         ini.map((c, i) => range(ini[i], fin[i]))
};

(function init() {	
  /* same as before */
	
  for(let p in O) {
    O[p].rng = range(O[p].ini, O[p].fin);
    _SHAPE.setAttribute(p, O[p].afn(O[p].ini));
  }
	
  /* same as before */
})();

Jetzt müssen wir nur noch den Interpolationsteil in der update()-Funktion schreiben. Mithilfe einer Schleife gehen wir alle Attribute durch, die wir von einem Endzustand zum anderen glatt ändern wollen. Innerhalb dieser Schleife setzen wir ihren aktuellen Wert auf den Wert, den wir als Ergebnis einer Interpolationsfunktion erhalten, die vom Anfangswert(en), Bereich(en) des aktuellen Attributs (ini und rng), der verwendeten Timing-Funktion (tfn) und dem Fortschritt (k) abhängt.

function update() {	
  /* same as before */
	
  for(let p in O) {
    let c = O[p];

    _SHAPE.setAttribute(p, c.afn(int(c.ini, c.rng, TFN[c.tfn], k)));
  }
	
  /* same as before */
};

Der letzte Schritt ist das Schreiben dieser Interpolationsfunktion. Sie ähnelt ziemlich der, die uns die Bereichswerte liefert

function int(ini, rng, tfn, k) {
  return typeof ini == 'number' ? 
         Math.round(ini + (m + dir*tfn(m + dir*k))*rng) : 
         ini.map((c, i) => int(ini[i], rng[i], tfn, k))
};

Das gibt uns schließlich eine Form, die bei Klick von einem Stern zu einem Herz morpht und bei einem zweiten Klick zurück zu einem Stern!

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Es ist fast das, was wir wollten – es gibt noch ein winziges Problem. Bei zyklischen Werten wie Winkelwerten wollen wir uns beim zweiten Klick nicht um einen halben Kreis zurückbewegen. Stattdessen wollen wir uns weiterhin in die gleiche Richtung um einen weiteren halben Kreis bewegen. Wenn wir diesen halben Kreis nach dem zweiten Klick mit dem nach dem ersten Klick zurückgelegten addieren, erhalten wir einen vollen Kreis, so dass wir wieder am Anfang sind.

Wir setzen dies in Code um, indem wir eine optionale Kontinuitätseigenschaft hinzufügen und die Update- und Interpolationsfunktionen etwas anpassen

function int(ini, rng, tfn, k, cnt) {
  return typeof ini == 'number' ? 
         Math.round(ini + cnt*(m + dir*tfn(m + dir*k))*rng) : 
         ini.map((c, i) => int(ini[i], rng[i], tfn, k, cnt))
};

function update() {	
  /* same as before */
	
  for(let p in O) {
    let c = O[p];

    _SHAPE.setAttribute(p, c.afn(int(c.ini, c.rng, TFN[c.tfn], k, c.cnt ? dir : 1)));
  }
	
  /* same as before */
};

(function init() {	
  /* same as before */
	
  O.transform = {
    ini: -180, 
    fin: 0, 
    afn: (ang) => fnStr('rotate', ang),
    tfn: 'bounce-ini-fin',
    cnt: 1
  };
	
  /* same as before */
})();

Wir haben nun das Ergebnis, nach dem wir gesucht haben: eine Form, die sich von einem goldenen Stern in ein karminrotes Herz verwandelt und sich bei jeder Bewegung zwischen den Zuständen um eine halbe Umdrehung im Uhrzeigersinn dreht

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.