Leistungsstarke Animationen zum Maximieren und Minimieren erstellen

Paul Lewis
McGruer
Stephen McGruer

Kurzfassung

Verwenden Sie Skalierungstransformationen bei der Animation von Clips. Sie können verhindern, dass die untergeordneten Elemente während der Animation gestreckt und verzerrt werden, indem Sie sie gegenskalieren.

Wir haben bereits Updates zum Erstellen leistungsstarker Parallax-Effekte und Unendliches Scrollen veröffentlicht. In diesem Post sehen wir uns an, worauf es bei der Erstellung effektiver Clip-Animationen ankommt. Eine Demo finden Sie im GitHub-Repository mit Beispiel-UI-Elementen.

Nehmen wir z. B. ein erweiterbares Menü:

Einige Optionen für die Erstellung sind leistungsstärker als andere.

Schlecht: Breite und Höhe eines Containerelements werden animiert.

Stellen Sie sich vor, Sie verwenden CSS-Code, um Breite und Höhe des Containerelements zu animieren.

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

Das unmittelbare Problem bei diesem Ansatz besteht darin, dass width und height animiert werden müssen. Diese Eigenschaften erfordern eine Berechnung des Layouts und die Darstellung der Ergebnisse auf jedem Frame der Animation, was sehr teuer sein kann und dazu führt, dass Ihnen in der Regel 60 fps entgehen. Wenn das für Sie interessant ist, lesen Sie unsere Leitfäden zur Renderingleistung. Dort finden Sie weitere Informationen zur Funktionsweise des Renderingprozesses.

Schlecht: Verwenden Sie die CSS-Eigenschaften "clip" oder "clip-path".

Alternativ zur Animation von width und height kann auch das (inzwischen eingestellte) clip-Attribut verwendet werden, um den Maximierungs- und Minimierungseffekt zu animieren. Wenn Sie möchten, können Sie auch clip-path verwenden. Die Verwendung von clip-path wird jedoch weniger gut unterstützt als clip. Aber clip wurde eingestellt. Nach rechts. Aber keine Sorge, das ist nicht die Lösung, die Sie sich gewünscht haben!

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

Diese Methode ist zwar besser als das Animieren von width und height des Menüelements, den Nachteil dieser Methode ist jedoch, dass sie trotzdem Paint auslöst. Außerdem erfordert das Attribut clip, falls Sie diesen Weg gehen, dafür, dass das Element, auf dem es ausgeführt wird, entweder absolut oder fest positioniert ist, was ein zusätzliches Wrangling erfordern kann.

Gut: Waagen animieren

Da bei diesem Effekt etwas größer und kleiner wird, können Sie eine Skalierungstransformation verwenden. Das sind gute Neuigkeiten, da für das Ändern von Transformationen kein Layout oder Paint erforderlich ist und der Browser an die GPU übergeben kann. Das bedeutet, dass der Effekt beschleunigt wird und deutlich mehr 60 fps erreicht.

Der Nachteil dieses Ansatzes besteht, wie bei den meisten Aspekten der Rendering-Leistung, darin, dass er einige Einrichtungsschritte erfordert. Es lohnt sich jedoch auf jeden Fall!

Schritt 1: Start- und Endzustände berechnen

Bei einem Ansatz, der Animationen mit Skalierung verwendet, besteht der erste Schritt darin, Elemente zu lesen, die Ihnen mitteilen, welche Größe das Menü sowohl beim Minimieren als auch beim Maximieren haben muss. Es kann sein, dass Sie in manchen Situationen nicht beide Informationen auf einmal abrufen können und Sie beispielsweise einige Klassen umschalten müssen, um die verschiedenen Zustände der Komponente zu lesen. Wenn Sie dies jedoch tun müssen, seien Sie vorsichtig: getBoundingClientRect() (oder offsetWidth und offsetHeight) zwingt den Browser, Stile und Layoutübergänge auszuführen, wenn Stile sich seit der letzten Ausführung geändert haben.

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

