Performante, erweiterbare Animationen: Keyframes on the fly erstellen

Avatar of Bernardo Cardoso
Bernardo Cardoso am

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

Animationen haben sich stark weiterentwickelt und stellen Entwicklern kontinuierlich bessere Werkzeuge zur Verfügung. Insbesondere **CSS-Animationen** haben die Grundlage für die Lösung der meisten Anwendungsfälle geschaffen. Es gibt jedoch einige Animationen, die etwas mehr Aufwand erfordern.

Sie wissen wahrscheinlich, dass Animationen auf der Kompositionsebene laufen sollten. (Ich werde hier nicht näher darauf eingehen, aber wenn Sie mehr erfahren möchten, lesen Sie diesen Artikel.) Das bedeutet, dass Eigenschaften wie transform oder opacity animiert werden, die keine Layout- oder Paint-Ebenen auslösen. Das Animieren von Eigenschaften wie height und width ist ein absolutes No-Go, da sie diese Ebenen auslösen, was den Browser zwingt, Stile neu zu berechnen.

Darüber hinaus sollten Sie, selbst wenn Sie transform-Eigenschaften animieren, um wirklich 60-FPS-Animationen zu erreichen, wahrscheinlich ein wenig Hilfe von JavaScript in Anspruch nehmen und die FLIP-Technik für noch flüssigere Animationen verwenden!

Das Problem bei der Verwendung von transform für erweiterbare Animationen ist jedoch, dass die scale-Funktion nicht ganz dasselbe ist wie das Animieren von width/height-Eigenschaften. Sie erzeugt einen verzerrten Effekt auf dem Inhalt, da alle Elemente gestreckt (beim Hochskalieren) oder gestaucht (beim Herunterskalieren) werden.

Daher war meine bevorzugte Lösung (und wahrscheinlich immer noch ist, aus Gründen, die ich später erläutern werde) **Technik #3** aus Brandon Smiths Artikel. Diese verwendet immer noch eine Transition auf height, aber verwendet JavaScript, um die Inhaltsgröße zu berechnen und eine Transition mithilfe von requestAnimationFrame zu erzwingen. Bei OutSystems haben wir dies tatsächlich verwendet, um die Animation für das OutSystems UI Accordion Pattern zu erstellen.

Keyframes mit JavaScript generieren

Kürzlich bin ich auf einen weiteren großartigen Artikel von Paul Lewis gestoßen, der eine neue Lösung für erweiternde und kollabierende Animationen beschreibt. Das hat mich motiviert, diesen Artikel zu schreiben und diese Technik zu verbreiten.

In seinen Worten besteht die Hauptidee darin, dynamische Keyframes zu generieren, schrittweise...

[…] von 0 auf 100 zu gehen und die benötigten Skalierungswerte für das Element und seine Inhalte zu berechnen. Diese können dann zu einem String zusammengefasst und als Style-Element auf die Seite eingefügt werden.

Um dies zu erreichen, sind drei Hauptschritte erforderlich.

Schritt 1: Start- und Endzustände berechnen

Wir müssen den korrekten Skalierungswert für beide Zustände berechnen. Das bedeutet, wir verwenden getBoundingClientRect() für das Element, das als Stellvertreter für den Startzustand dient, und dividieren es durch den Wert des Endzustands. Es sollte ungefähr so aussehen:

function calculateStartScale () {
  const start= startElement.getBoundingClientRect();
  const end= endElement.getBoundingClientRect();
  return {
    x: start.width / end.width,
    y: start.height / end.height
  };
}

Schritt 2: Keyframes generieren

Jetzt müssen wir eine for-Schleife durchlaufen, wobei die Anzahl der benötigten Frames als Länge verwendet wird. (Sie sollte nicht weniger als 60 betragen, um eine flüssige Animation zu gewährleisten.) In jeder Iteration berechnen wir dann den korrekten Ease-Wert mithilfe einer ease-Funktion.

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

let easedStep = ease(i / frame);

Mit diesem Wert erhalten wir die Skalierung des Elements im aktuellen Schritt, indem wir folgende Mathematik verwenden:

const xScale = x + (1 - x) * easedStep;
const yScale = y + (1 - y) * easedStep;

Und dann fügen wir den Schritt zur Animationszeichenkette hinzu:

animation += `${step}% {
  transform: scale(${xScale}, ${yScale});
}`;

Um zu vermeiden, dass der Inhalt gestreckt/verzerrt wird, sollten wir eine Gegenteil-Animation auf ihn anwenden, indem wir die invertierten Werte verwenden.

const invXScale = 1 / xScale;
const invYScale = 1 / yScale;

inverseAnimation += `${step}% {
  transform: scale(${invXScale}, ${invYScale});
}`;

Schließlich können wir die abgeschlossenen Animationen zurückgeben oder sie direkt in ein neu erstelltes Style-Tag einfügen.

