Unschärfe animieren

Unkenntlichmachungen sind eine gute Möglichkeit, die Aufmerksamkeit der Nutzenden zu lenken. Wenn einige visuelle Elemente verschwommen dargestellt werden, 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 den Inhalt, den sie lesen können. Ein Beispiel wäre eine Liste von Symbolen, die Details zu den einzelnen Elementen anzeigen, wenn der Mauszeiger darauf bewegt wird. Während dieser Zeit können die verbleibenden Auswahlmöglichkeiten unkenntlich gemacht werden, um den Nutzer zu den neu angezeigten Informationen weiterzuleiten.

Kurzfassung

Unschärfen zu animieren, ist sehr langsam. Berechnen Sie stattdessen eine Reihe von zunehmend unkenntlich gemachten Versionen im Voraus und überblenden Sie sie überblenden. Mein Kollege Yi Gu hat eine Bibliothek geschrieben, die sich um alles kümmert. Sehen Sie sich unsere Demo an.

Diese Methode kann jedoch sehr irritierend sein, wenn sie ohne Übergangszeit angewendet wird. Das Animieren einer Unkenntlichmachung – der Übergang von Unschärfe zu Unkenntlichmachung – scheint eine vernünftige Wahl zu sein. Wenn Sie dies jedoch schon einmal im Web ausprobiert haben, werden Sie wahrscheinlich feststellen, dass die Animationen alles andere als flüssig sind, wie diese Demo zeigt, wenn Sie kein leistungsstarkes Gerät haben. Können wir das besser?

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 Framepuffer. Die Unkenntlichmachung erfolgt
im Shader.

Derzeit ist es noch nicht möglich, Unkenntlichmachungen effizient zu animieren. Wir können jedoch eine Problemumgehung finden, die gut genug aussieht, aber technisch gesehen keine animierte Unschärfe ist. Sehen wir uns zuerst an, warum die animierte Unschärfe langsam ist. Es gibt zwei Techniken, um Elemente im Web unkenntlich zu machen: die CSS-Eigenschaft filter und SVG-Filter. Dank verbesserter Unterstützung und Nutzerfreundlichkeit werden in der Regel CSS-Filter verwendet. Wenn Internet Explorer unterstützt werden muss, haben Sie keine andere Wahl, als SVG-Filter zu verwenden, da diese von IE 10 und 11 unterstützt werden, aber nicht CSS-Filter. Die gute Nachricht ist, dass unsere Problemumgehung für die Animation einer Unkenntlichmachung mit beiden Techniken funktioniert. Sehen wir uns die Entwicklertools an, um den Engpass zu finden.

Wenn du in den Entwicklertools die Option „Farbblinken“ aktivierst, wird gar kein Blitz angezeigt. Es sieht so aus, als gäbe es keine Übermalungen. Das ist technisch richtig, denn eine "Übermalung" bezieht sich darauf, dass die CPU die Textur eines hochgestuften Elements neu streichen muss. Immer wenn ein Element heraufgestuft und unkenntlich gemacht wird, wird die Unkenntlichmachung von der GPU mithilfe eines Shaders angewendet.

Sowohl SVG- als auch CSS-Filter verwenden Faltungsfilter, um eine Unkenntlichmachung anzuwenden. Faltungsfilter sind relativ teuer, da für jedes Ausgabepixel eine Anzahl von Eingabepixeln berücksichtigt werden muss. Je größer das Bild oder der Weichzeichner-Radius, desto teurer ist der Effekt.

Und genau hier liegt das Problem: Wir führen für jeden Frame einen ziemlich teuren GPU-Vorgang aus, wodurch unser Frame-Budget von 16 ms überschritten wird und dadurch deutlich unter 60 fps liegt.

Ins Kaninchenloch

Was können wir also tun, damit dies reibungslos abläuft? Wir können auf Leichtigkeit zurückgreifen! Anstatt den tatsächlichen Unkenntlichmachungswert (den Radius der Unschärfe) zu animieren, berechnen wir einige unkenntlich gemachte Kopien, bei denen der Wert exponentiell zunimmt, im Voraus berechnet und dann mit opacity überblendet werden.

Beim Überblenden handelt es sich um eine Reihe von überlappenden Ein- und Ausblendungen mit Deckkraft. Wenn wir beispielsweise vier Unschärfephasen haben, wird die erste Phase ausgeblendet und die zweite Phase gleichzeitig ausgeblendet. Sobald die Deckkraft der zweiten Phase 100% und die erste Stufe 0 % erreicht hat, werden die Anzeigen in der zweiten Phase ausgeblendet und in der dritten Phase ausgeblendet. Danach werden wir die dritte Phase und die vierte und letzte Version ausblenden. In diesem Szenario würde jede Phase 1⁄4 der insgesamt gewünschten Dauer beanspruchen. Visuell ähnelt dies einer echten animierten Unschärfe.

In unseren Experimenten lieferte die exponentielle Erhöhung des Weichzeichner-Radius pro Phase die besten visuellen Ergebnisse. Beispiel: Wenn wir vier Unschärfephasen haben, würden wir filter: blur(2^n) auf jede Phase anwenden, d.h. Phase 0: 1 px, Phase 1: 2 px, Phase 2: 4 px und Phase 3: 8 px. Wenn wir jede dieser unkenntlich gemachten Kopien mithilfe von will-change: transform auf ihrer eigenen Ebene (das sogenannte „Hochstufen“) erzwingen, sollte das Ändern der Deckkraft dieser Elemente sehr schnell erfolgen. Theoretisch könnten wir so die kostspielige Arbeit des Unkenntlichmachens von vorne beginnen. Wie sich herausstellt, ist die Logik falsch. Wenn du diese Demo ausführst, wirst du feststellen, dass die Framerate immer noch unter 60 fps liegt und die Unkenntlichmachung schlechter als zuvor ist.

