Hacking CSS Animation State and Playback Time

Avatar of Lu Wang
Lu Wang on

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

CSS-only Wolfenstein ist ein kleines Projekt, das ich vor ein paar Wochen erstellt habe. Es war ein Experiment mit CSS-3D-Transformationen und Animationen.

Inspiriert von der FPS-Demo und einem weiteren Wolfenstein CodePen, beschloss ich, meine eigene Version zu entwickeln. Sie basiert lose auf Episode 1 – Floor 9 des originalen Wolfenstein 3D-Spiels.

Herausgeber: Dieses Spiel erfordert absichtlich schnelle Reaktionen, um einen Game Over-Bildschirm zu vermeiden.

Hier ist ein Gameplay-Video

Kurz gesagt, mein Projekt ist nichts weiter als eine sorgfältig geskriptete, lange CSS-Animation. Plus ein paar Instanzen des Checkbox-Hacks.

:checked ~ div { animation-name: spin; }

Die Umgebung besteht aus 3D-Gitterflächen und die Animationen sind meist einfache 3D-Translationen und -Rotationen. Nichts wirklich Besonderes.

Zwei Probleme waren jedoch besonders schwierig zu lösen

  • Spiele die „Waffenfeuer“-Animation ab, wann immer der Spieler auf einen Feind klickt.
  • Wenn der sich schnell bewegende Boss den letzten Treffer landete, trete in eine dramatische Zeitlupe ein.

Auf technischer Ebene bedeutete dies

  • Spiele eine Animation erneut ab, wenn die nächste Checkbox aktiviert wird.
  • Verlangsame eine Animation, wenn eine Checkbox aktiviert wird.

Tatsächlich war keines davon in meinem Projekt richtig gelöst! Ich habe entweder Workarounds verwendet oder einfach aufgegeben.

Andererseits habe ich nach einigem Recherchieren schließlich den Schlüssel zu beiden Problemen gefunden: das Ändern der Eigenschaften von laufenden CSS-Animationen. In diesem Artikel werden wir dieses Thema weiter untersuchen

  • Viele interaktive Beispiele.
  • Zerlegungen: Wie funktioniert jedes Beispiel (oder auch nicht)?
  • Hinter den Kulissen: Wie verarbeiten Browser Animationszustände?

Ich werde mal "meine Ziegel werfen".

Problem 1: Erneutes Abspielen einer Animation

Das erste Beispiel: „Nur eine weitere Checkbox“

Meine erste Intuition war „fügen Sie einfach eine weitere Checkbox hinzu“, was nicht funktioniert

Jede Checkbox funktioniert einzeln, aber nicht beide zusammen. Wenn eine Checkbox bereits aktiviert ist, funktioniert die andere nicht mehr.

So funktioniert es (oder „funktioniert nicht“)

  1. Der animation-name von <div> ist standardmäßig none.
  2. Der Benutzer klickt auf eine Checkbox, animation-name wird zu spin und die Animation beginnt von vorne.
  3. Nach einer Weile klickt der Benutzer auf die andere Checkbox. Eine neue CSS-Regel tritt in Kraft, aber animation-name ist *immer noch* spin, was bedeutet, dass keine Animation hinzugefügt oder entfernt wird. Die Animation spielt einfach weiter, als wäre nichts geschehen.

Das zweite Beispiel: „Klonen der Animation“

Ein funktionierender Ansatz ist, die Animation zu klonen

#spin1:checked ~ div { animation-name: spin1; }
#spin2:checked ~ div { animation-name: spin2; }

So funktioniert es

  1. animation-name ist zunächst none.
  2. Der Benutzer klickt auf „Spin!“, animation-name wird zu spin1. Die Animation spin1 startet von vorne, da sie gerade hinzugefügt wurde.
  3. Der Benutzer klickt auf „Spin again!“, animation-name wird zu spin2. Die Animation spin2 startet von vorne, da sie gerade hinzugefügt wurde.

Beachten Sie, dass in Schritt Nr. 3 spin1 aufgrund der Reihenfolge der CSS-Regeln entfernt wird. Es funktioniert nicht, wenn „Spin again!“ zuerst aktiviert wird.

Das dritte Beispiel: „Anhängen derselben Animation“

Ein weiterer funktionierender Ansatz ist das „Anhängen derselben Animation“

#spin1:checked ~ div { animation-name: spin; }
#spin2:checked ~ div { animation-name: spin, spin; }

Dies ist ähnlich wie im vorherigen Beispiel. Sie können das Verhalten tatsächlich auf diese Weise verstehen

#spin1:checked ~ div { animation-name: spin1; }
#spin2:checked ~ div { animation-name: spin2, spin1; }

