Animer un floutage

Le flou est un excellent moyen de rediriger l'attention d'un utilisateur. Le fait de rendre certains éléments visuels flous tout en gardant d'autres nets permet de diriger naturellement l'attention de l'utilisateur. Les utilisateurs ignorent le contenu flouté et se concentrent plutôt sur celui qu'ils peuvent lire. Par exemple, une liste d'icônes qui affichent des détails sur les éléments individuels lorsque l'utilisateur les survole. Pendant ce temps, les autres choix peuvent être floutés pour rediriger l'utilisateur vers les nouvelles informations affichées.

TL;DR

Animer un flou n'est pas vraiment une option, car c'est très lent. Au lieu de cela, 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'une image non floue à une image floue) 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 loin d'être fluides, comme le montre cette démonstration si vous n'avez pas une machine puissante. Pouvons-nous faire mieux ?

Problème

Le balisage est transformé en textures par le processeur. Les textures sont importées dans le GPU. Le GPU dessine ces textures dans le framebuffer à l'aide de nuanceurs. Le flou est appliqué dans le nuanceur.

Pour le moment, nous ne pouvons pas animer un flou de manière efficace. Nous pouvons toutefois trouver une solution de contournement qui semble suffisamment bien, mais qui, techniquement parlant, n'est pas un flou animé. Pour commencer, essayons de comprendre 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. Les filtres CSS sont généralement utilisés, car ils sont plus faciles à utiliser et bénéficient d'une meilleure compatibilité. 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. Bonne nouvelle : notre solution de contournement pour animer un flou fonctionne avec les deux techniques. Essayons donc de trouver le goulot d'étranglement en examinant les outils de développement.

Si vous activez l'option "Mise en évidence des zones de peinture" dans les outils de développement, vous ne verrez aucun clignotement. Il semble qu'aucune repeinte n'ait lieu. Techniquement, c'est correct, car une "repeinture" 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 mis en avant et flouté, le flou 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 un certain nombre de pixels d'entrée doivent être pris en compte pour chaque pixel de sortie. Plus l'image ou le rayon de flou sont grands, 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 tomber 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 enchaîné entre elles à l'aide de opacity.

Le fondu enchaîné est une série de fondues en entrée et en sortie qui se chevauchent. Par exemple, si nous avons quatre étapes de flou, nous diminuons l'opacité de la première étape tout en augmentant celle de la deuxième. Une fois que la deuxième étape atteint 100 % d'opacité et que la première atteint 0 %, nous diminuons l'opacité de la deuxième étape tout en augmentant celle de la troisième. Une fois cela fait, nous diminuons progressivement la luminosité de la troisième étape et augmentons progressivement celle de 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.

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

Outils de développement affichant une trace où le GPU est occupé pendant de longues périodes.

Un rapide coup d'œil dans les outils de développement 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, une fois de plus, dans la nature de l'effet de flou : comme expliqué précédemment, si l'élément est à la fois mis en avant et flouté, l'effet est appliqué par le GPU. Ainsi, 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 floutée à nouveau à chaque frame par le GPU. La raison pour laquelle la fréquence d'images est encore pire qu'avant est que, par rapport à l'implémentation naïve, le GPU a en fait plus de travail qu'avant, car la plupart du temps, deux textures sont visibles et doivent être floutées indépendamment.

Le résultat n'est pas très esthétique, mais l'animation est extrêmement rapide. Nous revenons à ne pas promouvoir l'élément à flouter, mais plutôt un wrapper parent. Si un élément est à la fois flou et mis en avant, l'effet est appliqué par le GPU. C'est ce qui a ralenti notre démo. Si l'élément est flou, mais pas mis en avant, le flou est rasterisé sur la texture parente la plus proche. Dans notre cas, il s'agit de l'élément wrapper parent mis en avant. L'image floutée est désormais la texture de l'élément parent et peut être réutilisée pour toutes les images suivantes. Cela ne fonctionne que parce que nous savons que les éléments flous ne sont pas animés et que la 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 :

Outils de développement affichant 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émo, nous avons dupliqué une structure DOM plusieurs fois pour obtenir des copies du contenu à flouter à différents niveaux d'intensité. Vous vous demandez peut-être comment cela fonctionnerait dans un environnement de production, car cela pourrait avoir des effets secondaires indésirables avec les styles CSS ou même le JavaScript de l'auteur. Vous avez raison. Découvrez le Shadow DOM !

Alors que la plupart des gens considèrent le Shadow DOM comme un moyen d'associer des éléments "internes" à leurs éléments personnalisés, il s'agit également d'une primitive d'isolation et de performances. JavaScript et CSS ne peuvent pas traverser les limites du Shadow DOM, 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 nous utilisons maintenant 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 veiller à copier toutes les feuilles de style dans ShadowRoot pour nous assurer que nos copies sont mises en forme de la même manière que l'original.

Certains navigateurs ne sont pas compatibles avec Shadow DOM v1. Pour ceux-ci, nous nous contentons de dupliquer le contenu et espérons 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 exploré le pipeline de rendu de Chrome, nous avons trouvé comment animer les flous efficacement dans tous les 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 à se trouver sur leur propre calque, nous pouvons repousser les limites des appareils bas de gamme. Copier toutes les feuilles de style dans chaque ShadowRoot présente également un risque potentiel pour les 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 LightDOM ou utiliser notre technique ShadowDOM. Mais parfois, notre technique peut être un investissement intéressant. Consultez le code dans notre dépôt GitHub, ainsi que la démonstration. N'hésitez pas à me contacter sur Twitter si vous avez des questions.