Bei so etwas wie einem Menü können wir davon ausgehen, dass es anfangs in der natürlichen Größe (1, 1) vorhanden ist. Diese natürliche Skala stellt den maximierten Zustand dar. Das heißt, Sie müssen von einer reduzierten Version (die oben berechnet wurde) wieder zu diesem natürlichen Maßstab hochfahren.

Aber, oh nein! Sicher würde dies auch den Inhalt der Speisekarte skalieren, oder? Nun, wie Sie unten sehen können, ja.

Was können Sie dagegen tun? Nun können Sie eine counter--Transformation auf den Inhalt anwenden. Wenn der Container beispielsweise auf ein Fünftel seiner normalen Größe verkleinert wird, können Sie den Inhalt um das 5-Fache hochskalieren, um zu verhindern, dass er zusammengedrängt wird. Hierbei sind zwei Dinge zu beachten:

  1. Die Gegentransformation ist ebenfalls ein Skalierungsvorgang. Dies ist gut, da es ebenso wie die Animation auf dem Container beschleunigt werden kann. Möglicherweise müssen Sie dafür sorgen, dass die animierten Elemente eine eigene Kompositor-Ebene erhalten (die GPU unterstützt dies). Dazu können Sie dem Element will-change: transform oder backface-visiblity: hidden hinzufügen, wenn Sie ältere Browser unterstützen müssen.

  2. Die Counter-Transformation muss pro Frame berechnet werden. An dieser Stelle kann es etwas knifflig sein, denn angenommen, dass die Animation in CSS ist und eine Easing-Funktion verwendet, muss das Easing selbst bei der Animation der Zählertransformation rückgängig gemacht werden. Die Berechnung der Umkehrkurve für – etwa – cubic-bezier(0, 0, 0.3, 1) ist jedoch nicht so offensichtlich.

Es mag verlockend sein, den Effekt mit JavaScript zu animieren. Schließlich könnten Sie dann eine Easing-Gleichung verwenden, um die Skalierungs- und Zählerwerte pro Frame zu berechnen. Der Nachteil jeder JavaScript-basierten Animation besteht darin, was passiert, wenn der Hauptthread (in dem Ihr JavaScript ausgeführt wird) mit einer anderen Aufgabe beschäftigt ist. Kurz gesagt: Die Animation kann ruckeln oder ganz anhalten, was für UX nicht gut ist.

Schritt 2: CSS-Animationen spontan erstellen

Die Lösung, die Ihnen auf den ersten Blick vielleicht seltsam erscheinen kann, besteht darin, eine Keyframe-Animation mit unserer eigenen Easing-Funktion dynamisch zu erstellen und in die Seite einzufügen, um sie über das Menü zu verwenden. Vielen Dank an den Chrome-Entwickler Robert Flack für den Hinweis. Der Hauptvorteil besteht darin, dass eine Keyframe-Animation, die Transformationen verändert, auf dem Compositor ausgeführt werden kann, sodass sie nicht von Aufgaben im Hauptthread beeinflusst wird.

Für die Keyframe-Animation gehen wir von 0 bis 100 und berechnen, welche Skalierungswerte für das Element und seine Inhalte erforderlich sind. Diese können dann in einen String verkleinert werden, der als Stilelement in die Seite eingefügt werden kann. Durch das Einfügen der Stile wird auf der Seite ein Vorgang zur Neuberechnung von Stilen ausgelöst. Dies ist ein zusätzlicher Aufwand für den Browser, der jedoch nur einmal beim Hochfahren der Komponente durchgeführt wird.

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

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

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

Die unendlich Neugierigen wundern sich vielleicht über die ease()-Funktion in der for-Schleife. So können Sie Werte von 0 bis 1 einem äquivalenten Äquivalent zuordnen.

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

Sie können auch die Google-Suche verwenden, um sich ein Diagramm anzeigen zu lassen. Sehr praktisch! Wenn Sie andere Easing-Gleichungen benötigen, sehen Sie sich Tween.js by Soledad Penadés an, das einen ganzen Haufen enthält.

