Unschärfe animieren

Durch Unschärfe lässt sich die Aufmerksamkeit des Nutzers auf andere Bereiche lenken. Wenn einige visuelle Elemente verschwommen dargestellt werden, während andere im Fokus bleiben, wird der Blick des Nutzers auf natürliche Weise gelenkt. Nutzer ignorieren die unkenntlich gemachten Inhalte und konzentrieren sich stattdessen auf die Inhalte, die sie lesen können. Ein Beispiel wäre eine Liste mit Symbolen, die Details zu den einzelnen Elementen anzeigen, wenn der Mauszeiger darauf bewegt wird. Während dieser Zeit könnten die verbleibenden Auswahlmöglichkeiten verschwommen dargestellt werden, um den Nutzer auf die neu angezeigten Informationen zu lenken.

Kurzfassung

Das Animieren eines Weichzeichners ist keine gute Option, da es sehr langsam ist. Stattdessen können Sie eine Reihe von immer stärker verschwommenen Versionen vorab berechnen und zwischen ihnen überblenden. Mein Kollege Yi Gu hat eine Bibliothek geschrieben, die sich um alles kümmert. Demo ansehen

Wenn sie ohne Übergangszeit angewendet wird, kann sie jedoch sehr abrupt wirken. Das Animieren einer Unschärfe – der Übergang von nicht verschwommen zu verschwommen – scheint eine vernünftige Wahl zu sein. Wenn Sie das jedoch schon einmal im Web versucht haben, haben Sie wahrscheinlich festgestellt, dass die Animationen alles andere als flüssig sind. Das Demo zeigt, wie es aussieht, wenn Sie keinen leistungsstarken Computer haben. Können wir es 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. Das Unkenntlichmachen erfolgt im Shader.

Derzeit ist es nicht möglich, das Animieren eines Weichzeichners effizient zu gestalten. Wir können jedoch einen Workaround finden, der gut genug aussieht, aber technisch gesehen keine animierte Unschärfe ist. Zuerst müssen wir verstehen, warum die animierte Unschärfe langsam ist. Es gibt zwei Methoden, um Elemente im Web zu verschwimmen: die CSS-Eigenschaft filter und SVG-Filter. Aufgrund der besseren Unterstützung und Nutzerfreundlichkeit 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 IE 10 und 11 diese, aber keine CSS-Filter unterstützen. Die gute Nachricht ist, dass unser Workaround zum Animieren einer Unschärfe mit beiden Techniken funktioniert. Lassen Sie uns also versuchen, den Engpass zu finden, indem wir uns die Entwicklertools ansehen.

Wenn Sie in den Entwicklertools „Paint Flashing“ aktivieren, werden keine Blitzeffekte angezeigt. Es scheint, dass keine Neulackierungen stattfinden. Das ist technisch korrekt, da sich „Neuzeichnen“ darauf bezieht, dass die CPU die Textur eines beworbenen Elements neu zeichnen muss. Wenn ein Element sowohl beworben als auch unkenntlich gemacht wird, wird die Unkenntlichmachung von der GPU mithilfe eines Shaders angewendet.

Sowohl SVG- als auch CSS-Filter verwenden Faltungsfilter, um einen Weichzeichner anzuwenden. Faltungsfilter sind relativ rechenintensiv, da für jedes Ausgabepixel eine Reihe von Eingabepixeln berücksichtigt werden muss. Je größer das Bild oder der Unschärferadius ist, desto mehr Ressourcen werden für den Effekt benötigt.

Und genau hier liegt das Problem: Wir führen in jedem Frame einen ziemlich rechenintensiven GPU-Vorgang aus, wodurch unser Frame-Budget von 16 ms überschritten wird und wir weit unter 60 fps landen.

Down the rabbit hole

Was können wir also tun, damit alles reibungslos abläuft? Wir können uns eines Tricks bedienen. Anstatt den tatsächlichen Unschärfewert (den Radius der Unschärfe) zu animieren, werden einige unscharfe Kopien vorab berechnet, bei denen der Unschärfewert exponentiell ansteigt. Anschließend wird mit opacity zwischen ihnen überblendet.

Die Überblendung besteht aus einer Reihe von sich überlappenden Ein- und Ausblendungen der Deckkraft. Wenn wir beispielsweise vier Unschärfephasen haben, blenden wir die erste Phase aus, während wir gleichzeitig die zweite Phase einblenden. Sobald die zweite Phase 100% Deckkraft erreicht hat und die erste Phase 0%, blenden wir die zweite Phase aus und die dritte Phase ein. Danach blenden wir die dritte Phase aus und die vierte und letzte Version ein. In diesem Szenario würde jede Phase ein Viertel der gewünschten Gesamtdauer in Anspruch nehmen. Optisch sieht das sehr ähnlich wie eine echte, animierte Unschärfe aus.