Beachten Sie, dass beim Anklicken von „Spin again!“ die alte laufende Animation zur zweiten Animation in der neuen Liste wird, was unintuitiv sein kann. Eine direkte Folge davon ist: Der Trick funktioniert nicht, wenn animation-fill-mode auf forwards gesetzt ist. Hier ist eine Demo

Wenn Sie sich fragen, warum das so ist, hier sind einige Hinweise

  • animation-fill-mode ist standardmäßig none, was bedeutet „Die Animation hat keinerlei Auswirkung, wenn sie nicht abgespielt wird“.
  • animation-fill-mode: forwards; bedeutet „Nachdem die Animation abgespielt wurde, muss sie für immer im letzten Keyframe verharren“.
  • spin1s Entscheidung überschreibt immer spin2s, da spin1 später in der Liste erscheint.
  • Angenommen, der Benutzer klickt auf „Spin!“, wartet auf eine volle Drehung und klickt dann auf „Spin again!“. Zu diesem Zeitpunkt ist spin1 bereits beendet und spin2 beginnt gerade.

Diskussion

Faustregel: Sie können eine bestehende CSS-Animation nicht „neu starten“. Stattdessen möchten Sie eine neue Animation hinzufügen und abspielen. Dies wird möglicherweise durch die W3C-Spezifikation bestätigt

Sobald eine Animation gestartet ist, läuft sie weiter, bis sie endet oder animation-name entfernt wird.

Wenn wir die letzten beiden Beispiele vergleichen, glaube ich, dass in der Praxis das „Klonen von Animationen“ oft besser funktionieren sollte, insbesondere wenn ein CSS-Präzessor verfügbar ist.

Problem 2: Zeitlupe

Man könnte denken, dass das Verlangsamen einer Animation nur eine Frage der Einstellung einer längeren animation-duration ist

div { animation-duration: 0.5s; }
#slowmo:checked ~ div { animation-duration: 1.5s; }

Tatsächlich funktioniert das

... oder etwa nicht?

Mit ein paar Anpassungen sollte es einfacher sein, das Problem zu erkennen.

Ja, die Animation wird verlangsamt. Und nein, es sieht nicht gut aus. Der Hund „springt“ (fast) immer, wenn Sie die Checkbox umschalten. Außerdem scheint der Hund zu einer zufälligen Position statt zur Anfangsposition zu springen. Wie kommt das?

Es wäre einfacher zu verstehen, wenn wir zwei „Schattenelemente“ einführen würden

Beide Schattenelemente führen dieselben Animationen mit unterschiedlicher animation-duration aus. Und sie werden nicht von der Checkbox beeinflusst.

Wenn Sie die Checkbox umschalten, wechselt das Element einfach sofort zwischen den Zuständen der beiden Schattenelemente.

Zitat der W3C-Spezifikation

Änderungen der Werte von Animationseigenschaften, während die Animation läuft, gelten so, als hätte die Animation diese Werte seit Beginn gehabt.

Dies folgt dem zustandslosen Design, das es Browsern ermöglicht, den animierten Wert leicht zu ermitteln. Die tatsächliche Berechnung wird hier und hier beschrieben.

Ein weiterer Versuch

Eine Idee ist, die aktuelle Animation zu pausieren und dann eine langsamere Animation hinzuzufügen, die von dort übernimmt

div {
  animation-name: spin1;
  animation-duration: 2s;
}

#slowmo:checked ~ div {
  animation-name: spin1, spin2;
  animation-duration: 2s, 5s;
  animation-play-state: paused, running;
}

Es funktioniert also

... oder etwa nicht?

Es verlangsamt sich, wenn Sie auf „Slowmo!“ klicken. Wenn Sie jedoch eine volle Umdrehung warten, sehen Sie einen „Sprung“. Tatsächlich springt es immer an die Position, an der „Slowmo!“ angeklickt wurde.

Der Grund ist, dass wir keinen from-Keyframe definiert haben – und das sollten wir auch nicht. Wenn der Benutzer auf „Slowmo!“ klickt, wird spin1 an einer bestimmten Position pausiert und spin2 startet an genau derselben Position. Wir können diese Position nicht vorhersehen… oder doch?

Eine funktionierende Lösung

Das können wir! Durch die Verwendung einer benutzerdefinierten Eigenschaft können wir den Winkel in der ersten Animation erfassen und ihn dann an die zweite Animation übergeben

div {
  transform: rotate(var(--angle1));
  animation-name: spin1;
  animation-duration: 2s;
}

#slowmo:checked ~ div {
  transform: rotate(var(--angle2));
  animation-name: spin1, spin2;
  animation-duration: 2s, 5s;
  animation-play-state: paused, running;
}

@keyframes spin1 {
  to {
    --angle1: 360deg;
  }
}

