Animer un floutage

Le floutage est un excellent moyen de rediriger l'attention de l'utilisateur. Faire apparaître certains éléments visuels floutés tout en gardant d'autres éléments au premier plan oriente naturellement l'attention de l'utilisateur. Les utilisateurs ignorent le contenu flouté et se concentrent sur le contenu qu'ils peuvent lire. Par exemple, une liste d'icônes qui affichent des informations sur les éléments individuels lorsque vous pointez dessus. Pendant ce temps, les autres choix peuvent être floutés pour rediriger l'utilisateur vers les informations nouvellement affichées.

TL;DR

L'animation d'un flou n'est pas vraiment une option, car elle est très lente. À la place, précalculez une série de versions de plus en plus floues et effectuez un fondu enchaîné entre elles. Ma collègue Yi Gu a écrit une bibliothèque pour s'occuper de tout pour vous. Découvrez notre démonstration.

Toutefois, cette technique peut être assez choquante lorsqu'elle est appliquée sans période de transition. Animer un flou (passer d'un flou à un flou) semble être un choix raisonnable, mais si vous avez déjà essayé de le faire sur le Web, vous avez probablement constaté que les animations étaient tout sauf fluides, comme le montre cette démo si vous ne disposez pas d'une machine puissante. Pouvons-nous faire mieux ?

Problème

Le balisage est converti en textures par le processeur. Les textures sont importées dans le GPU. Le GPU dessine ces textures dans le frame buffer à l'aide de nuanceurs. Le floutage se produit dans le nuanceur.

Pour le moment, nous ne pouvons pas animer efficacement un flou. Nous pouvons toutefois trouver une solution de contournement qui semble assez bonne, mais qui, techniquement parlant, n'est pas un flou animé. Pour commencer, voyons pourquoi le flou animé est lent. Pour flouter des éléments sur le Web, il existe deux techniques: la propriété CSS filter et les filtres SVG. Grâce à une compatibilité et une facilité d'utilisation accrues, les filtres CSS sont généralement utilisés. Malheureusement, si vous devez prendre en charge Internet Explorer, vous n'avez pas d'autre choix que d'utiliser des filtres SVG, car IE 10 et 11 les prennent en charge, mais pas les filtres CSS. La bonne nouvelle est que notre solution de contournement pour animer un flou fonctionne avec les deux techniques. Essayons donc de trouver le goulot d'étranglement en examinant DevTools.

Si vous activez "Flashing de peinture" dans les outils de développement, aucun clignotement ne s'affiche. Il semble que les recolorations ne soient pas effectuées. Et c'est techniquement correct, car un "repaint" fait référence au fait que le processeur doit repeindre la texture d'un élément promu. Chaque fois qu'un élément est à la fois promu et flouté, le floutage est appliqué par le GPU à l'aide d'un nuanceur.

Les filtres SVG et CSS utilisent des filtres de convolution pour appliquer un flou. Les filtres de convolution sont assez coûteux, car pour chaque pixel de sortie, un certain nombre de pixels d'entrée doivent être pris en compte. Plus l'image est grande ou plus le rayon de floutage est important, plus l'effet est coûteux.

C'est là que réside le problème. Nous exécutons une opération GPU plutôt coûteuse à chaque frame, ce qui dépasse notre budget de 16 ms et nous fait donc passer bien en dessous de 60 fps.

Au fond du terrier

Que pouvons-nous faire pour que cela se passe bien ? Nous pouvons utiliser des tours de passe-passe ! Au lieu d'animer la valeur de flou réelle (le rayon du flou), nous précalculons quelques copies floutées où la valeur de flou augmente de manière exponentielle, puis nous effectuons un fondu entre elles à l'aide de opacity.

Le fondu croisé est une série de fondus d'entrée et de sortie d'opacité qui se chevauchent. Par exemple, si nous avons quatre étapes de floutage, nous atténuons la première étape tout en atténuant la deuxième étape en même temps. Une fois que la deuxième étape atteint une opacité de 100% et que la première a atteint 0%, nous faisons disparaître la deuxième étape tout en faisant apparaître la troisième. Une fois cela fait, nous atténuons la troisième étape et faisons apparaître la quatrième et dernière version. Dans ce scénario, chaque étape prendrait un quart de la durée totale souhaitée. Visuellement, cela ressemble beaucoup à un flou animé réel.

Dans nos tests, augmenter le rayon de flou de manière exponentielle à chaque étape a donné les meilleurs résultats visuels. Exemple: Si nous avons quatre étapes de floutage, nous appliquons filter: blur(2^n) à chaque étape, c'est-à-dire à l'étape 0: 1 px, à l'étape 1: 2 px, à l'étape 2: 4 px et à l'étape 3: 8 px. Si nous forçons chacune de ces copies floutées sur leur propre calque (appelé "promotion") à l'aide de will-change: transform, la modification de l'opacité de ces éléments devrait être super-super rapide. En théorie, cela nous permettrait de précharger le travail coûteux de floutage. Il s'avère que cette logique est erronée. Si vous exécutez cette démo, vous constaterez que le framerate est toujours inférieur à 60 FPS, et que le flou est pire qu'auparavant.

