Was Houdini für die Animation von Transformationen bedeutet

Avatar of Ana Tudor
Ana Tudor am

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

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?

SVG illustration. Shows the timeline, highlighting the 0%, 50% and 100% keyframes. At 0%, we want the translation to start slowly, but the scaling to start fast. At 50%, we want the translation to be at its fastest, while the scaling would be at its slowest. At 100%, the translation ends slowly, while the scaling ends fast.
Die Animationszeitleiste (live).

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}); } }
Animated gif. Shows a square box oscillating horizontally from left to right and back. The motion is slow at the left and right ends and faster in the middle.
Die einfache oszillierende Box, die wir mit der neuen Methode erhalten (Live-Demo, erfordert Houdini-Unterstützung).

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.

Screenshot showing the Experimental Web Platform features flag being enabled in Chrome.
Das aktivierte Flag „Experimental Web Platform features“ in Chrome.

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 } }
Animated gif. Shows the same oscillating box from before now also scaling down to 10% when it's right in the middle. The scaling is fast at the beginning and the end and slow in the middle.
Das oszillierende und skalierende Beispiel mit der neuen Methode (Live-Demo, erfordert Houdini-Unterstützung).

Schöner aussehende Dinge

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

Screenshots of the two demos we dissect here. Left: a rotating wavy rainbow grid of cubes. Right: bouncing square.
Interessantere Beispiele. Links: rotierendes, wellenförmiges Gitter aus Würfeln. Rechts: hüpfendes Quadrat.

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.

Screenshot. Shows all cubes (as wireframes) in the same position in the middle of the scene, making it look as if there's only one wireframe.
Alle Würfel sind in der Mitte gestapelt (Live-Demo).

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)

SVG-Illustration. Zeigt, wie man ein grundlegendes Gitter aus quadratischen, vertikalen Würfelausschnitten mit nc Spalten und nr Zeilen erstellt, beginnend mit der Position des obersten linken Elements. Das oberste linke Element befindet sich in der ersten Spalte (Index <code>0</code>) und in der ersten Zeile (Index <code>0</code>). Alle Elemente in der zweiten Spalte (Index <code>1</code>) sind horizontal um eine Kantenlänge versetzt. Alle Elemente in der dritten Spalte (Index <code>2</code>) sind horizontal um zwei Kantenlängen versetzt. Im Allgemeinen sind alle Elemente in der Spalte mit dem Index <code>i</code> horizontal um <code>i</code> Kantenlängen versetzt. Alle Elemente in der letzten Spalte (Index <code>nc - 1</code>) sind horizontal um <code>nc - 1</code> Kantenlängen versetzt. Alle Elemente in der zweiten Zeile (Index <code>1</code>) sind vertikal um eine Kantenlänge versetzt. Alle Elemente in der dritten Zeile (Index <code>2</code>) sind vertikal um zwei Kantenlängen versetzt. Im Allgemeinen sind alle Elemente in der Zeile mit dem Index <code>j</code> vertikal um <code>j</code> Kantenlängen versetzt. Alle Elemente in der letzten Zeile (Index <code>nr - 1</code>) sind vertikal um <code>nr - 1</code> Kantenlängen versetzt.
Wie man ein grundlegendes Gitter ab der Position des obersten linken Elements erstellt (live).
/* $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.

Screenshot. Shows the grid with nc columns and nr rows, with cubes repersented as wireframes. The midpoint of the top left cube of the rectangular grid is dead in the middle of the screen..
Das Gitter mit dem Mittelpunkt des obersten linken Würfels in der Mitte des Bildschirms (Live-Demo).

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

Die Differenz zwischen der Position des Gittermittelpunkts und des Mittelpunkts des obersten linken Elements (live).

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.

Screenshot. Shows a grid of cube wireframes right in the middle.
Das Gitter befindet sich nun in der Mitte (live).

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!

Animated gif. Shows the previously created grid scaling with the viewport.
Das Gitter befindet sich nun in der Mitte und ist so responsiv, dass es immer in den Viewport passt (live).

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%);
}
Screenshot. The assembly wireframe has now a rainbow look, with every column of cubes having a different hue.
Das Regenbogen-Gitter (Live-Demo).

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));
}
Screenshot. The assembly wireframe now appears twisted, with every row being rotated at a different angle, increasing from top to bottom.
Das verdrehte Gitter (Live-Demo).

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
});
Animated gif. Every cube now moves back and forth along its own z axis (post row rotation), between half a grid width behind its xOy plane and half a grid width in front of its xOy plane. Each cube also scales along all three axes, going from its initial size to a tenth of it along each axis and then back to its initial size.
Das animierte Gitter (Live-Demo, erfordert Houdini-Unterstützung).

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)));
Screenshot
Der wogende Gittereffekt (live).

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.

Animated gif. Now the cube faces are opaque (we've given them a black background) whole assembly rotates around its y axis, making the animation more interesting.
Das Endergebnis (Live-Demo, erfordert Houdini-Unterstützung).

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 -100px aus dem ersten Satz von @keyframes und 0 aus dem zweiten, was uns -100px ergibt.
  • bei 20% haben wir 0 aus dem ersten Satz von @keyframes und 0 aus dem zweiten (da wir 0 für jeden Frame von 0% bis 75% haben), was uns 0 ergibt.
  • bei 75% haben wir 0 aus dem ersten Satz von @keyframes (da wir 0 für jeden Frame von 20% bis 100% haben) und 0 aus dem zweiten, was uns 0 ergibt.
  • bei 100% haben wir 0 aus dem ersten Satz von @keyframes und -100px aus dem zweiten, was uns -100px ergibt.

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 -135deg aus dem ersten Satz von @keyframes und 0deg aus dem zweiten, was uns -135deg ergibt.
  • bei 20% haben wir 0deg aus dem ersten Satz von @keyframes und 0deg aus dem zweiten (da wir 0deg für jeden Frame von 0% bis 80% haben), was uns 0deg ergibt.
  • bei 80% haben wir 0deg aus dem ersten Satz von @keyframes (da wir 0deg für jeden Frame von 20% bis 100% haben) und 0deg aus dem zweiten, was uns 0deg ergibt.
  • bei 100% haben wir 0deg aus dem ersten Satz von @keyframes und 135deg aus dem zweiten, was uns 135deg ergibt.

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

Jede Transformation mit einer 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.

Animated gif. The square rotates in the air, falls down and gets squished against the ground, then bounces back up and the cycle repeats.
Das Endergebnis (live, benötigt Houdini-Unterstützung)