In unseren Tests haben wir die besten visuellen Ergebnisse erzielt, indem wir den Unschärferadius pro Phase exponentiell erhöht haben. Beispiel: Wenn wir vier Unschärfephasen haben, wenden wir filter: blur(2^n) auf jede Phase an, d.h. Phase 0: 1 px, Phase 1: 2 px, Phase 2: 4 px und Phase 3: 8 px. Wenn wir jede dieser verschwommenen Kopien mit will-change: transform auf eine eigene Ebene verschieben (sogenanntes „Promoting“), sollte das Ändern der Deckkraft dieser Elemente sehr schnell gehen. Theoretisch könnten wir so die aufwendige Arbeit des Unkenntlichmachens vorziehen. Es stellt sich heraus, dass die Logik fehlerhaft ist. Wenn Sie diese Demo ausführen, sehen Sie, dass die Framerate immer noch unter 60 fps liegt und die Unschärfe sogar schlimmer als zuvor ist.

DevTools mit einem Trace, in dem die GPU über lange Zeiträume ausgelastet ist.

Ein kurzer Blick in die Entwicklertools zeigt, dass die GPU immer noch sehr beschäftigt ist und jeden Frame auf etwa 90 ms dehnt. Aber warum? Wir ändern nicht mehr den Unschärfewert, sondern nur noch die Deckkraft. Was ist los? Das Problem liegt wieder einmal in der Art des Unschärfeeffekts: Wie bereits erklärt, wird der Effekt von der GPU angewendet, wenn das Element sowohl beworben als auch unscharf dargestellt wird. Auch wenn wir den Unschärfewert nicht mehr animieren, ist die Textur selbst immer noch nicht unscharf und muss von der GPU in jedem Frame neu unscharf gemacht werden. Der Grund für die noch schlechtere Framerate als zuvor liegt darin, dass die GPU im Vergleich zur naiven Implementierung tatsächlich mehr Arbeit hat, da die meiste Zeit zwei Texturen sichtbar sind, die unabhängig voneinander verschwommen dargestellt werden müssen.

Das Ergebnis ist nicht schön, aber die Animation ist unglaublich schnell. Wir kehren dazu zurück, das zu unkenntlich machende Element nicht zu bewerben, sondern stattdessen einen übergeordneten Wrapper. Wenn ein Element sowohl unkenntlich gemacht als auch hervorgehoben wird, wird der Effekt von der GPU angewendet. Das hat unsere Demo verlangsamt. Wenn das Element verschwommen, aber nicht hochgestuft wird, wird die Unschärfe stattdessen auf die nächstgelegene übergeordnete Textur gerastert. In unserem Fall ist das das beworbene übergeordnete Wrapper-Element. Das verschwommene 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 verschwommenen Elemente nicht animiert sind und das Caching tatsächlich von Vorteil ist. Hier finden Sie eine Demo, in der diese Technik implementiert ist. Was wohl das Moto G4 dazu sagt? Spoileralarm: Es findet es toll:

DevTools mit einem Trace, in dem die GPU viel Leerlaufzeit hat.

Jetzt haben wir viel Spielraum auf 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 zu haben, die mit unterschiedlicher Stärke verschwommen dargestellt werden können. Sie fragen sich vielleicht, wie das in einer Produktionsumgebung funktionieren würde, da dies einige unbeabsichtigte Nebenwirkungen mit den CSS-Stilen oder sogar dem JavaScript des Autors haben könnte. Du hast recht. Shadow DOM

Die meisten Leute denken bei Shadow DOM an eine Möglichkeit, „interne“ Elemente an ihre benutzerdefinierten Elemente anzuhängen. Es ist aber auch ein Primitiv für Isolation und Leistung. JavaScript und CSS können Shadow-DOM-Grenzen nicht durchdringen. So können wir Inhalte duplizieren, ohne die Styles oder die Anwendungslogik des Entwicklers zu beeinträchtigen. Wir haben bereits ein <div>-Element für jede zu rasternde Kopie und verwenden diese <div>-Elemente jetzt als Schatten-Hosts. Wir erstellen ein ShadowRoot mit attachShadow({mode: 'closed'}) und hängen eine Kopie der Inhalte an das ShadowRoot anstelle des <div> an. Wir müssen darauf achten, dass wir auch alle Stylesheets in den ShadowRoot kopieren, damit unsere Kopien genauso formatiert werden wie das Original.

Einige Browser unterstützen Shadow DOM v1 nicht. In diesem Fall wird der Inhalt einfach dupliziert und wir hoffen, dass nichts kaputt geht. Wir könnten das Shadow DOM-Polyfill mit ShadyCSS verwenden, haben dies aber nicht in unserer Bibliothek implementiert.

Das war's. Nach unserem Ausflug in die Rendering-Pipeline von Chrome haben wir herausgefunden, wie wir Unschärfen browserübergreifend effizient animieren können.

Fazit

Diese Art von Effekt sollte nicht leichtfertig eingesetzt werden. Da wir DOM-Elemente kopieren und sie auf eine eigene Ebene zwingen, können wir die Grenzen von Low-End-Geräten ausreizen. Das Kopieren aller Stylesheets in jedes ShadowRoot birgt ebenfalls ein potenzielles Leistungsrisiko. Sie sollten daher entscheiden, ob Sie Ihre Logik und Stile so anpassen möchten, dass sie nicht durch Kopien im LightDOM beeinträchtigt werden, oder ob Sie unsere ShadowDOM-Technik verwenden möchten. Manchmal kann es sich aber lohnen, in unsere Technik zu investieren. Sehen Sie sich den Code in unserem GitHub-Repository sowie die Demo an und Twitter, wenn Sie Fragen haben.