DevTools montrant une trace où le GPU présente de longues périodes d'occupation.

Un rapide coup d'œil dans DevTools révèle que le GPU est toujours extrêmement occupé et étire chaque frame à environ 90 ms. Mais pourquoi ? Nous ne modifions plus la valeur de flou, mais uniquement l'opacité. Que se passe-t-il ? Le problème réside, encore une fois, dans la nature de l'effet de floutage: comme expliqué précédemment, si l'élément est à la fois promu et flouté, l'effet est appliqué par le GPU. Par conséquent, même si nous n'animons plus la valeur de flou, la texture elle-même n'est toujours pas floutée et doit être à nouveau floutée à chaque frame par le GPU. La raison pour laquelle la fréquence d'images est encore pire qu'avant vient du fait que, par rapport à l'implémentation naïve, le GPU a en fait plus de travail qu'auparavant, car la plupart du temps, deux textures sont visibles et doivent être floutées indépendamment.

Ce que nous avons trouvé n'est pas joli, mais il rend l'animation extrêmement rapide. Nous revenons à la non promotion de l'élément à flouter, mais nous promouvons plutôt un wrapper parent. Si un élément est à la fois flouté et mis en avant, l'effet est appliqué par le GPU. C'est ce qui a ralenti notre démonstration. Si l'élément est flouté, mais pas promu, le flou est plutôt rastérisé sur la texture parente la plus proche. Dans notre cas, il s'agit de l'élément de wrapper parent promu. L'image floutée est désormais la texture de l'élément parent et peut être réutilisée pour tous les futurs frames. Cela ne fonctionne que parce que nous savons que les éléments floutés ne sont pas animés et que leur mise en cache est réellement bénéfique. Voici une démonstration qui implémente cette technique. Je me demande ce que le Moto G4 pense de cette approche. Alerte spoiler: il pense que c'est génial:

DevTools montrant une trace où le GPU a beaucoup de temps d'inactivité.

Nous avons maintenant beaucoup de marge de manœuvre sur le GPU et une fluidité de 60 FPS. On a réussi !

Mettre un modèle en production

Dans notre démonstration, nous avons dupliqué une structure DOM plusieurs fois pour avoir des copies du contenu à flouter à différentes intensités. Vous vous demandez peut-être comment cela fonctionnerait dans un environnement de production, car cela pourrait avoir des effets secondaires involontaires sur les styles CSS ou même le code JavaScript de l'auteur. Vous avez raison. Découvrez le Shadow DOM !

La plupart des gens considèrent le DOM ombragé comme un moyen d'associer des éléments "internes" à leurs éléments personnalisés, mais il s'agit également d'une primitive d'isolation et de performances. JavaScript et CSS ne peuvent pas percer les limites du DOM fantôme, ce qui nous permet de dupliquer le contenu sans interférer avec les styles ou la logique d'application du développeur. Nous disposons déjà d'un élément <div> pour chaque copie à rasteriser et utilisons désormais ces <div> comme hôtes fantômes. Nous créons un ShadowRoot à l'aide de attachShadow({mode: 'closed'}) et joignons une copie du contenu au ShadowRoot au lieu du <div> lui-même. Nous devons également copier toutes les feuilles de style dans le ShadowRoot pour nous assurer que nos copies sont stylisées de la même manière que l'original.

Certains navigateurs ne sont pas compatibles avec Shadow DOM v1. Pour eux, nous nous contentons de dupliquer le contenu en espérant que rien ne se casse. Nous pourrions utiliser le polyfill Shadow DOM avec ShadyCSS, mais nous ne l'avons pas implémenté dans notre bibliothèque.

Et voilà. Après avoir parcouru le pipeline de rendu de Chrome, nous avons découvert comment animer efficacement les flous dans les différents navigateurs.

Conclusion

Ce type d'effet ne doit pas être utilisé à la légère. Étant donné que nous copions les éléments DOM et les forçons sur leur propre couche, nous pouvons repousser les limites des appareils d'entrée de gamme. La copie de toutes les feuilles de style dans chaque ShadowRoot représente également un risque potentiel de performances. Vous devez donc décider si vous préférez ajuster votre logique et vos styles pour qu'ils ne soient pas affectés par les copies dans le LightDOM ou utiliser notre technique ShadowDOM. Toutefois, il peut arriver que notre technique soit un investissement intéressant. Consultez le code dans notre dépôt GitHub, ainsi que la démo, et contactez-moi sur Twitter si vous avez des questions.