Ich spiele seit über fünf Jahren mit CSS-Transformationen herum und eine Sache, die mich immer gestört hat, war, dass ich die Komponenten einer transform-Kette nicht einzeln animieren konnte. Dieser Artikel wird das Problem, den alten Workaround, die neue magische Houdini Lösung erklären und Ihnen schließlich ein Fest für die Augen mit besser aussehenden Beispielen als denen zur Veranschaulichung von Konzepten bieten.
Das Problem
Um das vorliegende Problem besser zu verstehen, betrachten wir das Beispiel einer Box, die wir horizontal über den Bildschirm bewegen. Das bedeutet für das HTML eine div
<div class="box"></div>
Das CSS ist ebenfalls ziemlich einfach. Wir geben dieser Box Abmessungen, einen background und positionieren sie horizontal mit einem margin in der Mitte.
$d: 4em;
.box {
margin: .25*$d auto;
width: $d; height: $d;
background: #f90;
}
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Als Nächstes bewegen wir sie mithilfe einer Translation entlang der x-Achse um die Hälfte der Viewport-Breite (50vw) nach links (in die negative Richtung der x-Achse, die positive Richtung ist nach rechts)
transform: translate(-50vw);
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Nun befindet sich die linke Hälfte der Box außerhalb des Bildschirms. Wenn wir den absoluten Betrag der Translation um die Hälfte ihrer Kantenlänge verringern, ist sie vollständig innerhalb des Viewports, während sie, wenn wir sie um mehr verringern, sagen wir um eine volle Kantenlänge (was $d oder 100% entspricht – denken Sie daran, dass %-Werte in translate()-Funktionen sich auf die Abmessungen des zu übersetzenden Elements beziehen), nicht einmal mehr den linken Rand des Viewports berührt.
transform: translate(calc(-1*(50vw - 100%)));
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Dies wird unsere anfängliche Animationsposition sein.
Dann erstellen wir einen Satz von @keyframes, um die Box in die symmetrische Position relativ zur anfänglichen Position ohne Translation zu bewegen und referenzieren diese bei der Einstellung der animation
$t: 1.5s;
.box {
/* same styles as before */
animation: move $t ease-in-out infinite alternate;
}
@keyframes move {
to { transform: translate(calc(50vw - 100%)); }
}
Dies alles funktioniert wie erwartet und ergibt eine Box, die von links nach rechts und zurück bewegt wird
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Aber das ist eine ziemlich langweilige Animation, also machen wir sie interessanter. Nehmen wir an, wir möchten, dass die Box auf einen Faktor von .1 skaliert wird, wenn sie sich in der Mitte befindet, und ihre normale Größe an den beiden Enden hat. Wir könnten einen weiteren Keyframe hinzufügen
50% { transform: scale(.1); }
Die Box skaliert nun auch (Demo), aber da wir einen zusätzlichen Keyframe hinzugefügt haben, wird die Timing-Funktion nicht mehr für die gesamte Animation angewendet – nur für die Abschnitte zwischen den Keyframes. Dies macht unsere Translation in der Mitte (bei 50%) langsam, da wir dort auch einen Keyframe haben. Wir müssen also die Timing-Funktion sowohl im animation-Wert als auch in den @keyframes anpassen. In unserem Fall, da wir insgesamt eine ease-in-out haben möchten, können wir sie in eine ease-in und eine ease-out aufteilen.
.box {
animation: move $t ease-in infinite alternate;
}
@keyframes move {
50% {
transform: scale(.1);
animation-timing-function: ease-out;
}
to { transform: translate(calc(50vw - 100%)); }
}
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Jetzt funktioniert alles gut, aber was ist, wenn wir unterschiedliche Timing-Funktionen für die Translation und die Skalierung wünschen? Die eingestellten Timing-Funktionen bedeuten, dass die animation am Anfang langsamer, in der Mitte schneller und dann am Ende wieder langsamer ist. Was, wenn wir wollten, dass dies nur für die Translation gilt, aber nicht für die Skalierung? Was, wenn wir wollten, dass die Skalierung am Anfang schnell erfolgt, wenn sie von 1 zu .1 geht, in der Mitte langsam ist, wenn sie um .1 liegt, und dann am Ende wieder schnell ist, wenn sie zurück zu 1 geht?
Nun, es ist einfach nicht möglich, unterschiedliche Timing-Funktionen für verschiedene Transformationsfunktionen in derselben Kette festzulegen. Wir können die Translation am Anfang langsam und die Skalierung schnell machen oder umgekehrt in der Mitte. Zumindest nicht, solange wir die transform-Eigenschaft animieren und sie Teil derselben transform-Kette sind.
Der alte Workaround
Es gibt natürlich Wege, dieses Problem zu umgehen. Traditionell war die Lösung, die transform (und folglich die animation) auf mehrere Elemente aufzuteilen. Dies ergibt die folgende Struktur
<div class="wrap">
<div class="box"></div>
</div>
Wir verschieben die width-Eigenschaft auf den Wrapper. Da div-Elemente standardmäßig Block-Elemente sind, bestimmt dies auch die width ihres .box-Kindelements, ohne dass wir sie explizit festlegen müssen. Wir behalten jedoch die height im .box bei, da die height eines Kindelements (in diesem Fall des .box) auch die height seines Elternelements (in diesem Fall des Wrappers) bestimmt.
Wir verschieben auch die Eigenschaften margin, transform und animation nach oben. Zusätzlich wechseln wir zurück zu einer ease-in-out-Timing-Funktion für diese animation. Wir modifizieren auch den move-Satz von @keyframes zu dem, was er ursprünglich war, damit wir scale() entfernen.
.wrap {
margin: .25*$d calc(50% - #{.5*$d});
width: $d;
transform: translate(calc(-1*(50vw - 100%)));
animation: move $t ease-in-out infinite alternate;
}
@keyframes move {
to { transform: translate(calc(50vw - 100%)); }
}
Wir erstellen einen weiteren Satz von @keyframes, den wir für das eigentliche .box-Element verwenden. Dies ist eine alternierende animation von halber Dauer der einen, die die oszillierende Bewegung erzeugt.
.box {
height: $d;
background: #f90;
animation: size .5*$t ease-out infinite alternate;
}
@keyframes size { to { transform: scale(.1); } }
Wir haben nun das gewünschte Ergebnis
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Dies ist ein solider Workaround, der nicht zu viel zusätzlichen Code hinzufügt, ganz zu schweigen davon, dass wir in diesem speziellen Fall nicht wirklich zwei Elemente benötigen, wir könnten mit nur einem und einem seiner Pseudo-Elemente auskommen. Aber wenn unsere Transformationskette länger wird, bleibt uns nichts anderes übrig, als zusätzliche Elemente hinzuzufügen. Und im Jahr 2018 können wir das besser machen!
Die Houdini-Lösung
Einige von Ihnen wissen vielleicht schon, dass CSS-Variablen nicht animierbar sind (und ich schätze, jeder, der es noch nicht wusste, hat es gerade herausgefunden). Wenn wir versuchen, sie in einer animation zu verwenden, springen sie einfach von einem Wert zum anderen, wenn die Hälfte der Zeit dazwischen verstrichen ist.
Betrachten Sie das anfängliche Beispiel der oszillierenden Box (ohne Skalierung). Nehmen wir an, wir versuchen, sie mit einer benutzerdefinierten Eigenschaft --x zu animieren
.box {
/* same styles as before */
transform: translate(var(--x, calc(-1*(50vw - #{$d}))));
animation: move $t ease-in-out infinite alternate
}
@keyframes move { to { --x: calc(50vw - #{$d}) } }
Leider führt dies nur zu einem Sprung bei 50%, der offizielle Grund dafür ist, dass Browser den Typ der benutzerdefinierten Eigenschaft nicht kennen können (was mir nicht sinnvoll erscheint, aber ich schätze, das spielt keine wirkliche Rolle).
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Aber wir können das alles vergessen, denn jetzt ist Houdini ins Spiel gekommen und wir können solche benutzerdefinierten Eigenschaften registrieren, damit wir ihnen explizit einen Typ (die syntax) geben.
Für weitere Informationen dazu, siehe den Vortrag und die Folien von Serg Hospodarets.
CSS.registerProperty({
name: '--x',
syntax: '<length>',
initialValue: 0,
inherits: false
});
inherits war in den frühen Versionen der Spezifikation optional, wurde dann aber obligatorisch. Wenn Sie also eine ältere Houdini-Demo finden, die nicht mehr funktioniert, liegt es möglicherweise daran, dass inherits nicht explizit gesetzt ist.
Wir haben initialValue auf 0 gesetzt, weil wir ihn auf etwas setzen müssen und dieses Etwas ein berechnungstechnisch unabhängiger Wert sein muss – das heißt, er kann nicht von etwas abhängen, das wir in CSS einstellen oder ändern können, und da die anfänglichen und endgültigen Translationswerte von den Box-Abmessungen abhängen, die wir in CSS festlegen, ist calc(-1*(50vw - 100%)) hier nicht gültig. Es funktioniert nicht einmal, --x auf calc(-1*(50vw - 100%)) zu setzen, wir müssen stattdessen calc(-1*(50vw - #{$d})) verwenden.
$d: 4em;
$t: 1.5s;
.box {
margin: .25*$d auto;
width: $d; height: $d;
--x: calc(-1*(50vw - #{$d}));
transform: translate(var(--x));
background: #f90;
animation: move $t ease-in-out infinite alternate;
}
@keyframes move { to { --x: calc(50vw - #{$d}); } }

Derzeit funktioniert dies nur in Blink-Browsern hinter dem Flag Experimental Web Platform features. Dies kann über chrome://flags (oder, wenn Sie Opera verwenden, opera://flags) aktiviert werden.

In allen anderen Browsern sehen wir immer noch den Sprung bei 50%.
Wenn wir dies auf unsere oszillierende und skalierende Demo anwenden, führen wir zwei benutzerdefinierte Eigenschaften ein, die wir registrieren und animieren – eine ist der Translationsbetrag entlang der x-Achse (--x) und die andere ist der einheitliche Skalierungsfaktor (--f).
CSS.registerProperty({ /* same as before */ });
CSS.registerProperty({
name: '--f',
syntax: '<number>',
initialValue: 1,
inherits: false
});
Das relevante CSS ist wie folgt
.box {
--x: calc(-1*(50vw - #{$d}));
transform: translate(var(--x)) scale(var(--f));
animation: move $t ease-in-out infinite alternate,
size .5*$t ease-out infinite alternate;
}
@keyframes move { to { --x: calc(50vw - #{$d}); } }
@keyframes size { to { --f: .1 } }

Schöner aussehende Dinge
Ein einfaches oszillierendes und skalierendes Quadrat ist jedoch nicht das Aufregendste, also sehen wir uns schönere Demos an!

Die 3D-Version
Von 2D zu 3D wird das Quadrat zu einem Würfel, und da ein einzelner Würfel nicht interessant genug ist, nehmen wir ein ganzes Gitter davon!
Wir betrachten den body als unsere Szene. In dieser Szene haben wir eine 3D-Anordnung von Würfeln (.a3d). Diese Würfel sind auf einem Gitter von nr Zeilen und nc Spalten verteilt.
- var nr = 13, nc = 13;
- var n = nr*nc;
.a3d
while n--
.cube
- var n6hedron= 6; // cube always has 6 faces
while n6hedron--
.cube__face
Das erste, was wir tun, sind einige grundlegende Stile, um eine Szene mit Perspektive zu erstellen, die gesamte Anordnung in die Mitte zu setzen und jede Würfelfläche an ihren Platz zu bringen. Wir werden nicht ins Detail gehen, wie man einen CSS-Würfel baut, da ich diesem Thema bereits einen sehr detaillierten Artikel gewidmet habe. Wenn Sie also eine Wiederholung benötigen, schauen Sie dort nach!
Das bisherige Ergebnis ist unten zu sehen – alle Würfel sind in der Mitte der Szene gestapelt.

Bei all diesen Würfeln befindet sich ihre vordere Hälfte vor der Bildschirmebene und ihre hintere Hälfte hinter der Bildschirmebene. In der Bildschirmebene haben wir einen quadratischen Ausschnitt unseres Würfels. Dieses Quadrat ist identisch mit denen, die die Würfelflächen darstellen.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Als Nächstes setzen wir die Spalten- (--i) und Zeilenindizes (--j) auf Gruppen von Würfeln. Anfangs setzen wir beide Indizes für alle Würfel auf 0.
.cube {
--i: 0;
--j: 0;
}
Da wir eine Anzahl von Würfeln haben, die der Anzahl der Spalten (nc) in jeder Zeile entspricht, setzen wir dann den Zeilenindex für alle Würfel nach den ersten nc auf 1. Dann setzen wir für alle Würfel nach den ersten 2*nc den Zeilenindex auf 2. Und so weiter, bis wir alle nr Zeilen abgedeckt haben.
style
| .cube:nth-child(n + #{1*nc + 1}) { --j: 1 }
| .cube:nth-child(n + #{2*nc + 1}) { --j: 2 }
//- and so on
| .cube:nth-child(n + #{(nr - 1)*nc + 1}) { --j: #{nr - 1} }
Wir können dies in einer Schleife verdichten.
style
- for(var i = 1; i < nr; i++) {
| .cube:nth-child(n + #{i*nc + 1}) { --j: #{i} }
-}
Anschließend fahren wir mit dem Setzen der Spaltenindizes fort. Für die Spalten müssen wir immer eine Anzahl von Würfeln überspringen, die nc - 1 entspricht, bevor wir einen weiteren Würfel mit demselben Index antreffen. Also wird für jeden Würfel der nc-te Würfel danach denselben Index haben und wir werden nc solche Gruppen von Würfeln haben.
(Wir müssen den Index nur für die letzten nc - 1 setzen, da alle Würfel anfänglich den Spaltenindex 0 haben. Wir können also die erste Gruppe überspringen, die die Würfel enthält, für die der Spaltenindex 0 ist – es ist nicht nötig, --i erneut auf denselben Wert zu setzen, den er bereits hat.)
style
| .cube:nth-child(#{nc}n + 2) { --i: 1 }
| .cube:nth-child(#{nc}n + 3) { --i: 2 }
//- and so on
| .cube:nth-child(#{nc}n + #{nc}) { --i: #{nc - 1} }
Dies kann ebenfalls in einer Schleife verdichtet werden.
style
- for(var i = 1; i < nc; i++) {
| .cube:nth-child(#{nc}n + #{i + 1}) { --i: #{i} }
-}
Nachdem wir nun alle Zeilen- und Spaltenindizes gesetzt haben, können wir diese Würfel in einer 2D-Anordnung auf dem Bildschirm mithilfe einer 2D-translate()-Transformation verteilen, gemäß der folgenden Abbildung, bei der jeder Würfel durch seinen quadratischen Ausschnitt in der Bildebene dargestellt wird und die Abstände zwischen den transform-origin-Punkten gemessen werden (die standardmäßig bei 50% 50% 0 liegen, also genau in der Mitte der quadratischen Würfelausschnitte aus der Bildebene sind)
/* $l is the cube edge length */
.cube {
/* same as before */
--x: calc(var(--i)*#{$l});
--y: calc(var(--j)*#{$l});
transform: translate(var(--x), var(--y));
}
Dies ergibt ein Gitter, aber es ist nicht in der Mitte des Bildschirms.

Genau genommen ist jetzt der Mittelpunkt des obersten linken Würfels in der Mitte des Bildschirms, wie in der obigen Demo hervorgehoben. Was wir wollen, ist, dass das Gitter in der Mitte liegt, was bedeutet, dass wir alle Würfel nach links und oben (in die negative Richtung sowohl der x- als auch der y-Achse) um die horizontalen und vertikalen Differenzen zwischen der Hälfte der Gitterabmessungen (calc(.5*var(--nc)*#{$l}) und calc(.5*var(--nr)*#{$l})) und den Abständen zwischen der oberen linken Ecke des Gitters und dem Mittelpunkt des vertikalen Querschnitts des obersten linken Würfels in der Bildebene verschieben müssen (diese Abstände sind jeweils die Hälfte der Würfelkante, oder .5*$l).
Wenn wir diese Differenzen von den vorherigen Beträgen abziehen, wird unser Code
.cube {
/* same as before */
--x: calc(var(--i)*#{$l} - (.5*var(--nc)*#{$l} - .5*#{$l}));
--y: calc(var(--j)*#{$l} - (.5*var(--nr)*#{$l} - .5*#{$l}));
}
Oder noch besser
.cube {
/* same as before */
--x: calc((var(--i) - .5*(var(--nc) - 1))*#{$l}));
--y: calc((var(--j) - .5*(var(--nr) - 1))*#{$l}));
}
Wir müssen auch sicherstellen, dass wir die benutzerdefinierten Eigenschaften --nc und --nr setzen.
- var nr = 13, nc = 13;
- var n = nr*nc;
//- same as before
.a3d(style=`--nc: ${nc}; --nr: ${nr}`)
//- same as before
Dies ergibt ein Gitter, das sich in der Mitte des Viewports befindet.

Wir haben auch die Kantenlänge des Würfels $l verkleinert, damit das Gitter in den Viewport passt.
Alternativ können wir auch eine CSS-Variable --l verwenden, damit wir die Kantenlänge abhängig von der Anzahl der Spalten und Zeilen steuern können. Der erste Schritt hier ist, das Maximum der beiden zu einer --nmax-Variable zu machen.
- var nr = 13, nc = 13;
- var n = nr*nc;
//- same as before
.a3d(style=`--nc: ${nc}; --nr: ${nr}; --max: ${Math.max(nc, nr)}`)
//- same as before
Dann setzen wir die Kantenlänge (--l) auf etwas wie 80% (ein völlig beliebiger Wert) des minimalen Viewport-Dimensionsmaßes über dieses Maximum (--max).
.cube {
/* same as before */
--l: calc(80vmin/var(--max));
}
Schließlich aktualisieren wir die Würfel- und Flächen-transforms, die Flächenabmessungen und den margin, um --l anstelle von $l zu verwenden.
.cube {
/* same as before */
--l: calc(80vmin/var(--max));
--x: calc((var(--i) - .5*(var(--nc) - 1))*var(--l));
--y: calc((var(--j) - .5*(var(--nr) - 1))*var(--l));
&__face {
/* same as before */
margin: calc(-.5*var(--l));
width: var(--l); height: var(--l);
transform: rotate3d(var(--i), var(--j), 0, calc(var(--m, 1)*#{$ba4gon}))
translatez(calc(.5*var(--l)));
}
}
Jetzt haben wir ein schönes, responsives Gitter!

Aber es ist ein hässliches, also machen wir es zu einem schönen Regenbogen, indem wir die color jedes Würfels von seinem Spaltenindex (--i) abhängig machen.
.cube {
/* same as before */
color: hsl(calc(var(--i)*360/var(--nc)), 65%, 65%);
}

Wir haben auch den Hintergrund der Szene dunkel gemacht, damit wir einen besseren Kontrast zu den nun helleren Würfelkanten haben.
Um die Sache noch weiter aufzupeppen, fügen wir eine Zeilendrehung um die y-Achse hinzu, die vom Zeilenindex (--j) abhängt.
.cube {
/* same as before */
transform: rotateY(calc(var(--j)*90deg/var(--nr)))
translate(var(--x), var(--y));
}

Wir haben auch die Würfelkantenlänge --l reduziert und den perspective-Wert erhöht, um dieses verdrehte Gitter passend zu machen.
Jetzt kommt der lustige Teil! Für jeden Würfel animieren wir seine Position hin und her entlang der z-Achse um die halbe Gitterbreite (wir machen die translate() zu einer translate3d() und verwenden eine zusätzliche benutzerdefinierte Eigenschaft --z, die zwischen calc(.5*var(--nc)*var(--l)) und calc(-.5*var(--nc)*var(--l)) liegt) und seine Größe (über eine einheitliche scale3d() mit dem Faktor --f, die zwischen 1 und .1 liegt). Dies ist ziemlich genau dasselbe, was wir für das Quadrat in unserem ursprünglichen Beispiel getan haben, nur dass die Bewegung nun entlang der z-Achse und nicht entlang der x-Achse erfolgt und die Skalierung in 3D statt nur in 2D erfolgt.
$t: 1s;
.cube {
/* same as before */
--z: calc(var(--m)*.5*var(--nc)*var(--l));
transform: rotateY(calc(var(--j)*90deg/var(--nr)))
translate3d(var(--x), var(--y), var(--z))
scale3d(var(--f), var(--f), var(--f));
animation: a $t ease-in-out infinite alternate;
animation-name: move, zoom;
animation-duration: $t, .5*$t;
}
@keyframes move { to { --m: -1 } }
@keyframes zoom { to { --f: .1 } }
Dies tut nichts, bis wir den Multiplikator --m und den Skalierungsfaktor --f registrieren, um ihnen einen Typ und einen Anfangswert zu geben.
CSS.registerProperty({
name: '--m',
syntax: '<number>',
initialValue: 1,
inherits: false
});
CSS.registerProperty({
name: '--f',
syntax: '<number>',
initialValue: 1,
inherits: false
});

Zu diesem Zeitpunkt animieren sich alle Würfel gleichzeitig. Um die Sache interessanter zu machen, fügen wir eine Verzögerung hinzu, die sowohl vom Spalten- als auch vom Zeilenindex abhängt.
animation-delay: calc((var(--i) + var(--j))*#{-2*$t}/(var(--nc) + var(--nr)));

Der letzte Schliff ist die Hinzufügung einer Drehung der 3D-Anordnung.
.a3d {
top: 50%; left: 50%;
animation: ry 8s linear infinite;
}
@keyframes ry { to { transform: rotateY(1turn); } }
Wir machen die Flächen auch opak, indem wir ihnen einen schwarzen Hintergrund geben, und wir haben das Endergebnis.

Die Leistung ist hierfür ziemlich schlecht, wie man aus der obigen GIF-Aufnahme sehen kann, aber es ist trotzdem interessant zu sehen, wie weit wir gehen können.
Hüpfendes Quadrat
Ich bin über das Original in einem Kommentar zu einem anderen Artikel gestolpert und dachte sofort, es sei der perfekte Kandidat für eine Überarbeitung mit etwas Houdini-Magie!
Beginnen wir damit, zu verstehen, was im Originalcode passiert.
Im HTML haben wir neun divs.
<div class="frame">
<div class="center">
<div class="down">
<div class="up">
<div class="squeeze">
<div class="rotate-in">
<div class="rotate-out">
<div class="square"></div>
</div>
</div>
</div>
</div>
</div>
<div class="shadow"></div>
</div>
</div>
Nun, diese Animation ist viel komplexer als alles, was ich mir je hätte ausdenken können, aber selbst neun Elemente scheinen übertrieben zu sein. Schauen wir uns also das CSS an, sehen wir, wofür jedes einzelne verwendet wird, und sehen wir, wie sehr wir den Code vereinfachen können, um uns auf den Wechsel zur Houdini-gestützten Lösung vorzubereiten.
Beginnen wir mit den animierten Elementen. Die Elemente .down und .up haben jeweils eine animation, die sich auf die vertikale Bewegung des Quadrats bezieht.
/* original */
.down {
position: relative;
animation: down $duration ease-in infinite both;
.up {
animation: up $duration ease-in-out infinite both;
/* the rest */
}
}
@keyframes down {
0% {
transform: translateY(-100px);
}
20%, 100% {
transform: translateY(0);
}
}
@keyframes up {
0%, 75% {
transform: translateY(0);
}
100% {
transform: translateY(-100px);
}
}
Mit @keyframes und Animationen bei beiden Elementen mit gleicher Dauer können wir einen Trick mit „eins aus zwei“ vollbringen.
Im Fall des ersten Satzes von @keyframes geschieht die gesamte Aktion (von -100px auf 0) im Intervall [0%, 20%], während im Fall des zweiten Satzes die gesamte Aktion (von 0 auf -100px) im Intervall [75%, 100%] geschieht. Diese beiden Intervalle überschneiden sich nicht. Wegen dieses und weil beide Animationen die gleiche Dauer haben, können wir die Translationswerte an jedem Keyframe addieren.
- bei
0%haben wir-100pxaus dem ersten Satz von@keyframesund0aus dem zweiten, was uns-100pxergibt. - bei
20%haben wir0aus dem ersten Satz von@keyframesund0aus dem zweiten (da wir0für jeden Frame von0%bis75%haben), was uns0ergibt. - bei
75%haben wir0aus dem ersten Satz von@keyframes(da wir0für jeden Frame von20%bis100%haben) und0aus dem zweiten, was uns0ergibt. - bei
100%haben wir0aus dem ersten Satz von@keyframesund-100pxaus dem zweiten, was uns-100pxergibt.
Unser neuer Code sieht wie folgt aus. Wir haben animation-fill-mode aus dem Kurzschreibweise entfernt, da er in diesem Fall nichts bewirkt, da unsere animation unendlich oft läuft, eine nicht-null Dauer hat und keine Verzögerung.
/* new */
.jump {
position: relative;
transform: translateY(-100px);
animation: jump $duration ease-in infinite;
/* the rest */
}
@keyframes jump {
20%, 75% {
transform: translateY(0);
animation-timing-function: ease-in-out;
}
}
Beachten Sie, dass wir unterschiedliche Timing-Funktionen für die beiden Animationen haben, daher müssen wir in den @keyframes zwischen ihnen wechseln. Wir haben immer noch den gleichen Effekt, aber wir haben ein Element und einen Satz von @keyframes entfernt.
Als Nächstes machen wir dasselbe für die Elemente .rotate-in und .rotate-out und ihre @keyframes.
/* original */
.rotate-in {
animation: rotate-in $duration ease-out infinite both;
.rotate-out {
animation: rotate-out $duration ease-in infinite both;
}
}
@keyframes rotate-in {
0% {
transform: rotate(-135deg);
}
20%, 100% {
transform: rotate(0deg);
}
}
@keyframes rotate-out {
0%, 80% {
transform: rotate(0);
}
100% {
transform: rotate(135deg);
}
}
Ähnlich wie im vorherigen Fall addieren wir die Rotationswerte für jeden Keyframe.
- bei
0%haben wir-135degaus dem ersten Satz von@keyframesund0degaus dem zweiten, was uns-135degergibt. - bei
20%haben wir0degaus dem ersten Satz von@keyframesund0degaus dem zweiten (da wir0degfür jeden Frame von0%bis80%haben), was uns0degergibt. - bei
80%haben wir0degaus dem ersten Satz von@keyframes(da wir0degfür jeden Frame von20%bis100%haben) und0degaus dem zweiten, was uns0degergibt. - bei
100%haben wir0degaus dem ersten Satz von@keyframesund135degaus dem zweiten, was uns135degergibt.
Das bedeutet, wir können die Dinge verdichten zu
/* new */
.rotate {
transform: rotate(-135deg);
animation: rotate $duration ease-out infinite;
}
@keyframes rotate {
20%, 80% {
transform: rotate(0deg);
animation-timing-function: ease-in;
}
100% { transform: rotate(135deg); }
}
Wir haben nur ein Element mit einer skalierenden transform, die unser weißes Quadrat verzerrt.
/* original */
.squeeze {
transform-origin: 50% 100%;
animation: squeeze $duration $easing infinite both;
}
@keyframes squeeze {
0%, 4% {
transform: scale(1);
}
45% {
transform: scale(1.8, 0.4);
}
100% {
transform: scale(1);
}
}
Hier können wir nicht wirklich viel tun, was die Code-Kompaktierung angeht, außer animation-fill-mode zu entfernen und den 100%-Keyframe mit den 0%- und 4%-Keyframes zu gruppieren.
/* new */
.squeeze {
transform-origin: 50% 100%;
animation: squeeze $duration $easing infinite;
}
@keyframes squeeze {
0%, 4%, 100% { transform: scale(1); }
45% { transform: scale(1.8, .4); }
}
Das innerste Element (.square) wird nur verwendet, um die weiße Box anzuzeigen, und hat keine transform darauf gesetzt.
/* original */
.square {
width: 100px;
height: 100px;
background: #fff;
}
Das bedeutet, wir können es entfernen, wenn wir seine Stile auf sein Elternelement verschieben.
/* new */
$d: 6.25em;
.rotate {
width: $d; height: $d;
transform: rotate(-135deg);
background: #fff;
animation: rotate $duration ease-out infinite;
}
Wir haben bisher drei Elemente entfernt, und unsere Struktur ist geworden
.frame
.center
.jump
.squeeze
.rotate
.shadow
Das äußerste Element (.frame) dient als Szene oder Container. Dies ist das große blaue Quadrat.
/* original */
.frame {
position: absolute;
top: 50%;
left: 50%;
width: 400px;
height: 400px;
margin-top: -200px;
margin-left: -200px;
border-radius: 2px;
box-shadow: 1px 2px 10px 0px rgba(0,0,0,0.2);
overflow: hidden;
background: #3498db;
color: #fff;
font-family: 'Open Sans', Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Es gibt in dieser Demo keinen Text, daher können wir textbezogene Eigenschaften entfernen. Wir können auch die color-Eigenschaft entfernen, da nicht nur nirgendwo in dieser Demo Text vorhanden ist, sondern wir sie auch nicht für Ränder, Schatten, Hintergründe (über currentColor) usw. verwenden.
Wir können auch vermeiden, dieses enthaltende Element aus dem Dokumentfluss zu nehmen, indem wir ein Flexbox-Layout für den body verwenden. Dies eliminiert auch die Abstände und die margin-Eigenschaften.
/* new */
$s: 4*$d;
body {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
.frame {
overflow: hidden;
position: relative;
width: $s; height: $s;
border-radius: 2px;
box-shadow: 1px 2px 10px rgba(#000, .2);
background: #3498db;
}
Wir haben auch die Abmessungen dieses Elements an die des hüpfenden Quadrats gebunden.
Das Element .center dient nur zur Positionierung seiner direkten Kinder (.jump und .shadow), daher können wir es ganz weglassen und die Abstände direkt auf diese Kinder anwenden.
Wir verwenden absolute Positionierung für alle .frame-Nachfahren. Das macht die Elemente .jump und .squeeze zu 0x0-Boxen, also passen wir die transform-origin für die Quetsch-transform an (100% von 0 ist immer 0, aber der gewünschte Wert ist die halbe Quadratkantenlänge .5*$d). Wir setzen auch einen margin von minus der halben Quadratkantenlänge (-.5*$d) auf das Element .rotate (um das translate(-50%, -50%) zu kompensieren, das wir auf dem entfernten Element .center hatten).
/* new */
.frame * { position: absolute, }
.jump {
top: $top; left: $left;
/* same as before */
}
.squeeze {
transform-origin: 50% .5*$d;
/* same as before */
}
.rotate {
margin: -.5*$d;
/* same as before */
}
Schließlich betrachten wir das Element .shadow.
/* original */
.shadow {
position: absolute;
z-index: -1;
bottom: -2px;
left: -4px;
right: -4px;
height: 2px;
border-radius: 50%;
background: rgba(0,0,0,0.2);
box-shadow: 0 0 0px 8px rgba(0,0,0,0.2);
animation: shadow $duration ease-in-out infinite both;
}
@keyframes shadow {
0%, 100% {
transform: scaleX(.5);
}
45%, 50% {
transform: scaleX(1.8);
}
}
Wir entfernen natürlich die Position, da wir diese bereits für alle Nachfahren von .frame gesetzt haben. Wir können auch z-index entfernen, wenn wir .shadow im DOM vor das Element .jump verschieben.
Als Nächstes haben wir die Abstände. Der Mittelpunkt des Schattens ist horizontal um $left (genau wie das Element .jump) und vertikal um $top plus die halbe Quadratkantenlänge (.5*$d) versetzt.
Wir sehen eine height von 2px. Entlang der anderen Achse berechnet sich die width zur Kantenlänge des Quadrats ($d) plus 4px von links und 4px von rechts. Das sind insgesamt plus 8px. Aber eines fällt uns auf: Der box-shadow mit einer Ausbreitung von 8px und keiner Unschärfe ist nur eine Erweiterung des background. Wir können also einfach die Abmessungen unseres Elements auf beiden Achsen um das Zweifache der Ausbreitung erhöhen und den box-shadow ganz weglassen.
Wie bei den anderen Elementen entfernen wir auch animation-fill-mode aus der Kurzschreibweise animation.
/* new */
.shadow {
margin: .5*($d - $sh-h) (-.5*$sh-w);
width: $sh-w; height: $sh-h;
border-radius: 50%;
transform: scaleX(.5);
background: rgba(#000, .2);
animation: shadow $duration ease-in-out infinite;
}
@keyframes shadow {
45%, 50% { transform: scaleX(1.8); }
}
Wir haben nun den Code in der Originaldemo um etwa **40%** reduziert, während wir immer noch das gleiche Ergebnis erzielen.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Unser nächster Schritt ist es, die Komponenten .jump, .squeeze und rotate zu einer einzigen zusammenzuführen, damit wir von drei Elementen zu einem einzigen gelangen. Nur zur Erinnerung, die relevanten Stile, die wir zu diesem Zeitpunkt haben, sind:
.jump {
transform: translateY(-100px);
animation: jump $duration ease-in infinite;
}
.squeeze {
transform-origin: 50% .5*$d;
animation: squeeze $duration $easing infinite;
}
.rotate {
transform: rotate(-135deg);
animation: rotate $duration ease-out infinite;
}
@keyframes jump {
20%, 75% {
transform: translateY(0);
animation-timing-function: ease-in-out;
}
}
@keyframes squeeze {
0%, 4%, 100% { transform: scale(1); }
45% { transform: scale(1.8, .4); }
}
@keyframes rotate {
20%, 80% {
transform: rotate(0deg);
animation-timing-function: ease-in;
}
100% { transform: rotate(135deg); }
}
Das einzige Problem hierbei ist, dass die skalierende transform eine transform-origin hat, die sich von der Standardeinstellung 50% 50% unterscheidet. Glücklicherweise können wir das umgehen.
Jede transform mit einer transform-origin, die sich von der Standardeinstellung unterscheidet, ist äquivalent zu einer transform-Kette mit Standard-transform-origin, die das Element zuerst so übersetzt, dass sein Standard-transform-origin-Punkt (der 50% 50%-Punkt im Fall von HTML-Elementen und der 0 0-Punkt des viewBox im Fall von SVG-Elementen) zum gewünschten transform-origin wird, die eigentliche Transformation durchführt (Skalierung, Drehung, Scherung, eine Kombination davon… egal) und dann die umgekehrte Übersetzung anwendet (die Werte für jede der Koordinatenachsen werden mit -1 multipliziert).
transform mit einer transform-origin, die sich von der Standardeinstellung unterscheidet, ist äquivalent zu einer Kette, die den Punkt der Standard-transform-origin zum benutzerdefinierten verschiebt, die gewünschte transform ausführt und dann die anfängliche Verschiebung umkehrt (Live-Demo).Dies in Code umgesetzt bedeutet, dass, wenn wir eine transform mit transform-origin: $x1 $y1 haben, die folgenden beiden äquivalent sind:
/* transform on HTML element with transform-origin != default */
transform-origin: $x1 $y1;
transform: var(--transform); /* can be rotation, scaling, shearing */
/* equivalent transform chain on HTML element with default transform-origin */
transform: translate(calc(#{$x1} - 50%), calc(#{$y1} - 50%))
var(--transform)
translate(calc(50% - #{$x1}), calc(50% - $y1);
In unserem speziellen Fall haben wir die Standard-transform-origin-Wert auf der x-Achse, sodass wir nur eine Translation entlang der y-Achse durchführen müssen. Durch den Austausch der hartcodierten Werte durch Variablen erhalten wir die folgende Transformationskette:
transform: translateY(var(--y))
translateY(.5*$d) scale(var(--fx), var(--fy)) translateY(-.5*$d)
rotate(var(--az));
Wir können dies etwas verdichten, indem wir die ersten beiden Translationen zusammenfügen.
transform: translateY(calc(var(--y) + #{.5*$d}))
scale(var(--fx), var(--fy)) translateY(-.5*$d)
rotate(var(--az));
Wir haben auch die drei Animationen auf den drei Elementen in nur eine zusammengefasst.
animation: jump $duration ease-in infinite,
squeeze $duration $easing infinite,
rotate $duration ease-out infinite;
Und wir modifizieren die @keyframes, sodass wir nun die neu eingeführten benutzerdefinierten Eigenschaften --y, --fx, --fy und --az animieren.
@keyframes jump {
20%, 75% {
--y: 0;
animation-timing-function: ease-in-out;
}
}
@keyframes squeeze {
0%, 4%, 100% { --fx: 1; --fy: 1 }
45% { --fx: 1.8; --fy: .4 }
}
@keyframes rotate {
20%, 80% {
--az: 0deg;
animation-timing-function: ease-in;
}
100% { --az: 135deg }
}
Dies funktioniert jedoch nicht, es sei denn, wir registrieren diese CSS-Variablen, die wir eingeführt und animieren möchten.
CSS.registerProperty({
name: '--y',
syntax: '<length>',
initialValue: '-100px',
inherits: false
});
CSS.registerProperty({
name: '--fx',
syntax: '<number>',
initialValue: 1,
inherits: false
});
/* exactly the same for --fy */
CSS.registerProperty({
name: '--az',
syntax: '<angle>',
initialValue: '-135deg',
inherits: false
});
Wir haben jetzt eine funktionierende Demo der Methode zur Animation von CSS-Variablen. Da unsere Struktur nun aus einem Wrapper mit zwei Kindern besteht, können wir sie auf ein Element und zwei Pseudo-Elemente reduzieren und so die endgültige Version erhalten, die unten zu sehen ist. Es ist erwähnenswert, dass dies nur in Blink-Browsern funktioniert, wenn das Flag "Experimental Web Platform features" aktiviert ist.

Ihre Artikel sind so interessant und gründlich, Ana, es ist eine Freude, sie zu lesen.
Wie lange brauchen Sie, um sie zu schreiben?
Das kommt darauf an.
Diesen hier habe ich am 22. Januar begonnen und am 27. Februar fertiggestellt. Wahrscheinlich etwa 100 Stunden Arbeit daran, ein erheblicher Teil davon war nur das Polieren von Dingen. Das Polieren des eigentlichen Textes, das Wiederholen der interaktiven Demo, die die Verkettung erklärt, die einem
transformmittransform-originentspricht, denn manchmal habe ich einfach das Bedürfnis, mit einem frischen Geist und frischem Code wieder an etwas zu arbeiten, damit es nicht von alten Ideen verunreinigt wird.Aber es hängt wirklich vom Artikel ab. Der Rant über Modals war etwas, das ich in nur einer halben Woche geschrieben und poliert habe. Die über Timing-Funktionen oder
background-clipdauerten viel länger als dieser. Im Allgemeinen dauern Dinge mit Illustrationen oder interaktiven Demos länger. Deshalb habe ich mich nach einer schlechten Serie von Artikeln, die ich schließlich nach zwei bis acht Monaten Arbeit daran aufgegeben habe, von Themen zurückgezogen, die diese Art von Dingen erfordern würden.Das ist absolut verblüffend und brillant, Ana.