@keyframes spin2 {
  from {
    --angle2: var(--angle1);
  }
  to {
    --angle2: calc(var(--angle1) + 360deg);
  }
}

Hinweis: @property wird in diesem Beispiel verwendet, was nicht von allen Browsern unterstützt wird.

Die „perfekte“ Lösung

Die vorherige Lösung hat einen Nachteil: „Zeitlupe verlassen“ funktioniert nicht gut.

Hier ist eine bessere Lösung

In dieser Version kann die Zeitlupe nahtlos ein- und ausgeschaltet werden. Es werden auch keine experimentellen Features verwendet. Ist es also die perfekte Lösung? Ja und nein.

Diese Lösung funktioniert wie das „Schalten“ von „Gängen“

  • Gänge: Es gibt zwei <div>s. Einer ist das Elternelement des anderen. Beide haben die spin-Animation, aber mit unterschiedlicher animation-duration. Der Endzustand des Elements ist die Akkumulation beider Animationen.
  • Schalten: Am Anfang läuft nur die Animation eines <div>s. Das andere ist pausiert. Wenn die Checkbox umgeschaltet wird, tauschen beide Animationen ihre Zustände.

Obwohl mir das Ergebnis sehr gut gefällt, gibt es ein Problem: Es ist eine nette Ausnutzung der spin-Animation, die im Allgemeinen nicht für andere Animationstypen funktioniert.

Eine praktische Lösung (mit JS)

Für allgemeine Animationen ist es möglich, die Zeitlupenfunktion mit ein wenig JavaScript zu erreichen

Eine kurze Erklärung

  • Eine benutzerdefinierte Eigenschaft wird verwendet, um den Animationsfortschritt zu verfolgen.
  • Die Animation wird neu gestartet, wenn die Checkbox umgeschaltet wird.
  • Der JS-Code berechnet die korrekte animation-delay, um einen nahtlosen Übergang zu gewährleisten. Ich empfehle diesen Artikel, wenn Sie mit negativen Werten von animation-delay nicht vertraut sind.

Sie können diese Lösung als Hybrid aus „Neustart der Animation“ und dem „Gangschaltungs“-Ansatz betrachten.

Hier ist es wichtig, den Animationsfortschritt korrekt zu verfolgen. Workarounds sind möglich, wenn @property nicht verfügbar ist. Als Beispiel verwendet diese Version z-index, um den Fortschritt zu verfolgen

Nebenbemerkung: Ursprünglich habe ich auch versucht, eine reine CSS-Version zu erstellen, war aber nicht erfolgreich. Obwohl nicht 100% sicher, glaube ich, dass dies daran liegt, dass animation-delay nicht animierbar ist.

Hier ist eine Version mit minimalem JavaScript. Nur das „Betreten der Zeitlupe“ funktioniert.

Bitte lassen Sie mich wissen, wenn Sie eine funktionierende reine CSS-Version erstellen können!

Zeitlupe für jede Animation (mit JS)

Zuletzt möchte ich eine Lösung teilen, die für (fast) jede Animation funktioniert, auch mit mehreren komplizierten @keyframes

Grundsätzlich müssen Sie einen Tracker für den Animationsfortschritt hinzufügen und dann sorgfältig animation-delay für die neue Animation berechnen. Manchmal kann es jedoch schwierig (aber möglich) sein, die richtigen Werte zu erhalten.

Zum Beispiel:

  • animation-timing-function ist nicht linear.
  • animation-direction ist nicht normal.
  • mehrere Werte in animation-name mit unterschiedlichen animation-duration und animation-delay.

Diese Methode wird auch hier für die Web Animations API beschrieben.

Danksagungen

Ich begann diesen Weg, nachdem ich auf reine CSS-Projekte gestoßen war. Einige waren filigrane Kunstwerke und einige waren komplexe Konstruktionen. Meine Favoriten sind diejenigen, die 3D-Objekte beinhalten, zum Beispiel dieser hüpfende Ball und dieser verpackte Würfel.

Am Anfang hatte ich keine Ahnung, wie diese gemacht wurden. Später las ich und lernte viel aus diesem schönen Tutorial von Ana Tudor.

Wie sich herausstellte, ist das Erstellen und Animieren von 3D-Objekten mit CSS nicht viel anders als mit Blender, nur mit einem etwas anderen Geschmack.

Fazit

In diesem Artikel haben wir das Verhalten von CSS-Animationen untersucht, wenn eine animate-*-Eigenschaft geändert wird. Insbesondere haben wir Lösungen für „erneutes Abspielen einer Animation“ und „Animations-Zeitlupe“ erarbeitet.

Ich hoffe, Sie finden diesen Artikel interessant. Bitte lassen Sie mich Ihre Gedanken wissen!