Schritt 3: CSS-Animationen aktivieren

Auf der CSS-Seite müssen wir die Animationen auf den entsprechenden Elementen aktivieren:

.element--expanded {
  animation-name: animation;
  animation-duration: 300ms;
  animation-timing-function: step-end;
}

.element-contents--expanded {
  animation-name: inverseAnimation ;
  animation-duration: 300ms;
  animation-timing-function: step-end;
}

Sie können das Beispiel eines Menüs aus Paul Lewis' Artikel auf Codepen (mit freundlicher Genehmigung von Chris) überprüfen.

Erstellen eines erweiterbaren Abschnitts

Nachdem ich diese grundlegenden Konzepte verstanden hatte, wollte ich prüfen, ob ich diese Technik auf einen anderen Anwendungsfall anwenden kann, wie z. B. einen erweiterbaren Abschnitt.

Wir müssen in diesem Fall nur die Höhe animieren, insbesondere die Funktion zur Berechnung von Skalierungen. Wir nehmen den Y-Wert vom Abschnittstitel, um den zusammengeklappten Zustand darzustellen, und den gesamten Abschnitt, um den erweiterten Zustand darzustellen.

    _calculateScales () {
      var collapsed = this._sectionItemTitle.getBoundingClientRect();
      var expanded = this._section.getBoundingClientRect();
      
      // create css variable with collapsed height, to apply on the wrapper
      this._sectionWrapper.style.setProperty('--title-height', collapsed.height + 'px');

      this._collapsed = {
        y: collapsed.height / expanded.height
      }
    }

Da wir möchten, dass der erweiterte Abschnitt absolute positioniert ist (um zu vermeiden, dass er im zusammengeklappten Zustand Platz einnimmt), setzen wir die CSS-Variable dafür auf die zusammengeklappte Höhe, angewendet auf den Wrapper. Dies wird das einzige Element mit relativer Positionierung sein.

Als Nächstes kommt die Funktion zum Erstellen der Keyframes: _createEaseAnimations(). Dies unterscheidet sich nicht wesentlich von dem, was oben erklärt wurde. Für diesen Anwendungsfall müssen wir tatsächlich vier Animationen erstellen:

  1. Die Animation zum Erweitern des Wrappers
  2. Die Gegenteil-Animations-Erweiterung des Inhalts
  3. Die Animation zum Zusammenklappen des Wrappers
  4. Die Gegenteil-Animations-Zusammenklappen des Inhalts

Wir folgen dem gleichen Ansatz wie zuvor, führen eine for-Schleife mit einer Länge von 60 (für eine flüssige 60-FPS-Animation) aus und erstellen einen Keyframe-Prozentsatz basierend auf dem Ease-Schritt. Dann fügen wir ihn zu den endgültigen Animationszeichenketten hinzu.

outerAnimation.push(`
  ${percentage}% {
    transform: scaleY(${yScale});
  }`);
  
innerAnimation.push(`
  ${percentage}% {
    transform: scaleY(${invScaleY});
  }`);

Wir beginnen damit, ein Style-Tag zu erstellen, das die fertigen Animationen aufnehmen soll. Da dies als Konstruktor konzipiert ist, um verschiedene Muster einfach hinzufügen zu können, möchten wir alle diese generierten Animationen im selben Stylesheet haben. Daher prüfen wir zuerst, ob das Element existiert. Wenn nicht, erstellen wir es und fügen einen aussagekräftigen Klassennamen hinzu. Andernfalls hätten Sie am Ende ein Stylesheet für jede erweiterbare Sektion, was nicht ideal ist.

 var sectionEase = document.querySelector('.section-animations');
 if (!sectionEase) {
  sectionEase = document.createElement('style');
  sectionEase.classList.add('section-animations');
 }

Davon abgesehen fragen Sie sich vielleicht schon: „Hmm, wenn wir mehrere erweiterbare Abschnitte haben, verwenden sie dann nicht trotzdem Animationen mit demselben Namen, aber möglicherweise mit falschen Werten für ihren Inhalt?“

Das ist absolut richtig! Um das zu verhindern, generieren wir auch dynamische Animationsnamen. Cool, oder?

Wir nutzen den Index, der dem Konstruktor aus der for-Schleife bei der Ausführung von querySelectorAll('.section') übergeben wird, um ein eindeutiges Element zum Namen hinzuzufügen.

var sectionExpandAnimationName = "sectionExpandAnimation" + index;
var sectionExpandContentsAnimationName = "sectionExpandContentsAnimation" + index;

Dann verwenden wir diesen Namen, um eine CSS-Variable auf dem aktuellen erweiterbaren Abschnitt zu setzen. Da diese Variable nur in diesem Geltungsbereich existiert, müssen wir nur die Animation auf die neue Variable in CSS setzen, und jedes Muster erhält seinen jeweiligen animation-name-Wert.

.section.is--expanded {
  animation-name: var(--sectionExpandAnimation);
}

