Unschärfe animieren

Unwichtige Elemente können durch Unkenntlichmachen ausgeblendet werden, um den Fokus des Nutzers auf das Wesentliche zu lenken. Wenn einige visuelle Elemente unscharf erscheinen, während andere Elemente im Fokus bleiben, wird der Fokus des Nutzers auf natürliche Weise gelenkt. Nutzer ignorieren die unkenntlich gemachten Inhalte und konzentrieren sich stattdessen auf die lesbaren Inhalte. Ein Beispiel wäre eine Liste von Symbolen, bei denen beim Bewegen des Mauszeigers Details zu den einzelnen Elementen angezeigt werden. Während dieser Zeit können die verbleibenden Optionen unkenntlich gemacht werden, um den Nutzer zu den neu angezeigten Informationen weiterzuleiten.

Kurzfassung

Eine Unschärfe zu animieren ist keine wirkliche Option, da das sehr langsam ist. Stattdessen können Sie eine Reihe von zunehmend unscharfen Versionen vorab berechnen und zwischen ihnen einen Weichzeichnereffekt verwenden. Mein Kollege Yi Gu hat eine Bibliothek geschrieben, die alles für Sie erledigt. Sehen Sie sich unsere Demo an.

Diese Methode kann jedoch sehr irritierend sein, wenn sie ohne Übergangsphase angewendet wird. Das Animieren einer Unkenntlichmachung – also der Übergang von einem unscharfen Bild zu einem unscharfen Bild – scheint eine vernünftige Wahl zu sein. Wenn Sie dies jedoch schon einmal im Web versucht haben, haben Sie wahrscheinlich festgestellt, dass die Animationen alles andere als flüssig sind, wie diese Demo zeigt, wenn Sie keinen leistungsstarken Computer haben. Können wir etwas besser machen?

Das Problem

Das Markup wird von der CPU in Texturen umgewandelt. Texturen werden auf die GPU hochgeladen. Die GPU zeichnet diese Texturen mithilfe von Shadern in den Framebuffer. Die Unkenntlichmachung erfolgt im Shader.

Derzeit können wir die Unkenntlichmachung nicht effizient animieren. Wir können jedoch eine Lösung finden, die gut genug aussieht, aber technisch gesehen keine animierte Unschärfe ist. Zuerst wollen wir herausfinden, warum die animierte Unkenntlichmachung so langsam ist. Es gibt zwei Möglichkeiten, Elemente im Web zu verwischen: die CSS-Eigenschaft filter und SVG-Filter. Aufgrund der besseren Unterstützung und der einfachen Verwendung werden in der Regel CSS-Filter verwendet. Wenn Sie Internet Explorer unterstützen müssen, haben Sie leider keine andere Wahl, als SVG-Filter zu verwenden, da diese von IE 10 und 11 unterstützt werden, CSS-Filter jedoch nicht. Die gute Nachricht ist, dass unsere Lösung zum Animieren eines Weichzeichners mit beiden Techniken funktioniert. Sehen wir uns also die DevTools an, um das Problem zu finden.

Wenn Sie „Paint Flashing“ in den Entwicklertools aktivieren, werden keine Blitze angezeigt. Offenbar werden keine Neuanstriche durchgeführt. Und das ist technisch korrekt, da „Neumalen“ bedeutet, dass die CPU die Textur eines hervorgehobenen Elements neu malen muss. Wenn ein Element sowohl hervorgehoben als auch unscharf gestellt wird, wird die Unschärfe von der GPU mithilfe eines Shaders angewendet.

Sowohl SVG- als auch CSS-Filter verwenden Glättungsfilter, um einen Weichzeichner anzuwenden. Diese Filter sind relativ teuer, da für jedes Ausgabepixel eine Reihe von Eingabepixeln berücksichtigt werden muss. Je größer das Bild oder der Unkenntlichmachungsradius ist, desto höher ist der Aufwand für den Effekt.

Und genau hier liegt das Problem: Wir führen bei jedem Frame eine ziemlich teure GPU-Operation aus, wodurch unser Frame-Budget von 16 ms überschritten wird und wir weit unter 60 fps liegen.

Down the Rabbit Hole

Was können wir tun, damit alles reibungslos funktioniert? Wir können Zaubertricks anwenden! Anstatt den tatsächlichen Weichzeichnerwert (den Radius der Unschärfe) zu animieren, berechnen wir vorab einige unscharfe Kopien, bei denen der Weichzeichnerwert exponentiell ansteigt, und führen dann mit opacity einen Cross-Fade zwischen ihnen aus.

Der Cross-Fade besteht aus einer Reihe überlappender Überblendungen mit unterschiedlicher Deckkraft. Wenn wir beispielsweise vier Unkenntlichmachungsstufen haben, blenden wir die erste Stufe aus und gleichzeitig die zweite ein. Sobald die zweite Stufe eine Deckkraft von 100% und die erste eine Deckkraft von 0 % erreicht hat, wird die zweite Stufe ausgeblendet und die dritte eingeblendet. Danach blenden wir die dritte Phase aus und die vierte und letzte Version ein. In diesem Szenario würde jede Phase ¼ der gesamten gewünschten Dauer in Anspruch nehmen. Visuell sieht das sehr ähnlich aus wie eine echte, animierte Unschärfe.