Die Entwicklertools zeigen einen Trace an, bei dem die GPU lange ausgelastet ist.

Ein kurzer Blick in die Entwicklertools zeigt, dass die GPU immer noch extrem ausgelastet ist und jeden Frame auf ca. 90 ms erweitert wird. Aber warum? Wir ändern nichts mehr den Wert für die Unkenntlichmachung, sondern nur die Deckkraft. Das Problem liegt wieder in der Art des Weichzeichnereffekts: Wie bereits erläutert, wird der Effekt von der GPU angewendet, wenn das Element sowohl hoch- als auch unkenntlich gemacht wird. Obwohl wir den Unkenntlichmachungswert nicht mehr animieren, ist die Textur selbst immer noch nicht verschwommen und muss von der GPU für jeden Frame neu unkenntlich gemacht werden. Der Grund für eine noch schlechtere Framerate als zuvor ist, 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 unkenntlich gemacht werden müssen.

Das ist zwar nicht schön, aber die Animation ist blitzschnell. Das Element, das unkenntlich gemacht werden soll, wird nicht hochgestuft, sondern ein übergeordneter Wrapper. Wenn ein Element sowohl unkenntlich gemacht als auch hochgestuft wird, wird der Effekt von der GPU angewendet. Das hat unsere Demo verlangsamt. Wenn das Element unkenntlich gemacht, aber nicht hochgestuft wird, wird die Weichzeichnung stattdessen mit der nächstgelegenen übergeordneten Textur gerastert. In unserem Fall ist das das übergeordnete Wrapper-Element, für das geworben wird. Das unkenntlich gemachte Bild ist jetzt die Textur des übergeordneten Elements und kann für alle zukünftigen Frames wiederverwendet werden. Dies funktioniert nur, weil wir wissen, dass die unkenntlich gemachten Elemente nicht animiert sind und die Speicherung im Cache tatsächlich von Vorteil ist. Hier finden Sie eine Demo, in der dieses Verfahren implementiert wird. Ich frage mich, was das Moto G4 von diesem Ansatz hält. Achtung, Spoiler: Es findet es super:

Die Entwicklertools zeigen einen Trace an, in dem die GPU viel inaktiv ist.

Jetzt haben wir viel Spielraum auf der GPU und 60 fps. Geschafft!

Zur Produktionsreife bringen

In unserer Demo haben wir eine DOM-Struktur mehrfach dupliziert, um Kopien der Inhalte zu erhalten, die bei unterschiedlichen Stärken unkenntlich gemacht werden können. Sie fragen sich vielleicht, wie das in einer Produktionsumgebung funktioniert, da es unbeabsichtigte Nebeneffekte mit den CSS-Stilen des Autors oder sogar dessen JavaScript haben kann. Du hast recht. Kommt in Shadow DOM!

Die meisten Nutzer betrachten Shadow DOM als Möglichkeit, „interne“ Elemente an ihre benutzerdefinierten Elemente anzuhängen, es ist aber auch eine Isolations- und Leistungsprinzip. JavaScript und CSS können die Shadow DOM-Grenzen nicht durchbrechen, sodass wir Inhalte duplizieren können, ohne die Stile oder die Anwendungslogik des Entwicklers zu beeinträchtigen. Wir haben bereits ein <div>-Element für jede Kopie zum Rastern und verwenden diese <div>-Elemente jetzt als Schattenhosts. Wir erstellen ein ShadowRoot mit attachShadow({mode: 'closed'}) und hängen eine Kopie des Inhalts an das ShadowRoot statt an das <div> selbst an. Außerdem müssen alle Stylesheets in ShadowRoot kopiert werden, damit unsere Kopien den gleichen Stil wie das Original erhalten.

Einige Browser unterstützen Shadow DOM v1 nicht. In diesen Fällen duplizieren wir den Inhalt und hoffen, dass nichts kaputtgeht. Wir könnten das Shadow DOM-Polyfill mit ShadyCSS verwenden, haben dies aber nicht in unserer Bibliothek implementiert.

Nach unserem Weg durch die Rendering-Pipeline von Chrome haben wir herausgefunden, wie sich Unkenntlichmachungen effizient über verschiedene Browser hinweg animieren lassen.

Fazit

Dieser Effekt sollte nicht leichtfertig eingesetzt werden. Da wir DOM-Elemente kopieren und auf ihrer eigenen Ebene erzwingen, können wir die Grenzen von Low-End-Geräten erweitern. Das Kopieren aller Stylesheets in jede ShadowRoot birgt ebenfalls ein potenzielles Leistungsrisiko. Entscheiden Sie daher, ob Sie Ihre Logik und Stile so anpassen möchten, dass sie nicht durch Kopien in LightDOM beeinflusst werden, oder ob Sie unser ShadowDOM-Verfahren verwenden möchten. Manchmal kann sich unsere Technik jedoch lohnen. Sehen Sie sich den Code in unserem GitHub-Repository und die Demo an. Bei Fragen können Sie sich auch gerne auf Twitter an mich wenden.