.is--expanded .section-item {
  animation-name: var(--sectionExpandContentsAnimation);
}

.section.is--collapsed {
  animation-name: var(--sectionCollapseAnimation);
}

.is--collapsed .section-item {
  animation-name: var(--sectionCollapseContentsAnimation);
}

Der Rest des Skripts betrifft das Hinzufügen von Event-Listenern, Funktionen zum Umschalten des Zusammenklapp-/Erweitern-Status und einige Verbesserungen der Barrierefreiheit.

Zu HTML und CSS: Es bedarf etwas zusätzlicher Arbeit, damit die erweiterbare Funktionalität funktioniert. Wir benötigen einen zusätzlichen Wrapper, der das relative Element ist, das sich nicht animiert. Die erweiterbaren Kinder haben eine absolute-Positionierung, damit sie im zusammengeklappten Zustand keinen Platz einnehmen.

Denken Sie daran, da wir Gegenteil-Animationen durchführen müssen, skalieren wir es auf die volle Größe, um einen Verzerrungseffekt auf dem Inhalt zu vermeiden.

.section-item-wrapper {
  min-height: var(--title-height);
  position: relative;
}

.section {
  animation-duration: 300ms;
  animation-timing-function: step-end;
  contain: content;
  left: 0;
  position: absolute;
  top: 0;
  transform-origin: top left;
  will-change: transform;
}

.section-item {
  animation-duration: 300ms;
  animation-timing-function: step-end;
  contain: content;
  transform-origin: top left;
  will-change: transform;  
}

Ich möchte die Bedeutung der Eigenschaft animation-timing-function hervorheben. Sie sollte auf linear oder step-end gesetzt werden, um ein Easeing zwischen den einzelnen Keyframes zu vermeiden.

Die Eigenschaft will-change – wie Sie wahrscheinlich wissen – aktiviert die GPU-Beschleunigung für die Transform-Animation für ein noch flüssigeres Erlebnis. Und die Verwendung der contain-Eigenschaft mit dem Wert contents hilft dem Browser, das Element unabhängig vom Rest des DOM-Baums zu behandeln, und begrenzt den Bereich, bevor er Layout-, Stil-, Paint- und Größeneigenschaften neu berechnet.

Wir verwenden visibility und opacity, um den Inhalt auszublenden und Screenreader daran zu hindern, darauf zuzugreifen, wenn er zusammengeklappt ist.

.section-item-content {
  opacity: 1;
  transition: opacity 500ms ease;
}

.is--collapsed .section-item-content {
  opacity: 0;
  visibility: hidden;
}

Und schließlich haben wir unseren erweiterbaren Abschnitt! Hier ist der vollständige Code und die Demo, damit Sie sie sich ansehen können.

Performance-Prüfung

Immer wenn wir mit Animationen arbeiten, muss die Performance im Hinterkopf behalten werden. Nutzen wir also die Entwicklertools, um zu prüfen, ob all diese Arbeit performance-technisch wert war. Mithilfe der Performance-Registerkarte (ich verwende Chrome DevTools) können wir die FPS und die CPU-Auslastung während der Animationen analysieren.

Und die Ergebnisse sind großartig!

Je höher die grüne Leiste, desto höher die Frames. Und es gibt keinen Junk, der durch rote Bereiche signalisiert würde.

Wenn wir das FPS-Meter-Tool verwenden, um die Werte detaillierter zu überprüfen, können wir sehen, dass es selbst bei missbräuchlicher Nutzung ständig die 60-FPS-Marke erreicht.

Abschließende Überlegungen

Also, was ist das Urteil? Ersetzt das alle anderen Methoden? Ist das die „Heiliger Gral“-Lösung?

Meiner Meinung nach nein.

Aber… das ist wirklich in Ordnung! Es ist eine weitere Lösung in der Liste. Und wie bei jeder anderen Methode sollte analysiert werden, ob sie für den jeweiligen Anwendungsfall der beste Ansatz ist.

Diese Technik hat definitiv ihre Verdienste. Wie Paul Lewis sagt, erfordert sie viel Vorarbeit. Aber auf der anderen Seite müssen wir sie nur einmal beim Laden der Seite ausführen. Während der Interaktionen schalten wir lediglich Klassen um (und in einigen Fällen Attribute für Barrierefreiheit).

Dies bringt jedoch einige Einschränkungen für die Benutzeroberfläche der Elemente mit sich. Wie Sie für das erweiterbare Abschnittselement sehen konnten, macht die Gegenteil-Skalierung es viel zuverlässiger für absolute und Off-Canvas-Elemente wie Floating Actions oder Menüs. Es ist auch schwierig, den Rand zu stylen, da overflow: hidden verwendet wird.

Nichtsdestotrotz denke ich, dass dieser Ansatz viel Potenzial hat. Lassen Sie mich wissen, was Sie denken!