In unseren Tests haben wir festgestellt, dass die besten visuellen Ergebnisse erzielt werden, wenn der Unkenntlichmachungsradius pro Stufe exponentiell erhöht wird. Beispiel: Wenn wir vier Unkenntlichmachungsstufen haben, wenden wir filter: blur(2^n) auf jede Stufe an, also Stufe 0: 1 px, Stufe 1: 2 px, Stufe 2: 4 px und Stufe 3: 8 px. Wenn wir jede dieser unscharfen Kopien mit will-change: transform auf eine eigene Ebene legen (dies wird als „Hochstufen“ bezeichnet), sollte sich die Deckkraft dieser Elemente superschnell ändern lassen. Theoretisch könnten wir so die kostenintensive Arbeit des Unkenntlichmachens im Voraus erledigen. Die Logik ist jedoch fehlerhaft. Wenn Sie diese Demo ausführen, sehen Sie, dass die Framerate immer noch unter 60 fps liegt und die Unschärfe sogar schlimmer ist als zuvor.

In den DevTools wird ein Trace angezeigt, in dem die GPU lange Zeit ausgelastet ist.

Ein kurzer Blick in die Entwicklertools zeigt, dass die GPU immer noch extrem ausgelastet ist und jeden Frame auf etwa 90 ms ausdehnt. Aber warum? Wir ändern den Unkenntlichmachungswert nicht mehr, sondern nur die Deckkraft. Was passiert also? Das Problem liegt wieder einmal in der Natur des Weichzeichnereffekts: Wie bereits erwähnt, wird der Effekt von der GPU angewendet, wenn das Element sowohl vorangestellt als auch unscharf gestellt wird. Auch wenn wir den Unschärfewert nicht mehr animieren, ist die Textur selbst noch nicht unscharf und muss von der GPU in jedem Frame neu unscharf gestellt werden. Der Grund dafür, dass die Framerate noch schlechter ist als zuvor, liegt daran, dass die GPU im Vergleich zur naiven Implementierung mehr Arbeit hat als zuvor, da die meiste Zeit zwei Texturen sichtbar sind, die unabhängig voneinander unscharf gestellt werden müssen.

Unsere Lösung ist nicht schön, aber sie macht die Animation blitzschnell. Wir kehren dazu zurück, das Element, das unkenntlich gemacht werden soll, nicht zu bewerben, sondern stattdessen einen übergeordneten Wrapper. Wenn ein Element sowohl unscharf als auch hervorgehoben ist, wird der Effekt von der GPU angewendet. Das hat unsere Demo verlangsamt. Wenn das Element unscharf ist, aber nicht hochgestuft wurde, wird die Unschärfe stattdessen auf die nächstgelegene übergeordnete Textur gerastert. In unserem Fall ist das das übergeordnete Wrapper-Element. Das unscharfe Bild ist jetzt die Textur des übergeordneten Elements und kann für alle zukünftigen Frames wiederverwendet werden. Das funktioniert nur, weil wir wissen, dass die unscharfen Elemente nicht animiert sind und das Caching tatsächlich von Vorteil ist. In dieser Demo wird diese Technik implementiert. Ich frage mich, was das Moto G4 von diesem Ansatz hält? Spoileralarm: Google findet es großartig:

In den DevTools wird ein Trace angezeigt, in dem die GPU viel Leerlaufzeit hat.

Jetzt haben wir viel Spielraum bei der GPU und eine flüssige Bildrate von 60 fps. Wir haben es geschafft!

Zur Produktionsreife bringen

In unserer Demo haben wir eine DOM-Struktur mehrmals dupliziert, um Kopien des Inhalts mit unterschiedlicher Stärke zu verwischen. Sie fragen sich vielleicht, wie das in einer Produktionsumgebung funktionieren würde, da dies unerwünschte Auswirkungen auf die CSS-Stile oder sogar das JavaScript des Autors haben könnte. Sie haben Recht. Hier kommt das Shadow-DOM ins Spiel.

Die meisten Menschen betrachten das Shadow DOM als Möglichkeit, „interne“ Elemente an ihre benutzerdefinierten Elemente anzuhängen. Es ist aber auch ein Isolations- und Leistungselement. JavaScript und CSS können die Grenzen des Shadow DOM nicht durchdringen. So können wir Inhalte duplizieren, ohne die Stile oder die Anwendungslogik des Entwicklers zu beeinträchtigen. Wir haben bereits ein <div>-Element für jede Kopie, auf die gerastert werden soll, und verwenden diese <div> jetzt als Schattenhosts. Wir erstellen eine ShadowRoot mit attachShadow({mode: 'closed'}) und hängen eine Kopie der Inhalte an die ShadowRoot an, anstatt sie an die <div> selbst anzuhängen. Wir müssen auch alle Stylesheets in die ShadowRoot kopieren, damit unsere Kopien denselben Stil wie das Original haben.

Einige Browser unterstützen Shadow DOM v1 nicht. In diesen Fällen duplizieren wir die Inhalte und hoffen, dass nichts kaputtgeht. Wir hätten die Shadow DOM-Polyfill mit ShadyCSS verwenden können, haben sie aber nicht in unserer Bibliothek implementiert.

So haben wir es geschafft: Nach unserer Reise durch die Rendering-Pipeline von Chrome haben wir herausgefunden, wie wir Unkenntlichmachungen effizient in allen Browsern animieren können.

Fazit

Diese Art von Effekt sollte nicht leichtfertig verwendet werden. Da wir DOM-Elemente kopieren und auf eine eigene Ebene zwingen, können wir die Grenzen von Geräten der unteren Preisklasse ausreizen. Das Kopieren aller Stylesheets in jede ShadowRoot ist ebenfalls ein potenzielles Leistungsrisiko. Sie sollten daher entscheiden, ob Sie Ihre Logik und Stile lieber so anpassen möchten, dass sie nicht von Kopien in der LightDOM betroffen sind, oder ob Sie unsere ShadowDOM-Methode verwenden möchten. Manchmal kann unsere Methode jedoch eine lohnende Investition sein. Sehen Sie sich den Code in unserem GitHub-Repository und die Demo an. Bei Fragen können Sie sich jederzeit gern an mich wenden: Twitter.