Schritt 3: CSS-Animationen aktivieren

Nachdem diese Animationen in JavaScript erstellt und in die Seite integriert sind, besteht der letzte Schritt darin, die Klassen umzuschalten, die die Animationen aktivieren.

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

Dadurch werden die Animationen ausgeführt, die Sie im vorherigen Schritt erstellt haben. Da die fixierten Animationen bereits gelockert wurden, muss die Timing-Funktion auf linear gesetzt sein. Andernfalls wechseln Sie zwischen den einzelnen Keyframes weiter, was sehr seltsam aussieht.

Es gibt zwei Möglichkeiten, das Element wieder nach unten zu minimieren: Aktualisieren Sie die CSS-Animation, sodass sie rückwärts statt vorwärts ausgeführt wird. Das funktioniert problemlos, aber das "Gefühl" der Animation wird umgekehrt, d. h., wenn Sie eine Easing-Kurve verwenden, wirkt die Umkehrung in ruhiger, wodurch sie sich langsam anfühlt. Eine geeignetere Lösung besteht darin, ein zweites Paar von Animationen zum Minimieren des Elements zu erstellen. Diese können auf die gleiche Weise wie die Animationen für das Maximieren von Keyframes erstellt werden, jedoch mit vertauschten Start- und Endwerten.

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

Eine fortgeschrittenere Version: kreisförmige Enthüllungen

Mit dieser Technik lassen sich auch kreisförmige Maximierungs- und Minimierungsanimationen erstellen.

Die Prinzipien sind im Wesentlichen die gleichen wie in der vorherigen Version, in der Sie ein Element skalieren und seine unmittelbaren untergeordneten Elemente zählen. In diesem Fall hat das Element, das hochskaliert wird, einen border-radius von 50%, wodurch es kreisförmig ist und von einem anderen Element mit overflow: hidden umschlossen ist. Das bedeutet, dass der Kreis nicht außerhalb der Elementgrenzen maximiert wird.

Eine Warnung zu dieser Variante: Chrome zeigt auf Bildschirmen mit niedriger DPI während der Animation verschwommenen Text an, da Rundungsfehler aufgrund der Skalierung und Zählerskalierung des Texts auftreten. Wenn Sie sich dafür interessieren, finden Sie hier einen Programmfehler, den Sie markieren und folgen können.

Den Code für den kreisförmigen Maximierungseffekt finden Sie im GitHub-Repository.

Ergebnisse

Das ist eine Möglichkeit, mit Skalierungstransformationen beeindruckende Clipanimationen zu erzeugen. In einer perfekten Welt wäre es toll, wenn Clipanimationen beschleunigt werden würden (dazu gibt es einen Chromium-Programmfehler von Jake Archibald). Bis wir diese Phase erreicht haben, solltest du bei der Animation von clip oder clip-path vorsichtig sein und auf keinen Fall width oder height animieren.

Für solche Effekte wäre es auch praktisch, Web Animations zu verwenden, da sie eine JavaScript API haben, aber im Compositor-Thread ausgeführt werden können, wenn Sie nur transform und opacity animieren. Leider ist die Unterstützung für Webanimationen schlecht, aber Sie können Progressive Enhancement verwenden, um sie zu verwenden, falls sie verfügbar sind.

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

Solange sich dies noch nicht ändert, können Sie für die Animation JavaScript-basierte Bibliotheken verwenden. Möglicherweise erzielen Sie aber eine zuverlässigere Leistung, wenn Sie eine CSS-Animation verankern und stattdessen diese verwenden. Wenn Ihre Anwendung bereits auf JavaScript für ihre Animationen angewiesen ist, sind Sie möglicherweise besser bedient, wenn Sie zumindest mit Ihrer vorhandenen Codebasis konsistent sind.

Wenn Sie sich den Code für diesen Effekt ansehen möchten, werfen Sie einen Blick auf das GitHub-Repository für UI-Elementbeispiele. Wie immer freuen wir uns über die Kommentare unten.