Ich bin vor einiger Zeit auf ein paar solcher Animationen gestoßen und das brachte mich auf die Idee, meine eigenen Versionen mit möglichst wenig Code zu erstellen, ohne externe Bibliotheken, unter Verwendung verschiedener Methoden, von denen einige die neueren Funktionen nutzen, die wir heutzutage verwenden können, wie z. B. CSS-Variablen. Dieser Artikel führt Sie durch den Prozess des Erstellens dieser Demos.
Bevor wir überhaupt etwas tun, ist dies die Animation, die wir hier erreichen wollen

Unabhängig davon, welche Methode wir wählen, um die obige Animation nachzubilden, beginnen wir immer mit der statischen Yin-Yang-Form, die wie unten dargestellt aussieht
Die Struktur dieser Startform wird durch die folgende Abbildung beschrieben
Zuerst haben wir einen großen Kreis mit dem Durchmesser d. In diesen Kreis passen wir eng zwei kleinere Kreise ein, von denen jeder einen Durchmesser hat, der halb so groß ist wie der Durchmesser unseres anfänglichen großen Kreises. Das bedeutet, dass der Durchmesser jedes dieser beiden kleineren Kreise gleich dem Radius r des großen Kreises ist (oder .5*d). Innerhalb jedes dieser Kreise mit dem Durchmesser r haben wir einen noch kleineren konzentrischen Kreis. Wenn wir einen Durchmesser für den großen Kreis zeichnen, der durch alle Mittelpunkte aller dieser Kreise verläuft – das Liniensegment AB in der obigen Abbildung –, teilt es den Schnittpunkt mit den inneren Kreisen in 6 gleiche kleinere Segmente. Das bedeutet, dass der Durchmesser eines der kleinsten Kreise r/3 (oder d/6) und sein Radius r/6 beträgt.
Mit all dem im Hinterkopf, beginnen wir mit der ersten Methode!
Reines HTML + CSS
In diesem Fall können wir es mit einem Element und seinen beiden Pseudoelementen realisieren. Das Vorgehen beim Erstellen des Symbols wird durch die folgende Animation veranschaulicht (da sich das Ganze drehen wird, spielt es keine Rolle, ob wir die Achsen wechseln)
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Das eigentliche Element ist der große Kreis und er hat einen von oben nach unten verlaufenden Farbverlauf mit einem scharfen Übergang genau in der Mitte. Die Pseudoelemente sind die kleineren Kreise, die wir darüber legen. Der Durchmesser eines der kleineren Kreise ist halb so groß wie der Durchmesser des großen Kreises. Beide kleineren Kreise sind vertikal mittig am großen Kreis ausgerichtet.
Beginnen wir also mit dem Schreiben des Codes, der dies erreichen kann!
Zuerst entscheiden wir uns für einen Durchmesser $d für den großen Kreis. Wir verwenden Viewport-Einheiten, damit alles beim Vergrößern schön skaliert. Wir setzen diesen Durchmesserwert als width und height, machen das Element mit border-radius rund und geben ihm einen von oben nach unten verlaufenden Farbverlauf background mit einem scharfen Übergang von black zu white in der Mitte.
$d: 80vmin;
.☯ {
width: $d; height: $d;
border-radius: 50%;
background: linear-gradient(black 50%, white 0);
}
Bisher alles gut
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Kommen wir nun zu den kleineren Kreisen, die wir mit Pseudoelementen erstellen. Wir geben unserem Element display: flex und richten seine Kinder (oder Pseudoelemente in unserem Fall) vertikal mit align-items: center an ihm aus. Wir geben diesen Pseudoelementen die halbe height (50%) ihres Elternelements und stellen sicher, dass sie horizontal jeweils die Hälfte des großen Kreises abdecken. Schließlich machen wir sie mit border-radius rund, geben ihnen einen Dummy-background und setzen die content-Eigenschaft, nur damit wir sie sehen können.
.☯ {
display: flex;
align-items: center;
/* same styles as before */
&:before, &:after {
flex: 1;
height: 50%;
border-radius: 50%;
background: #f90;
content: '';
}
}
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Als Nächstes müssen wir ihnen unterschiedliche Hintergründe geben
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
background: black;
}
&:after { background: white }
}
Jetzt kommen wir der Sache näher!
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Alles, was noch zu tun ist, bevor wir das statische Symbol erhalten, ist, diesen beiden Pseudoelementen Rahmen zu geben. Das schwarze sollte einen weißen Rahmen erhalten, während das weiße einen schwarzen Rahmen erhalten sollte. Diese Rahmen sollten ein Drittel des Durchmessers des Pseudoelements betragen, was ein Drittel der Hälfte des Durchmessers des großen Kreises ist – das ergibt $d/6.
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
border: solid $d/6 white;
}
&:after {
/* same styles as before */
border-color: black;
}
}
Das Ergebnis sieht jedoch nicht ganz richtig aus
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Das liegt daran, dass der border vertikal zur height addiert wird, anstatt davon abgezogen zu werden. Horizontal haben wir keine width eingestellt, sodass er vom verfügbaren Platz abgezogen wird. Wir haben hier zwei mögliche Lösungen. Eine wäre, box-sizing: border-box für die Pseudoelemente festzulegen. Die zweite wäre, die height der Pseudoelemente auf $d/6 zu ändern – wir werden uns für diese entscheiden
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Wir haben nun die Grundform, also weiter zur Animation! Diese Animation beinhaltet den Übergang von dem Zustand, in dem sich das erste Pseudoelement auf beispielsweise die Hälfte seiner ursprünglichen Größe verkleinert hat (was einem Skalierungsfaktor $f von .5 entspricht), während sich das zweite Pseudoelement erweitert hat, um den gesamten verfügbaren Platz einzunehmen – also bis zum Durchmesser des großen Kreises (was doppelt so groß ist wie seine ursprüngliche Größe) abzüglich des Durchmessers des ersten Kreises (was $f seiner ursprünglichen Größe entspricht) bis zu dem Zustand, in dem sich das zweite Pseudoelement auf $f seiner ursprünglichen Größe verkleinert hat und sich das erste Pseudoelement auf 2 - $f seiner ursprünglichen Größe erweitert hat. Der erste Pseudoelement-Kreis skaliert relativ zu seinem linken Punkt (daher müssen wir einen transform-origin von 0 50% einstellen), während der zweite relativ zu seinem rechten Punkt skaliert (100% 50%).
$f: .5;
$t: 1s;
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
transform-origin: 0 50%;
transform: scale($f);
animation: s $t ease-in-out infinite alternate;
}
&:after {
/* same styles as before */
transform-origin: 100% 50%;
animation-delay: -$t;
}
}
@keyframes s { to { transform: scale(2 - $f) } }
Nun haben wir die sich ändernde Form, die wir uns gewünscht haben
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Der letzte Schritt ist, das gesamte Symbol rotieren zu lassen
$t: 1s;
.☯ {
/* same styles as before */
animation: r 2*$t linear infinite;
}
@keyframes r { to { transform: rotate(1turn) } }
Und wir haben das Endergebnis!
Es gibt jedoch noch eine weitere Sache, die wir tun können, um das kompilierte CSS effizienter zu gestalten: die Redundanz mit CSS-Variablen eliminieren!
white kann im HSL-Format als hsl(0, 0%, 100%) geschrieben werden. Der Farbton und die Sättigung spielen keine Rolle, jeder Wert mit einer Helligkeit von 100% ist white, also setzen wir beide einfach auf 0, um uns das Leben leichter zu machen. Ähnlich kann black als hsl(0, 0%, 0%) geschrieben werden. Auch hier spielen Farbton und Sättigung keine Rolle, jeder Wert mit einer Helligkeit von 0% ist black. Angesichts dessen wird unser Code zu
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
border: solid $d/6 hsl(0, 0%, 100% /* = 1*100% = (1 - 0)*100% */);
transform-origin: 0 /* = 0*100% */ 50%;
background: hsl(0, 0%, 0% /* 0*100% */);
animation: s $t ease-in-out infinite alternate;
animation-delay: 0 /* = 0*-$t */;
}
&:after {
/* same styles as before */
border-color: hsl(0, 0%, 0% /* = 0*100% = (1 - 1)*100% */);
transform-origin: 100% /* = 1*100% */ 50%;
background: hsl(0, 0%, 100% /* = 1*100% */);
animation-delay: -$t /* = 1*-$t */;
}
}
Aus dem Obigen ergibt sich, dass
- die
x-Komponente unserestransform-originfür das erste Pseudoelementcalc(0*100%)und für das zweitecalc(1*100%)ist - unsere
border-colorfür das erste Pseudoelementhsl(0, 0%, calc((1 - 0)*100%))und für das zweitehsl(0, 0%, calc((1 - 1)*100%))ist - unser
backgroundfür das erste Pseudoelementhsl(0, 0%, calc(0*100%))und für das zweitehsl(0, 0%, calc(1*100%))ist - unsere
animation-delayfür das erste Pseudoelementcalc(0*#{-$t})und für das zweitecalc(1*#{-$t})ist
Das bedeutet, wir können eine benutzerdefinierte Eigenschaft verwenden, die als Schalter fungiert und für das erste Pseudoelement 0 und für das zweite 1 ist
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
--i: 0;
border: solid $d/6 hsl(0, 0%, calc((1 - var(--i))*100%));
transform-origin: calc(var(--i)*100%) 50%;
background: hsl(0, 0%, calc(var(--i)*100%));
animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate;
}
&:after { --i: 1 }
}
Dies eliminiert die Notwendigkeit, all diese Regeln zweimal zu schreiben: Alles, was wir jetzt tun müssen, ist, den Schalter umzulegen! Leider funktioniert dies derzeit nur in WebKit-Browsern, da Firefox und Edge die Verwendung von calc() als Wert für animation-delay nicht unterstützen und Firefox es auch nicht innerhalb von hsl() unterstützt.
Update: Firefox 57+ unterstützt calc() als Wert für animation-delay und Firefox 59+ unterstützt es auch innerhalb von hsl().
Canvas + JavaScript
Während einige Leute diese Methode für übertrieben halten mögen, gefällt sie mir wirklich, weil sie ungefähr die gleiche Menge an Code wie die CSS-Methode erfordert, gute Unterstützung und gute Leistung bietet.
Wir beginnen mit einem canvas-Element und einigen grundlegenden Stilen, nur um es in der Mitte seines Containers (das ist in unserem Fall das body-Element) zu positionieren und es sichtbar zu machen. Wir machen es auch mit border-radius rund, um unsere Arbeit beim Zeichnen auf dem canvas zu vereinfachen.
$d: 80vmin;
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: lightslategray;
}
canvas {
width: $d; height: $d;
border-radius: 50%;
background: white;
}
Bisher alles gut – wir haben eine weiße Scheibe
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Okay, jetzt kommen wir zum JavaScript-Teil! Zuerst müssen wir das canvas-Element, den 2D-Kontext abrufen und die width- und height-Attribute des canvas-Elements festlegen (Dinge, die wir auf dem canvas zeichnen, würden sonst gestreckt aussehen). Dann benötigen wir einen Radius für unseren großen Kreis. Wir erhalten diesen Radius als Hälfte der berechneten Größe des canvas-Elements und, nachdem wir das getan haben, verschieben wir unseren Kontext so, dass wir den Punkt 0,0 unseres canvas genau in die Mitte bringen (er befindet sich ursprünglich in der oberen linken Ecke). Wir stellen sicher, dass wir den Radius und die Attribute width und height bei jeder Größenänderung neu berechnen, da wir in CSS die canvas-Abmessungen vom Viewport abhängig gemacht haben.
const _C = document.querySelector('canvas'),
CT = _C.getContext('2d');
let r;
function size() {
_C.width = _C.height = Math.round(_C.getBoundingClientRect().width);
r = .5*_C.width;
CT.translate(r, r);
};
size();
addEventListener('resize', size, false);
Nachdem wir dies getan haben, können wir zum Zeichnen auf dem Canvas übergehen. Was zeichnen? Nun, eine Form, die aus drei Bögen besteht, wie in der folgenden Abbildung gezeigt
Um einen Bogen auf einem 2D-Canvas zu zeichnen, müssen wir ein paar Dinge wissen. Zuerst die Koordinaten des Mittelpunkts des Kreises, zu dem dieser Bogen gehört. Dann brauchen wir den Radius dieses Kreises und die Winkel (relativ zur x-Achse des lokalen Koordinatensystems des Kreises), an denen die Start- und Endpunkte des Bogens liegen. Schließlich müssen wir wissen, ob wir uns vom Startpunkt zum Endpunkt im Uhrzeigersinn bewegen oder nicht (wenn wir dies nicht angeben, ist der Standard im Uhrzeigersinn).
Der erste Bogen befindet sich auf dem großen Kreis, dessen Durchmesser gleich den Canvas-Abmessungen ist, und da wir den Punkt 0,0 des canvas genau in die Mitte dieses Kreises gelegt haben, kennen wir beide ersten Koordinatensätze (es ist 0,0) und den Kreisradius (es ist r). Der Startpunkt dieses Bogens ist der linkeste Punkt dieses Kreises – dieser Punkt liegt bei -180° (oder -π). Der Endpunkt ist der rechteste Punkt des Kreises, der bei 0° (auch 0 im Bogenmaß) liegt. Wenn Sie eine Auffrischung der Winkel auf einem Kreis benötigen, schauen Sie sich diese Hilfsdemo an.
Das bedeutet, wir können einen Pfad erstellen und diesen Bogen hinzufügen und, um zu sehen, was wir bisher haben, können wir diesen Pfad schließen (was in diesem Fall bedeutet, den Endpunkt unseres Bogens mit dem Startpunkt zu verbinden) und ihn füllen (mit dem Standardfüllwert, der black ist).
CT.beginPath();
CT.arc(0, 0, r, -Math.PI, 0);
CT.closePath();
CT.fill();
Das Ergebnis ist in folgendem Pen zu sehen
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Nun zum zweiten Bogen. Die Koordinaten des Mittelpunkts des Kreises, auf dem er sich befindet, sind .5*r,0 und sein Radius ist .5*r (halb so groß wie der Radius des großen Kreises). Er verläuft von 0 bis π, wobei er sich dabei im Uhrzeigersinn bewegt. Der Bogen, den wir unserem Pfad vor dem Schließen hinzufügen, ist also
CT.arc(.5*r, 0, .5*r, 0, Math.PI);
Nachdem dieser Bogen hinzugefügt wurde, wird unsere Form
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Jetzt haben wir noch einen Bogen übrig. Der Radius ist derselbe wie beim vorherigen (.5*r) und der erste Koordinatensatz ist -.5*r,0. Dieser Bogen verläuft von 0 bis -π und ist der erste Bogen, der sich nicht im Uhrzeigersinn bewegt, also müssen wir diese Flagge ändern
CT.arc(-.5*r, 0, .5*r, 0, -Math.PI, true);
Wir haben nun die gewünschte Form
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Als Nächstes fügen wir den schwarzen Kreis zu diesem Pfad hinzu. Wir erstellen keinen weiteren Pfad, da das Ziel ist, alle Formen mit derselben Füllung in demselben Pfad für eine bessere Leistung zu gruppieren. Das Aufrufen von fill() ist teuer, daher wollen wir dies nicht öfter tun, als unbedingt nötig.
Ein Kreis ist nur ein Bogen von 0° bis 360° (oder von 0 bis 2*π). Der Mittelpunkt dieses Kreises fällt mit dem des letzten Bogens, den wir gezeichnet haben (-.5*r, 0), zusammen und sein Radius ist ein Drittel des Radius der beiden vorherigen Bögen.
CT.arc(-.5*r, 0, .5*r/3, 0, 2*Math.PI);
Jetzt sind wir dem vollständigen Symbol sehr nahe
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Alles, was noch zu tun ist, ist, einen weißen Kreis zu erstellen, der symmetrisch zum schwarzen Kreis in Bezug auf die y-Achse ist. Das bedeutet, wir müssen zu einer weißen Füllung wechseln, einen neuen Pfad starten und dann einen Bogen hinzufügen, der fast denselben Befehl verwendet wie zum Hinzufügen der schwarzen Kreisform – der einzige Unterschied ist, dass wir das Vorzeichen der x-Koordinate umkehren (diesmal ist es + und nicht -). Danach schließen wir diesen Pfad und füllen ihn.
CT.fillStyle = 'white';
CT.beginPath();
CT.arc(.5*r, 0, .5*r/3, 0, 2*Math.PI);
CT.closePath();
CT.fill();
Wir haben nun das statische Symbol!
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Für die Animation wollen wir von dem Zustand, in dem der erste der kleineren Bögen auf die Hälfte seines ursprünglichen Radius geschrumpft ist (daher verwenden wir einen Skalierungsfaktor F von .5), zu dem Zustand wechseln, in dem der andere Bogen entsprechend erweitert wurde, bis zu dem Zustand, in dem sich diese anfänglichen Radien umkehren.
Im Anfangszustand, gegeben dass der Radius der kleineren Bögen anfangs .5*r ist, ist der Radius des ersten nach dem Herunterskalieren um einen Faktor F r1 = F*.5*r. Da die Radien der kleineren Kreise sich zum Radius des großen Kreises r addieren müssen, ist der Radius des zweiten kleineren Kreises r2 = r - r1 = r - F*.5*r.
Um die x-Koordinate des Ursprungs des ersten kleineren Bogens für den Anfangszustand zu erhalten, müssen wir seinen Radius von der x-Koordinate des Punktes abziehen, an dem er beginnt. Auf diese Weise erhalten wir, dass diese Koordinate r - r1 = r2 ist. Ähnlich müssen wir, um die x-Koordinate des Ursprungs des zweiten kleineren Bogens zu erhalten, seinen Radius zu der Koordinate des Punktes addieren, an dem er endet. Auf diese Weise erhalten wir, dass diese Koordinate -r + r2 = -(r - r2) = -r1 ist.
Für den Endzustand sind die Werte der beiden Radien vertauscht. Der zweite ist F*.5*r, während der erste r - F*.5*r ist.
Mit jedem Frame unserer Animation erhöhen wir den aktuellen Radius des ersten kleineren Bogens vom Minimalwert (F*.5*r) zum Maximalwert (r - F*.5*r) und beginnen dann, ihn zum Minimalwert zu verringern, und dann wiederholt sich der Zyklus, während gleichzeitig der Radius des anderen kleineren Bogens entsprechend skaliert wird.
Um dies zu tun, legen wir zuerst den minimalen und maximalen Radius in der Funktion size() fest
const F = .5;
let rmin, rmax;
function size() {
/* same as before */
rmin = F*.5*r;
rmax = r - rmin;
};
Zu jedem Zeitpunkt ist der aktuelle Radius des ersten der kleineren Bögen k*rmin + (1 - k)*rmax, wobei dieser Faktor k von 1 auf 0 und dann wieder auf 1 geht. Das klingt ähnlich wie die Kosinusfunktion im Intervall [0, 360°]. Bei 0° ist der Wert des Kosinus 1. Dann beginnt er abzunehmen und tut dies weiterhin, bis er 180° erreicht, wo er seinen Minimalwert von -1 erreicht, danach beginnt der Wert der Funktion wieder anzusteigen, bis er 360° erreicht, wo er wieder 1 ist.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Okay, aber die Werte der Kosinusfunktion liegen im Intervall [-1, 1] und wir brauchen eine Funktion, die uns Werte im Intervall [0, 1] liefert. Nun, wenn wir 1 zum Kosinus addieren, verschieben wir den gesamten Graphen nach oben und die Werte liegen nun im Intervall [0, 2].
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
[0, 2] ist nicht [0, 1], also was wir hier noch tun müssen, ist, das Ganze durch 2 zu teilen (oder es mit .5 zu multiplizieren, dasselbe). Dies quetscht unseren Graphen in das gewünschte Intervall.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Gut, aber was ist mit diesem Winkel? Wir haben keinen Winkel, der von 0° bis 360° reicht. Wenn wir requestAnimationFrame verwenden, haben wir nur die Nummer des aktuellen Frames, die bei 0 beginnt und dann weiter ansteigt. Nun, am Anfang legen wir eine Gesamtzahl von Frames T für einen Animationszyklus fest (der erste Bogen geht vom minimalen Radiuswert zum maximalen Radiuswert und dann wieder zurück).
Für jeden Frame berechnen wir das Verhältnis zwischen der Nummer des aktuellen Frames (t) und der Gesamtzahl der Frames. Für einen Zyklus geht dieses Verhältnis von 0 bis 1. Wenn wir dieses Verhältnis mit 2*Math.PI (was dasselbe ist wie 360°) multiplizieren, geht das Ergebnis über den Verlauf eines Zyklus von 0 bis 2*Math.PI. Das wird also unser Winkel sein.
const T = 120;
(function ani(t = 0) {
let k = .5*(1 + Math.cos(t/T*2*Math.PI)),
cr1 = k*rmin + (1 - k)*rmax, cr2 = r - cr1;
})();
Der nächste Schritt ist, den Code, der unser Symbol tatsächlich zeichnet, in diese Funktion einzufügen. Der Code für das Beginnen, Schließen, Füllen von Pfaden, Ändern von Füllungen bleibt derselbe, ebenso wie der Code, der zum Erstellen des großen Bogens benötigt wird. Die Dinge, die sich ändern, sind
- die Radien der kleineren Bögen – sie sind
cr1bzw.cr2 - die
x-Koordinaten der Mittelpunkte für die kleineren Bögen – sie liegen beicr2bzw.-cr1 - die Radien der
schwarzenundweißenKreise – sie sindcr2/3bzw.cr1/3 - die
x-Koordinaten der Mittelpunkte dieser Kreise – sie liegen bei-cr1bzw.cr2
Unsere Animationsfunktion sieht also wie folgt aus
const T = 120;
(function ani(t = 0) {
let k = .5*(1 + Math.cos(t/T*2*Math.PI)),
cr1 = k*rmin + (1 - k)*rmax, cr2 = r - cr1;
CT.beginPath();
CT.arc(0, 0, r, -Math.PI, 0);
CT.arc(cr2, 0, cr1, 0, Math.PI);
CT.arc(-cr1, 0, cr2, 0, -Math.PI, true);
CT.arc(-cr1, 0, cr2/3, 0, 2*Math.PI);
CT.closePath();
CT.fill();
CT.fillStyle = 'white';
CT.beginPath();
CT.arc(cr2, 0, cr1/3, 0, 2*Math.PI);
CT.closePath();
CT.fill();
})();
Dies ergibt den Anfangszustand der Animation
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Bevor wir die Radien der Bögen tatsächlich animieren, müssen wir uns noch um ein paar weitere Dinge kümmern. Erstens, wenn wir die Animation jetzt starten, zeichnen wir nur, wie die Form für jeden Frame aussieht, über das, was wir für die vorherigen Frames gezeichnet haben, was zu einem großen Durcheinander führt. Um dies zu vermeiden, müssen wir den Canvas für jeden Frame löschen, bevor wir etwas darauf zeichnen. Was wir löschen, ist der sichtbare Teil, der sich innerhalb des Rechtecks der Canvas-Abmessungen befindet, dessen obere linke Ecke bei -r,-r liegt.
CT.clearRect(-r, -r, _C.width, _C.width);
Das zweite kleine Problem, das wir beheben müssen, ist, dass wir zu einer weißen Füllung wechseln, aber am Anfang des nächsten Frames brauchen wir eine schwarze. Wir müssen diesen Wechsel also für jeden Frame vor Beginn des ersten Pfades vornehmen.
CT.fillStyle = 'black';
Jetzt können wir tatsächlich mit der Animation beginnen
requestAnimationFrame(ani.bind(this, ++t));
Dies ergibt die Morphing-Animation, aber wir müssen das Ganze noch rotieren. Bevor wir uns dem zuwenden, werfen wir noch einmal einen Blick auf die Formel für k.
let k = .5*(1 + Math.cos(t/T*2*Math.PI))
T und 2*Math.PI sind während der gesamten Animation konstant, also können wir diesen Teil herausnehmen und als konstanten Winkel A speichern.
const T = 120, A = 2*Math.PI/T;
(function ani(t = 0) {
let k = .5*(1 + Math.cos(t*A));
/* same as before */
})();
Nun können wir für jeden Frame auch den Kontext um A drehen, nachdem wir den Canvas gelöscht haben.
CT.rotate(A);
Diese Rotation addiert sich mit jedem Frame und wir haben nun die rotierende und morphing-Animation, die wir uns gewünscht haben.
SVG + JavaScript
Wir beginnen mit einem SVG-Element und ziemlich der gleichen CSS wie im canvas-Fall
$d: 80vmin;
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: lightslategray;
}
svg {
width: $d; height: $d;
border-radius: 50%;
background: white;
}
Dies ergibt eine weiße Scheibe
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Nicht sehr aufregend, also weiter zum Zeichnen von etwas auf dem SVG-Canvas. Genau wie im canvas-Fall zeichnen wir einen Pfad, der aus denselben drei Bögen besteht (der große mit einem Radius, der halb so groß ist wie der des SVG-viewBox, und die beiden kleineren mit einem Radius, der halb so groß ist wie der des großen Bogens im statischen Fall) und zwei kleinen Kreisen (mit einem Radius, der ein Drittel des Radius des kleineren Bogens ist, mit dem sie ihren Mittelpunkt teilen).
Wir beginnen also damit, einen Radius r-Wert auszuwählen und ihn zu verwenden, um den viewBox des svg-Elements festzulegen
- var r = 1500;
svg(viewBox=[-r, -r, 2*r, 2*r].join(' '))
Der nächste Schritt ist das Hinzufügen des path, der aus den drei Bögen besteht. Das Erstellen eines path in SVG unterscheidet sich vom canvas. Hier wird die Form durch das d-Attribut der Pfaddaten beschrieben, das in unserem Fall aus Folgendem besteht:
- ein Befehl „Move to“ (
M), nach dem wir die Koordinaten des Startpunkts unseres Pfads angeben (in diesem Fall auch der Startpunkt des großen Bogens) - ein Befehl „Arc to“ (
A) für jeden unserer Bögen, nach dem wir unsere Bögen beschreiben; jeder dieser Bögen beginnt am Endpunkt des vorherigen Bogens oder im Fall des ersten Bogens am Startpunkt unseres Pfads
Werfen wir einen genaueren Blick auf die Komponenten eines „Arc to“ (A)-Befehls
- der Radius unseres Bogens entlang der
x-Achse seines Koordinatensystems – dieser ist gleichrim Fall des großen Bogens und.5*rim Fall der beiden kleineren - der Radius unseres Bogens entlang der
y-Achse seines Koordinatensystems – dieser ist gleich dem entlang derx-Achse im Fall von Kreisbögen, wie wir sie hier haben (er ist nur bei elliptischen Bögen anders, aber das liegt außerhalb des Rahmens dieses Artikels) - die Rotation des Koordinatensystems unseres Bogens – dies beeinflusst die Form des Bogens nur im Fall von elliptischen Bögen, sodass wir ihn für Kreisbögen immer sicher auf
0setzen können, um die Dinge zu vereinfachen - das Large-Arc-Flag – dies ist
1, wenn unser Bogen größer als ein Halbkreis ist, und andernfalls0; da unsere Bögen genau ein Halbkreis sind, sind sie nicht größer als ein Halbkreis, daher ist dies in unserem Fall immer0 - das Sweep-Flag – dies ist
1, wenn der Bogen im Uhrzeigersinn zwischen seinem Start- und Endpunkt verläuft, und andernfalls0; in unserem Fall verlaufen die ersten beiden Bögen im Uhrzeigersinn, während der dritte dies nicht tut, daher sind die Werte, die wir für die drei Bögen verwenden,1,1und0 - die
x-Koordinate des Endpunkts des Bogens – dies ist etwas, das wir für jeden Bogen bestimmen müssen - die
y-Koordinate des Endpunkts des Bogens – ebenfalls etwas, das wir für jeden Bogen bestimmen müssen
Zu diesem Zeitpunkt wissen wir bereits das meiste, was wir brauchen. Alles, was wir noch herausfinden müssen, sind die Koordinaten der Endpunkte der Bögen. Betrachten wir also die folgende Abbildung
Aus der obigen Abbildung können wir sehen, dass der erste Bogen (der große) bei (-r,0) beginnt und bei (r,0) endet, der zweite bei 0,0 endet und der dritte bei (-r,0) endet (auch der Startpunkt unseres Pfads). Beachten Sie, dass die y-Koordinaten all dieser Punkte 0 bleiben, auch wenn sich die Radien der kleineren Bögen ändern, aber die x-Koordinate des Endpunkts des zweiten Bogens zufällig 0 ist, nur in diesem Fall, wenn die Radien der kleineren Bögen genau halb so groß sind wie die des großen. Im allgemeinen Fall ist es r - 2*r1, wobei r1 der Radius des zweiten Bogens (des ersten der kleineren) ist. Das bedeutet, wir können nun unseren Pfad erstellen
- var r1 = .5*r, r2 = r - r1;
path(d=`M${-r} 0
A${r} ${r} 0 0 1 ${r} 0
A${r1} ${r1} 0 0 1 ${r - 2*r1} 0
A${r2} ${r2} 0 0 0 ${-r} 0`)
Dies ergibt die von uns gewünschte Bogenform
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Nun zu den kleinen Kreisen. Wir kennen bereits die Koordinaten ihrer Mittelpunkte und ihre Radien aus der canvas-Methode.
circle(r=r1/3 cx=r2)
circle(r=r2/3 cx=-r1)
Standardmäßig haben all diese Formen eine schwarze Füllung, also müssen wir explizit eine weiße auf dem Kreis bei (r2,0) setzen.
circle:nth-child(2) { fill: white }
Wir haben nun die statische Form!
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Als Nächstes animieren wir die Form unseres Pfads sowie die Größe und Position unserer beiden kleinen Kreise mit JavaScript. Das bedeutet, dass wir als Erstes diese Elemente abrufen, den Radius R des großen Kreises ermitteln und einen Skalierungsfaktor F festlegen, der uns den minimalen Radius (RMIN) angibt, auf den die Bögen skaliert werden können. Wir legen auch eine Gesamtzahl von Frames (T) und einen Einheitswinkel (A) fest.
const _P = document.querySelector('path'),
_C = document.querySelectorAll('circle'),
_SVG = document.querySelector('svg'),
R = -1*_SVG.getAttribute('viewBox').split(' ')[0],
F = .25, RMIN = F*R, RMAX = R - RMIN,
T = 120, A = 2*Math.PI/T;
Die Animationsfunktion ist so ziemlich dieselbe wie im canvas-Fall. Das Einzige, was anders ist, ist die Tatsache, dass wir nun, um die Pfadform zu ändern, ihr d-Attribut ändern und um die Radien und Positionen der kleinen Kreise zu ändern, ihre r- und cx-Attribute ändern. Aber alles andere funktioniert genau gleich.
(function ani(t = 0) {
let k = .5*(1 + Math.cos(t*A)),
cr1 = k*RMIN + (1 - k)*RMAX, cr2 = R - cr1;
_P.setAttribute('d', `M${-R} 0
A${R} ${R} 0 0 1 ${R} 0
A${cr1} ${cr1} 0 0 1 ${R - 2*cr1} 0
A${cr2} ${cr2} 0 0 0 ${-R} 0`);
_C[0].setAttribute('r', cr1/3);
_C[0].setAttribute('cx', cr2);
_C[1].setAttribute('r', cr2/3);
_C[1].setAttribute('cx', -cr1);
requestAnimationFrame(ani.bind(this, ++t));
})();
Dies ergibt die morphing-Form
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Es gibt nur noch eine Sache zu erledigen, und das ist die Rotation des gesamten Symbols, die wir auf dem _SVG-Element festlegen
let ca = t*A;
_SVG.style.transform = `rotate(${+ca.toFixed(2)}rad)`;
Und wir haben nun das gewünschte Ergebnis auch mit SVG und JavaScript!
SVG + CSS
Es gibt noch eine weitere Methode, dies zu tun, obwohl sie Änderungen wie die Pfaddaten aus CSS beinhaltet, was etwas ist, das derzeit nur Blink-Browser unterstützen (und sie nicht einmal der neuesten Spezifikation entsprechen).
Es ist auch etwas fehleranfällig, da wir denselben Radiuswert sowohl im SVG-viewBox-Attribut als auch als Sass-Variable haben müssen.
- var r = 1500;
svg(viewBox=[-r, -r, 2*r, 2*r].join(' '))
path
circle
circle
$d: 65vmin;
$r: 1500;
$r1: .5*$r;
$r2: $r - $r1;
$rmin: .25*$r;
$rmax: $r - $rmax;
Wir könnten auf den Wert dieses Radius aus CSS zugreifen, aber nur als benutzerdefinierte Eigenschaft, wenn wir etwas wie das hier tun würden
- var r = 1500;
svg(viewBox=[-r, -r, 2*r, 2*r].join(' '))
style :root { --r: #{r} }
Dies mag in einigen Fällen sehr hilfreich sein, ist hier jedoch nutzlos, da wir derzeit keine Möglichkeit haben, CSS-Variablen in die Pfaddaten-Zeichenkette einzufügen, die wir mit Sass erstellen. Wir sind also gezwungen, denselben Wert sowohl im Attribut viewBox als auch im Sass-Code festzulegen.
Die grundlegenden Stile sind dieselben und wir können die Pfaddaten mit Sass auf eine Weise erstellen, die der Pug-Methode ähnelt.
$r: 1500;
$r1: .5*$r;
$r2: $r - $r1;
path {
$data: 'M#{-$r} 0' +
'A#{$r} #{$r} 0 0 1 #{$r} 0' +
'A#{$r1} #{$r1} 0 0 1 #{$r - 2*$r1} 0' +
'A#{$r2} #{$r2} 0 0 0 #{-$r} 0';
d: path($data);
}
Dies ergibt unsere Drei-Bögen-Form.

Für die beiden kleinen Kreise legen wir ihre Radien und Positionen entlang der x-Achse fest. Wir müssen auch sicherstellen, dass einer von ihnen weiß ist.
circle {
r: $r1/3;
cx: $r2;
&:nth-child(2) { fill: white }
&:nth-child(3) {
r: $r2/3;
cx: -$r1
}
}
Jetzt haben wir die statische Form.

Um den gewünschten Effekt zu erzielen, benötigen wir die folgenden Animationen:
- eine morphing
animationderpath-Form, bei der der Radius des ersten der kleineren Bögen vom kleinstmöglichen Radius ($rmin: .25*$r) zum größtmöglichen ($rmax: $r - $rmin) und dann wieder zurück geht, während der Radius des letzten Bogens von$rmaxzu$rminund wieder zurück geht; dies kann mit einer Keyframe-animationvon einem Extrem zum anderen geschehen, und dann mit demalternate-Wert füranimation-direction. - eine weitere alternierende
animation, die den Radius des ersten kleinen Kreises von$rmin/3auf$rmax/3und dann wieder zurück auf$rmin/3skaliert; der zweite kleine Kreis verwendet dieselbeanimation, nur verzögert um den Wert einer normalenanimation-duration. - eine dritte alternierende Animation, die die Mittelpunkte der beiden kleinen Kreise hin und her bewegt; im Fall des ersten (
weißen) kleinen Kreises bewegt er sich von$rmaxnach$rmin; im Fall des zweiten (schwarzen) Kreises geht er von-$rminnach-$rmax; was wir hier tun können, um sie zu vereinheitlichen, ist die Verwendung einer CSS-Variable als Schalter (sie funktioniert nur in WebKit-Browsern, aber das Festlegen der Pfaddaten oder der Kreisradien oder -versätze aus CSS hat auch keine bessere Unterstützung).
Sehen wir uns also zuerst die morphing @keyframes an. Diese werden erstellt, indem praktisch dieselben Pfaddaten wie zuvor gesetzt werden, wobei nur $r1 durch $rmin und $r2 durch $rmax für den 0%-Keyframe und umgekehrt für den 100%-Keyframe ersetzt werden.
@keyframes m {
0% {
$data: 'M#{-$r} 0' +
'A#{$r} #{$r} 0 0 1 #{$r} 0' +
'A#{$rmin} #{$rmin} 0 0 1 #{$r - 2*$rmin} 0' +
'A#{$rmax} #{$rmax} 0 0 0 #{-$r} 0';
d: path($data);
}
100% {
$data: 'M#{-$r} 0' +
'A#{$r} #{$r} 0 0 1 #{$r} 0' +
'A#{$rmax} #{$rmax} 0 0 1 #{$r - 2*$rmax} 0' +
'A#{$rmin} #{$rmin} 0 0 0 #{-$r} 0';
d: path($data);
}
}
Jetzt müssen wir nur noch diese animation auf das path-Element anwenden.
$t: 1s;
path { animation: m $t ease-in-out infinite alternate }
Und der Form-Morphing-Teil funktioniert!

Nächster Schritt ist das Skalieren und Bewegen der beiden kleinen Kreise. Die Skalierungs-@keyframes folgen dem gleichen Muster wie die Morphing-Keyframes. Der Radiuswert beträgt $rmin/3 bei 0% und $rmax/3 bei 100%.
@keyframes s {
0% { r: $rmin/3 }
100% { r: $rmax/3 }
}
Wir wenden diese animation auf die circle-Elemente an.
circle { animation: s $t ease-in-out infinite alternate }
Und jetzt sind die Radien der beiden kleinen Kreise animiert.

Es ist ein Anfang, aber wir haben hier eine Reihe von Problemen. Erstens sollte der zweite kleine circle kleiner werden, wenn der erste größer wird und umgekehrt. Wir beheben dies, indem wir eine animation-delay festlegen, die von einer CSS-Variable abhängt, die wir zunächst auf 0 setzen und dann für den zweiten kleinen Kreis auf 1 umschalten.
circle {
--i: 0;
animation: s $t ease-in-out calc(var(--i)*#{$t}) infinite alternate
&:nth-child(3) { --i: 1 }
}
Wie bereits erwähnt, funktioniert die Verwendung von calc() als animation-delay-Wert nur in WebKit-Browsern, aber das Festlegen von r aus CSS hat eine noch schlechtere Unterstützung, sodass die animation-delay nicht das größte Problem ist, das wir hier haben. Das Ergebnis ist unten zu sehen.

Das ist schon viel besser, aber wir müssen immer noch die Positionen der kleinen Kreise entlang der x-Achse animieren. Dies tun wir mit einer Reihe von @keyframes, die cx von $rmax zu $rmin und wieder zurück für den ersten kleinen circle gehen lassen und von -$rmin zu -$rmax und wieder zurück für den zweiten. In diesen beiden Fällen haben wir sowohl eine andere Reihenfolge als auch ein anderes Vorzeichen, sodass wir eine Keyframe-Animation finden müssen, die beides erfüllt.
Das Problem mit der Reihenfolge zu umgehen ist der einfache Teil – wir verwenden dieselbe animation-delay wie für die Skalierungsradien-animation.
Aber was ist mit dem Vorzeichen? Nun, wir verwenden wieder unsere benutzerdefinierte Eigenschaft --i. Diese ist 0 für den ersten kleinen circle und 1 für den zweiten, also brauchen wir eine Funktion, die --i als Eingabe nimmt und uns 1 liefert, wenn der Wert dieser Variablen 0 ist, und -1 für einen Wert von 1. Die einfachste, die uns einfällt, ist das Potenzieren von -1 mit --i. Leider ist das mit CSS nicht möglich – wir können nur arithmetische Operationen innerhalb von calc() haben. calc(1 - 2*var(--i)) ist jedoch eine weitere Lösung, die funktioniert und nicht viel komplizierter ist. Damit sieht unser Code so aus:
circle {
--i: 0;
--j: calc(1 - 2*var(--i));
animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate;
animation-name: s, x;
&:nth-child(3) { --i: 1 }
}
@keyframes x {
0% { cx: calc(var(--j)*#{$rmax}) }
100% { cx: calc(var(--j)*#{$rmin}) }
}
Das Ergebnis ist unten zu sehen ... und es ist nicht ganz wie erwartet.

Was wir haben, sieht nach einem plötzlichen Flip bei 50% zwischen den beiden Endwerten aus, nicht nach einer sanften animation. Das ist nicht das, was wir wollten, also müssen wir diese Taktik wohl aufgeben.
Wir haben hier jedoch eine weitere Option: die Kombination von cx mit transform. Die beiden kleinen Kreise sind immer so positioniert, dass der Abstand zwischen ihren Mittelpunkten $r beträgt. Wir können also den zweiten der kleinen Kreise auf -$r positionieren und dann beide um eine Distanz verschieben, die zwischen $rmax und $rmin liegt.
circle {
transform: translate($r2*1px);
animation: s $t ease-in-out infinite alternate;
animation-name: s, x;
&:nth-child(3) {
cx: -$r;
animation-delay: -$t, 0s
}
}
@keyframes x {
0% { transform: translate($rmax*1px) }
100% { transform: translate($rmin*1px) }
}
Das verhält sich endlich so, wie wir es wollten!

Eine weitere Sache, die wir hier tun können, um den Code zu vereinfachen, ist, die anfänglichen Werte $r1 und $r2 zu entfernen und sie durch die Werte im 0%-Keyframe jeder Animation zu ersetzen.
path {
d: path('M#{-$r} 0A#{$r} #{$r} 0 0 1 #{$r} 0' +
'A#{$rmin} #{$rmin} 0 0 1 #{$r - 2*$rmin} 0' +
'A#{$rmax} #{$rmax} 0 0 0 #{-$r} 0');
animation: m $t ease-in-out infinite alternate
}
circle {
r: $rmin/3;
transform: translate($rmax*1px);
animation: s $t ease-in-out infinite alternate;
animation-name: s, x;
&:nth-child(3) {
cx: -$r;
animation-delay: -$t, 0s
}
}
@keyframes m {
to {
d: path('M#{-$r} 0A#{$r} #{$r} 0 0 1 #{$r} 0' +
'A#{$rmax} #{$rmax} 0 0 1 #{$r - 2*$rmax} 0' +
'A#{$rmin} #{$rmin} 0 0 0 #{-$r} 0');
}
}
@keyframes s { to { r: $rmax/3 } }
@keyframes x { to { transform: translate($rmin*1px) } }
Das visuelle Ergebnis ist exakt dasselbe, wir haben nur weniger Code.
Der letzte Schritt ist, das SVG-Element selbst unendlich rotieren zu lassen.
svg { animation: r 2*$t linear infinite }
@keyframes r { to { transform: rotate(1turn) } }
Die fertige Ladeanimation ist in diesem Pen zu sehen.
Da haben Sie es – eine Ladeanimation, vier verschiedene Methoden, sie von Grund auf für das Web neu zu erstellen. Nicht alles, was wir hier erforscht haben, ist heute praxistauglich. Zum Beispiel ist die Unterstützung für die letzte Methode sehr schlecht und die Leistung ist furchtbar. Das Erkunden der Grenzen dessen, was heutzutage möglich wird, war jedoch eine unterhaltsame Übung und eine großartige Lernerfahrung.
Es ist großartig. Ich hatte keinen so großen Tutorial (mit vielen Nuancen) zum Zeichnen eines Yin-Yang-Loaders erwartet, als ich den Titel des Artikels las.
Wow, erstaunlicher Beitrag. Viel auf einmal zu verarbeiten, aber die detaillierten, Schritt-für-Schritt-Beispiele halten es verständlich. (OK, ich habe die Hälfte davon schon wieder vergessen, bin mir aber sicher, dass ich zurückscrollen und es noch einmal verfolgen könnte, wenn ich genug motiviert wäre.)
Besonders gut gefallen mir die Strichzeichnungen, die die Dinge verdeutlichen, anstatt einfach nur zu springen.
Jedes Mal, wenn ich dachte, du wärst fertig, hast du einfach mit *einer weiteren* Implementierung weitergemacht. Die Canvas-Implementierung ist mein Favorit; ich kann nie genug von der altmodischen prozeduralen Zeichnung bekommen.
Jetzt frage ich mich, ist es nicht möglich, den SVG + CSS-Ansatz vollständig browserübergreifend (ohne Pfadmanipulation) nur mit Transformationen durchzuführen (Skalierung der äußeren und inneren Kreise als Gruppe)?
Nun, ich schätze, das ist möglich, aber nicht mit einem einzelnen Pfad für den Hauptkreis. Es würde einfach der reinen HTML + CSS-Methode ähneln, nur dass es anstelle von abgerundeten
divscircles wären, die ingroups verschachtelt sind.Alles, was Transformationen auf SVG-Elemente anwendet, ist entweder nicht browserübergreifend oder erfordert JS. ☹
CSS-Transformationen auf SVG-Elemente sind nicht browserübergreifend (Edge unterstützt sie nicht). SMIL ist nicht browserübergreifend (außerdem ist es Markup-Gemüse). Transformationsattribute können auf browserübergreifende Weise geändert werden, aber dies erfordert JS.
Ah, ich wusste nicht, dass CSS-Transformationen für SVG-Elemente nicht so gut sind. Offensichtlich habe ich diesen Artikel verpasst: https://css-tricks.de/transforms-on-svg-elements/
Nicht SVG, aber hier ist eine etwas schlechtere CSS-Implementierung.
Ich finde das sehr clever, aber ich habe ein Problem damit, wie das philosophische Symbol hier verwendet wird (und für einige ist es auch eine religiöse Empfindung). Yin und Yang sollen immer gleichwertig dargestellt werden und zeigen, dass es immer gleich viel Positives und Negatives in allem gibt. Dieses Gleichgewicht ist grundlegend für die Philosophie. Die Dualität des Einen. Dass sie sich gegenseitig zu- und abnehmen, ist wirklich antithetisch zum Taoismus. So ähnlich, als würde man die Arme des christlichen Kreuzes nehmen und sie gleich lang machen und wieder zurück.
Kinnlade fällt XD
Fantastisch, Ana! Ich fand es toll, wie du den Radius synchron und animiert gehalten hast. Eine weitere Implementierung mit einfacher CSS3-Animation.
Unglaubliche Arbeit, Ana. Ich werde etwa 20+ Lesungen brauchen, um das zu verstehen, aber ich genieße die Herausforderung!
Diese Ladeanimation zu beobachten, macht mich wahnsinnig 8-)
Episches Tutorial.
Aber sollte ein Entwickler so viel Zeit verschwenden, um diese Art von Lader zu erstellen? Es gibt immer noch PNG- und GIF-Animationen ;)